diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml
index 7cd0dfcac..5c1c453dd 100644
--- a/.github/actions/setup-base/action.yml
+++ b/.github/actions/setup-base/action.yml
@@ -5,7 +5,7 @@ runs:
using: composite
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml
index 5c441f085..7f3f8b672 100644
--- a/.github/workflows/build_debian_src.yml
+++ b/.github/workflows/build_debian_src.yml
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
path: meshtasticd
diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml
deleted file mode 100644
index 32cd45000..000000000
--- a/.github/workflows/build_esp32.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Build ESP32
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-esp32:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build ESP32
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: esp32
- pio_env: ${{ inputs.board }}
- pio_target: build
- ota_firmware_source: firmware.bin
- ota_firmware_target: release/bleota.bin
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-esp32-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.bin
- release/*.elf
diff --git a/.github/workflows/build_esp32_c3.yml b/.github/workflows/build_esp32_c3.yml
deleted file mode 100644
index 161786f99..000000000
--- a/.github/workflows/build_esp32_c3.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Build ESP32-C3
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-esp32-c3:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build ESP32-C3
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: esp32
- pio_env: ${{ inputs.board }}
- pio_target: build
- ota_firmware_source: firmware-c3.bin
- ota_firmware_target: release/bleota-c3.bin
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-esp32c3-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.bin
- release/*.elf
diff --git a/.github/workflows/build_esp32_c6.yml b/.github/workflows/build_esp32_c6.yml
deleted file mode 100644
index 90cdcc78e..000000000
--- a/.github/workflows/build_esp32_c6.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Build ESP32-C6
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-esp32-c6:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build ESP32-C6
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: esp32
- pio_env: ${{ inputs.board }}
- pio_target: build
- ota_firmware_source: firmware-c3.bin
- ota_firmware_target: release/bleota-c3.bin
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-esp32c6-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.bin
- release/*.elf
diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml
deleted file mode 100644
index e5ed48e3e..000000000
--- a/.github/workflows/build_esp32_s3.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Build ESP32-S3
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-esp32-s3:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build ESP32-S3
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: esp32
- pio_env: ${{ inputs.board }}
- pio_target: build
- ota_firmware_source: firmware-s3.bin
- ota_firmware_target: release/bleota-s3.bin
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-esp32s3-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.bin
- release/*.elf
diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml
new file mode 100644
index 000000000..2ef67405a
--- /dev/null
+++ b/.github/workflows/build_firmware.yml
@@ -0,0 +1,66 @@
+name: Build
+
+on:
+ workflow_call:
+ inputs:
+ version:
+ required: true
+ type: string
+ platform:
+ required: true
+ type: string
+ pio_env:
+ required: true
+ type: string
+
+permissions: read-all
+
+jobs:
+ pio-build:
+ name: build-${{ inputs.platform }}
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ submodules: recursive
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+
+ - name: Set OTA firmware source and target
+ if: startsWith(inputs.platform, 'esp32')
+ id: ota_dir
+ env:
+ PIO_PLATFORM: ${{ inputs.platform }}
+ run: |
+ if [ "$PIO_PLATFORM" = "esp32s3" ]; then
+ echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT
+ echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT
+ elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then
+ echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT
+ echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT
+ elif [ "$PIO_PLATFORM" = "esp32" ]; then
+ echo "src=firmware.bin" >> $GITHUB_OUTPUT
+ echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Build ${{ inputs.platform }}
+ id: build
+ uses: meshtastic/gh-action-firmware@main
+ with:
+ pio_platform: ${{ inputs.platform }}
+ pio_env: ${{ inputs.pio_env }}
+ pio_target: build
+ ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
+ ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
+
+ - name: Store binaries as an artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
+ overwrite: true
+ path: |
+ release/*.bin
+ release/*.elf
+ release/*.uf2
+ release/*.hex
+ release/*-ota.zip
diff --git a/.github/workflows/build_nrf52.yml b/.github/workflows/build_nrf52.yml
deleted file mode 100644
index 312aeb372..000000000
--- a/.github/workflows/build_nrf52.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Build NRF52
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-nrf52:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build NRF52
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: nrf52
- pio_env: ${{ inputs.board }}
- pio_target: build
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-nrf52840-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.uf2
- release/*.elf
- release/*.hex
- release/*-ota.zip
diff --git a/.github/workflows/build_rpi2040.yml b/.github/workflows/build_rpi2040.yml
deleted file mode 100644
index 2abd7a839..000000000
--- a/.github/workflows/build_rpi2040.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Build RPI2040
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-rpi2040:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build Raspberry Pi 2040
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: rp2xx0
- pio_env: ${{ inputs.board }}
- pio_target: build
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-rp2040-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.uf2
- release/*.elf
diff --git a/.github/workflows/build_stm32.yml b/.github/workflows/build_stm32.yml
deleted file mode 100644
index 10680f422..000000000
--- a/.github/workflows/build_stm32.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Build STM32
-
-on:
- workflow_call:
- inputs:
- board:
- required: true
- type: string
-
-permissions: read-all
-
-jobs:
- build-stm32:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v4
-
- - name: Get release version string
- shell: bash
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - name: Build STM32WL
- id: build
- uses: meshtastic/gh-action-firmware@main
- with:
- pio_platform: stm32wl
- pio_env: ${{ inputs.board }}
- pio_target: build
-
- - name: Store binaries as an artifact
- uses: actions/upload-artifact@v4
- with:
- name: firmware-stm32-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
- overwrite: true
- path: |
- release/*.hex
- release/*.bin
- release/*.elf
diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml
index 63d24687b..eb61554f2 100644
--- a/.github/workflows/daily_packaging.yml
+++ b/.github/workflows/daily_packaging.yml
@@ -30,7 +30,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- series: [plucky, noble, jammy]
+ series:
+ - jammy # 22.04
+ - noble # 24.04
+ - plucky # 25.04
+ - questing # 25.10
uses: ./.github/workflows/package_ppa.yml
with:
ppa_repo: ppa:meshtastic/daily
diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml
index cde7fd274..26a9cff18 100644
--- a/.github/workflows/docker_build.yml
+++ b/.github/workflows/docker_build.yml
@@ -47,7 +47,7 @@ jobs:
runs-on: ${{ inputs.runs-on }}
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml
index d1d1a5634..20b9ceee6 100644
--- a/.github/workflows/docker_manifest.yml
+++ b/.github/workflows/docker_manifest.yml
@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml
index 94aaca49c..2204cc02c 100644
--- a/.github/workflows/hook_copr.yml
+++ b/.github/workflows/hook_copr.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
ref: ${{ github.ref }}
diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml
index a676efa1e..ed14907dc 100644
--- a/.github/workflows/main_matrix.yml
+++ b/.github/workflows/main_matrix.yml
@@ -30,18 +30,31 @@ jobs:
strategy:
fail-fast: false
matrix:
- arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check]
- runs-on: ubuntu-latest
+ arch:
+ - esp32
+ - esp32s3
+ - esp32c3
+ - esp32c6
+ - nrf52840
+ - rp2040
+ - rp2350
+ - stm32
+ - check
+ runs-on: ubuntu-24.04
steps:
- - id: checkout
- uses: actions/checkout@v4
- name: Checkout base
- - id: jsonStep
+ - uses: actions/checkout@v5
+ - 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}} quick)
+ 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
@@ -52,9 +65,25 @@ jobs:
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
+ rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
stm32: ${{ steps.jsonStep.outputs.stm32 }}
check: ${{ steps.jsonStep.outputs.check }}
+ version:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - 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:
@@ -64,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Build base
id: base
uses: ./.github/actions/setup-base
@@ -72,67 +101,92 @@ jobs:
run: bin/check-all.sh ${{ matrix.board }}
build-esp32:
- needs: setup
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
- uses: ./.github/workflows/build_esp32.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: esp32
- build-esp32-s3:
- needs: setup
+ build-esp32s3:
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
- uses: ./.github/workflows/build_esp32_s3.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: esp32s3
- build-esp32-c3:
- needs: setup
+ build-esp32c3:
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
- uses: ./.github/workflows/build_esp32_c3.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: esp32c3
- build-esp32-c6:
- needs: setup
+ build-esp32c6:
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
- uses: ./.github/workflows/build_esp32_c6.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: esp32c6
- build-nrf52:
- needs: setup
+ build-nrf52840:
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
- uses: ./.github/workflows/build_nrf52.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: nrf52840
- build-rpi2040:
- needs: setup
+ build-rp2040:
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
- uses: ./.github/workflows/build_rpi2040.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: rp2040
+
+ build-rp2350:
+ needs: [setup, version]
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
+ uses: ./.github/workflows/build_firmware.yml
+ with:
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: rp2350
build-stm32:
- needs: setup
+ needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
- uses: ./.github/workflows/build_stm32.yml
+ uses: ./.github/workflows/build_firmware.yml
with:
- board: ${{ matrix.board }}
+ version: ${{ needs.version.outputs.long }}
+ pio_env: ${{ matrix.board }}
+ platform: stm32
build-debian-src:
if: github.repository == 'meshtastic/firmware'
@@ -210,26 +264,36 @@ jobs:
strategy:
fail-fast: false
matrix:
- arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
+ arch:
+ - esp32
+ - esp32s3
+ - esp32c3
+ - esp32c6
+ - nrf52840
+ - rp2040
+ - rp2350
+ - stm32
runs-on: ubuntu-latest
needs:
[
+ version,
build-esp32,
- build-esp32-s3,
- build-esp32-c3,
- build-esp32-c6,
- build-nrf52,
- build-rpi2040,
+ build-esp32s3,
+ build-esp32c3,
+ build-esp32c6,
+ build-nrf52840,
+ build-rp2040,
+ build-rp2350,
build-stm32,
]
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -238,17 +302,13 @@ jobs:
- name: Display structure of downloaded files
run: ls -R
- - name: Get release version string
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- 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}}-${{ steps.version.outputs.long }}
+ name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
@@ -262,9 +322,9 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v5
with:
- name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
+ name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
@@ -278,12 +338,12 @@ jobs:
chmod +x ./output/device-update.sh
- name: Zip firmware
- run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./output
+ 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}}-${{ steps.version.outputs.long }}.zip
+ name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
overwrite: true
path: ./*.elf
retention-days: 30
@@ -291,8 +351,8 @@ jobs:
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
- name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
- description: "Download firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
+ 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:
@@ -301,56 +361,49 @@ jobs:
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
+ uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- - name: Get release version string
- run: |
- echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
- id: version
- env:
- BUILD_LOCATION: local
-
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
with:
draft: true
prerelease: true
- name: Meshtastic Firmware ${{ steps.version.outputs.long }} Alpha
- tag_name: v${{ steps.version.outputs.long }}
+ 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
+ uses: actions/download-artifact@v5
with:
- pattern: firmware-debian-${{ steps.version.outputs.deb }}~UNRELEASED-src
+ 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
+ uses: actions/download-artifact@v5
with:
- pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }}
+ 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-${{ steps.version.outputs.deb }}-src.zip ./debian-src
- zip -9 -r ./platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip ./pio-deps-native-tft
+ 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
@@ -360,8 +413,8 @@ jobs:
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
- gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip
- gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip
+ 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 }}
@@ -369,26 +422,30 @@ jobs:
strategy:
fail-fast: false
matrix:
- arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
+ arch:
+ - esp32
+ - esp32s3
+ - esp32c3
+ - esp32c6
+ - nrf52840
+ - rp2040
+ - rp2350
+ - stm32
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
- needs: [release-artifacts]
+ needs: [release-artifacts, version]
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- - name: Get release version string
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v5
with:
- pattern: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
+ pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
@@ -401,16 +458,16 @@ jobs:
chmod +x ./output/device-update.sh
- name: Zip firmware
- run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./output
+ run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v5
with:
- name: debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
+ 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}}-${{ steps.version.outputs.long }}.zip ./elfs
+ run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
# For diagnostics
- name: Display structure of downloaded files
@@ -420,33 +477,30 @@ jobs:
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
- gh release upload v${{ steps.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
- gh release upload v${{ steps.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
+ 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]
+ needs: [release-firmware, version]
env:
- targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32
+ targets: |-
+ esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- - name: Get release version string
- run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- id: version
-
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v5
with:
- pattern: firmware-{${{ env.targets }}}-${{ steps.version.outputs.long }}
+ pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
@@ -460,9 +514,9 @@ jobs:
external_repository: meshtastic/meshtastic.github.io
publish_branch: master
publish_dir: ./publish
- destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ steps.version.outputs.long }}
+ 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: ${{ steps.version.outputs.long }}
+ commit_message: ${{ needs.version.outputs.long }}
enable_jekyll: true
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 309772b12..f26073ec4 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Trunk Check
uses: trunk-io/trunk-action@v1
@@ -31,7 +31,7 @@ jobs:
pull-requests: write # For trunk to create PRs
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Trunk Upgrade
uses: trunk-io/trunk-action/upgrade@v1
diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml
index 275ffce0e..4c547eadc 100644
--- a/.github/workflows/package_obs.yml
+++ b/.github/workflows/package_obs.yml
@@ -34,7 +34,7 @@ jobs:
needs: build-debian-src
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
path: meshtasticd
@@ -58,7 +58,7 @@ jobs:
id: version
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml
index 9f535b7b1..13d3d1b4e 100644
--- a/.github/workflows/package_pio_deps.yml
+++ b/.github/workflows/package_pio_deps.yml
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml
index a54b0bd36..aece730a0 100644
--- a/.github/workflows/package_ppa.yml
+++ b/.github/workflows/package_ppa.yml
@@ -32,7 +32,7 @@ jobs:
needs: build-debian-src
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: recursive
path: meshtasticd
@@ -60,7 +60,7 @@ jobs:
id: version
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml
new file mode 100644
index 000000000..93114e2c7
--- /dev/null
+++ b/.github/workflows/pr_enforce_labels.yml
@@ -0,0 +1,24 @@
+name: Check PR Labels
+
+on:
+ pull_request:
+ types: [opened, edited, labeled, unlabeled, synchronize, reopened]
+
+permissions:
+ pull-requests: read
+ contents: read
+
+jobs:
+ check-label:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Check for PR labels
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const labels = context.payload.pull_request.labels.map(label => label.name);
+ const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk'];
+ const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
+ if (!hasRequiredLabel) {
+ core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
+ }
diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml
new file mode 100644
index 000000000..786feeced
--- /dev/null
+++ b/.github/workflows/pr_tests.yml
@@ -0,0 +1,238 @@
+name: Tests
+
+# DISABLED: Changed from automatic PR triggers to manual only
+on:
+ workflow_dispatch:
+ inputs:
+ reason:
+ description: "Reason for manual test run"
+ required: false
+ default: "Manual test execution"
+
+concurrency:
+ group: tests-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ actions: read
+ checks: write
+ pull-requests: write
+
+jobs:
+ native-tests:
+ name: "🧪 Native Tests"
+ if: github.repository == 'meshtastic/firmware'
+ uses: ./.github/workflows/test_native.yml
+ permissions:
+ contents: read
+ actions: read
+ checks: write
+
+ test-summary:
+ name: "📊 Test Results"
+ runs-on: ubuntu-latest
+ needs: [native-tests]
+ if: always()
+ permissions:
+ contents: read
+ actions: read
+ checks: write
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ submodules: recursive
+
+ - name: Get release version string
+ run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
+ id: version
+
+ - name: Download test artifacts
+ if: needs.native-tests.result != 'skipped'
+ uses: actions/download-artifact@v5
+ with:
+ name: platformio-test-report-${{ steps.version.outputs.long }}.zip
+ merge-multiple: true
+
+ - name: Parse test results and create detailed summary
+ id: test-results
+ run: |
+ echo "## 🧪 Test Results Summary" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Check overall job status first
+ if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
+ echo "✅ **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY
+ elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then
+ echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY
+ elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then
+ echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY
+ exit 0
+ else
+ echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Parse detailed test results if available
+ if [ -f "testreport.xml" ]; then
+ echo "### 🔍 Individual Test Results" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ python3 << 'EOF'
+ import xml.etree.ElementTree as ET
+ import os
+
+ try:
+ tree = ET.parse('testreport.xml')
+ root = tree.getroot()
+
+ total_tests = 0
+ passed_tests = 0
+ failed_tests = 0
+ skipped_tests = 0
+
+ # Parse testsuite elements
+ for testsuite in root.findall('.//testsuite'):
+ suite_name = testsuite.get('name', 'Unknown')
+ suite_tests = int(testsuite.get('tests', '0'))
+ suite_failures = int(testsuite.get('failures', '0'))
+ suite_errors = int(testsuite.get('errors', '0'))
+ suite_skipped = int(testsuite.get('skipped', '0'))
+
+ total_tests += suite_tests
+ failed_tests += suite_failures + suite_errors
+ skipped_tests += suite_skipped
+ passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped
+
+ if suite_tests > 0:
+ status = "✅" if (suite_failures + suite_errors) == 0 else "❌"
+ print(f"**{status} Test Suite: {suite_name}**")
+ print(f"- Total: {suite_tests}")
+ print(f"- Passed: ✅ {suite_tests - suite_failures - suite_errors - suite_skipped}")
+ print(f"- Failed: ❌ {suite_failures + suite_errors}")
+ if suite_skipped > 0:
+ print(f"- Skipped: ⏭️ {suite_skipped}")
+ print("")
+
+ # Show individual test results for failed suites
+ if suite_failures + suite_errors > 0:
+ print("**Failed Tests:**")
+ for testcase in testsuite.findall('testcase'):
+ test_name = testcase.get('name', 'Unknown')
+ failure = testcase.find('failure')
+ error = testcase.find('error')
+
+ if failure is not None:
+ msg = failure.get('message', 'Unknown error')[:100]
+ print(f"- ❌ `{test_name}`: {msg}")
+ elif error is not None:
+ msg = error.get('message', 'Unknown error')[:100]
+ print(f"- ❌ `{test_name}`: ERROR - {msg}")
+ print("")
+ else:
+ # Show passed tests for successful suites
+ passed_count = 0
+ for testcase in testsuite.findall('testcase'):
+ if testcase.find('failure') is None and testcase.find('error') is None:
+ if passed_count < 5: # Limit to first 5 to avoid spam
+ test_name = testcase.get('name', 'Unknown')
+ print(f"- ✅ `{test_name}`: PASSED")
+ passed_count += 1
+ if passed_count > 5:
+ print(f"- ... and {passed_count - 5} more tests passed")
+ print("")
+
+ # Summary statistics
+ print("### 📊 Test Statistics")
+ print(f"- **Total Tests**: {total_tests}")
+ print(f"- **Passed**: ✅ {passed_tests}")
+ print(f"- **Failed**: ❌ {failed_tests}")
+ if skipped_tests > 0:
+ print(f"- **Skipped**: ⏭️ {skipped_tests}")
+
+ if failed_tests > 0:
+ print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**")
+ else:
+ print(f"\n✅ **All {total_tests} tests passed!**")
+
+ except Exception as e:
+ print(f"❌ Error parsing test results: {e}")
+ EOF
+ else
+ echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY
+ fi
+
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "---" >> $GITHUB_STEP_SUMMARY
+ echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
+
+ - name: Comment test results on PR
+ if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+
+ // Read the step summary to use as PR comment
+ let testSummary = "## 🧪 Test Results Summary\n\n";
+
+ if ("${{ needs.native-tests.result }}" === "success") {
+ testSummary += "✅ **All tests passed!**\n\n";
+ } else if ("${{ needs.native-tests.result }}" === "failure") {
+ testSummary += "❌ **Some tests failed.**\n\n";
+ } else {
+ testSummary += "⚠️ **Tests did not complete normally.**\n\n";
+ }
+
+ testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`;
+ testSummary += "---\n";
+ testSummary += "*This comment will be automatically updated when new commits are pushed.*";
+
+ // Find existing comment
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number
+ });
+
+ const botComment = comments.data.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes('🧪 Test Results Summary')
+ );
+
+ if (botComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: testSummary
+ });
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: testSummary
+ });
+ }
+
+ - name: Set overall status
+ run: |
+ if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
+ echo "All tests passed! ✅"
+ exit 0
+ else
+ echo "Some tests failed! ❌"
+ exit 1
+ fi
diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml
index ed2de1717..ccd99e792 100644
--- a/.github/workflows/release_channels.yml
+++ b/.github/workflows/release_channels.yml
@@ -20,7 +20,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- series: [plucky, noble, jammy]
+ series:
+ - jammy # 22.04
+ - noble # 24.04
+ - plucky # 25.04
+ # - questing # 25.10
uses: ./.github/workflows/package_ppa.yml
with:
ppa_repo: |-
@@ -56,7 +60,7 @@ jobs:
shell: bash
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
@@ -99,8 +103,9 @@ jobs:
with:
base: ${{ github.event.repository.default_branch }}
branch: create-pull-request/bump-version
+ labels: github_actions
title: Bump release version
- commit-message: automated bumps
+ commit-message: Automated version bumps
add-paths: |
version.properties
debian/changelog
diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml
index e391aa07b..96c993cba 100644
--- a/.github/workflows/sec_sast_semgrep_cron.yml
+++ b/.github/workflows/sec_sast_semgrep_cron.yml
@@ -21,7 +21,7 @@ jobs:
steps:
# step 1
- name: clone application source code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# step 2
- name: full scan
diff --git a/.github/workflows/sec_sast_semgrep_pull.yml b/.github/workflows/sec_sast_semgrep_pull.yml
index 3707c91b8..e93b2ae8b 100644
--- a/.github/workflows/sec_sast_semgrep_pull.yml
+++ b/.github/workflows/sec_sast_semgrep_pull.yml
@@ -13,7 +13,7 @@ jobs:
steps:
# step 1
- name: clone application source code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml
index dc05959fd..6b788f4c7 100644
--- a/.github/workflows/test_native.yml
+++ b/.github/workflows/test_native.yml
@@ -14,7 +14,7 @@ jobs:
name: Native Simulator Tests
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -70,7 +70,7 @@ jobs:
name: Native PlatformIO Tests
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -127,7 +127,7 @@ jobs:
- platformio-tests
if: always()
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -137,7 +137,7 @@ jobs:
id: version
- name: Download test artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
merge-multiple: true
@@ -150,7 +150,7 @@ jobs:
reporter: java-junit
- name: Download coverage artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v5
with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
path: code-coverage-report
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 34b28b39c..52f180aa2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: test-runner
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# - uses: actions/setup-python@v5
# with:
diff --git a/.github/workflows/trunk_annotate_pr.yml b/.github/workflows/trunk_annotate_pr.yml
index 62c1c01b7..23dcf8d09 100644
--- a/.github/workflows/trunk_annotate_pr.yml
+++ b/.github/workflows/trunk_annotate_pr.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Trunk Check
uses: trunk-io/trunk-action@v1
diff --git a/.github/workflows/trunk_check.yml b/.github/workflows/trunk_check.yml
index 55656bf48..41731d491 100644
--- a/.github/workflows/trunk_check.yml
+++ b/.github/workflows/trunk_check.yml
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Trunk Check
uses: trunk-io/trunk-action@v1
diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml
index 33f4182eb..2d191fc44 100644
--- a/.github/workflows/trunk_format_pr.yml
+++ b/.github/workflows/trunk_format_pr.yml
@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml
index ccdcc19ae..c06e06b0a 100644
--- a/.github/workflows/update_protobufs.yml
+++ b/.github/workflows/update_protobufs.yml
@@ -11,7 +11,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
submodules: true
@@ -34,7 +34,9 @@ jobs:
uses: peter-evans/create-pull-request@v7
with:
branch: create-pull-request/update-protobufs
+ labels: submodules
title: Update protobufs and classes
+ commit-message: Update protobufs
add-paths: |
protobufs
src/mesh
diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index f0271c856..c57c16319 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -1,22 +1,22 @@
version: 0.1
cli:
- version: 1.24.0
+ version: 1.25.0
plugins:
sources:
- id: trunk
- ref: v1.7.1
+ ref: v1.7.2
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- - checkov@3.2.450
- - renovate@41.29.1
+ - checkov@3.2.467
+ - renovate@41.88.0
- prettier@3.6.2
- - trufflehog@3.89.2
+ - trufflehog@3.90.5
- yamllint@1.37.1
- bandit@1.8.6
- - trivy@0.64.1
- - taplo@0.9.3
- - ruff@0.12.2
+ - trivy@0.65.0
+ - taplo@0.10.0
+ - ruff@0.12.10
- isort@6.0.1
- markdownlint@0.45.0
- oxipng@9.1.5
@@ -25,10 +25,10 @@ lint:
- flake8@7.3.0
- hadolint@2.12.1-beta
- shfmt@3.6.0
- - shellcheck@0.10.0
+ - shellcheck@0.11.0
- black@25.1.0
- git-diff-check
- - gitleaks@8.27.2
+ - gitleaks@8.28.0
- clang-format@16.0.3
ignore:
- linters: [ALL]
diff --git a/Dockerfile b/Dockerfile
index e033b1bba..b1e151ac7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
-FROM python:3.13-bookworm AS builder
+FROM python:3.13-slim-trixie AS builder
ARG PIO_ENV=native
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
@@ -36,7 +36,7 @@ RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/fir
##### PRODUCTION BUILD #############
-FROM debian:bookworm-slim
+FROM debian:trixie-slim
LABEL org.opencontainers.image.title="Meshtastic" \
org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \
org.opencontainers.image.url="https://meshtastic.org" \
@@ -51,8 +51,8 @@ ENV TZ=Etc/UTC
USER root
RUN apt-get update && apt-get --no-install-recommends -y install \
- libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev \
- liborcania2.3 libulfius2.7 libssl3 \
+ libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
+ liborcania2.3 libulfius2.7t64 libssl3t64 \
libx11-6 libinput10 libxkbcommon-x11-0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \
@@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \
# Fetch compiled binary from the builder
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/
-COPY --from=builder /tmp/web /usr/share/meshtasticd/
+COPY --from=builder /tmp/web /usr/share/meshtasticd/web/
# Copy config templates
COPY ./bin/config.d /etc/meshtasticd/available.d
diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini
index 6b9ebcb24..8990053eb 100644
--- a/arch/esp32/esp32.ini
+++ b/arch/esp32/esp32.ini
@@ -54,8 +54,8 @@ lib_deps =
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=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
- lewisxhe/XPowersLib@0.3.0
+ # 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
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini
index 4a77ec4b2..36effe017 100644
--- a/arch/nrf52/nrf52.ini
+++ b/arch/nrf52/nrf52.ini
@@ -23,7 +23,7 @@ build_flags =
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
build_src_filter =
- ${arduino_base.build_src_filter} - - - - - - - - - -
+ ${arduino_base.build_src_filter} - - - - - - - - - - -
lib_deps=
${arduino_base.lib_deps}
diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini
index 03a8a6583..20b3f8e3d 100644
--- a/arch/portduino/portduino.ini
+++ b/arch/portduino/portduino.ini
@@ -2,7 +2,7 @@
[portduino_base]
platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
- https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip
+ https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip
framework = arduino
build_src_filter =
@@ -17,7 +17,6 @@ build_src_filter =
+
-
-
- +<../variants/portduino>
lib_deps =
${env.lib_deps}
@@ -35,11 +34,12 @@ lib_deps =
build_flags =
${arduino_base.build_flags}
+ -D ARCH_PORTDUINO
-fPIC
-Isrc/platform/portduino
-DRADIOLIB_EEPROM_UNSUPPORTED
-DPORTDUINO_LINUX_HARDWARE
- -DHAS_UDP_MULTICAST
+ -DHAS_UDP_MULTICAST=1
-lpthread
-lstdc++fs
-lbluetooth
diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini
index be1ed662f..d91607a7d 100644
--- a/arch/stm32/stm32.ini
+++ b/arch/stm32/stm32.ini
@@ -2,7 +2,7 @@
extends = arduino_base
platform =
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
- platformio/ststm32@19.2.0
+ platformio/ststm32@19.3.0
platform_packages =
# TODO renovate
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
@@ -23,14 +23,20 @@ build_flags =
-DMESHTASTIC_EXCLUDE_SCREEN=1
-DMESHTASTIC_EXCLUDE_MQTT=1
-DMESHTASTIC_EXCLUDE_BLUETOOTH=1
- -DMESHTASTIC_EXCLUDE_GPS=1
-DMESHTASTIC_EXCLUDE_WIFI=1
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
+ -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small.
+ -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported.
-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized.
- ;-DDEBUG_MUTE
+ -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs.
-fmerge-all-constants
-ffunction-sections
-fdata-sections
+ -DRADIOLIB_EXCLUDE_SX128X=1
+ -DRADIOLIB_EXCLUDE_SX127X=1
+ -DRADIOLIB_EXCLUDE_LR11X0=1
+ -DHAL_DAC_MODULE_ONLY
+ -DHAL_RNG_MODULE_ENABLED
build_src_filter =
${arduino_base.build_src_filter} - - - - - - - - - - - - - -
diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh
index c53f1b660..fdd7caa11 100644
--- a/bin/build-firmware.sh
+++ b/bin/build-firmware.sh
@@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then
elif (echo $2 | grep -q "stm32"); then
bin/build-stm32.sh $1
elif (echo $2 | grep -q "rpi2040"); then
- bin/build-rpi2040.sh $1
+ bin/build-rp2xx0.sh $1
else
echo "Unknown target $2"
exit 1
diff --git a/bin/config.d/lora-RAK6421.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml
similarity index 56%
rename from bin/config.d/lora-RAK6421.yaml
rename to bin/config.d/lora-RAK6421-13300-slot1.yaml
index bbf38a474..6f65f9ccd 100644
--- a/bin/config.d/lora-RAK6421.yaml
+++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml
@@ -9,13 +9,4 @@ Lora:
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
- # CS: 8
-
-
- ### RAK13300in Slot 2 pins
-# IRQ: 18 #IO6
-# Reset: 24 # IO4
-# Busy: 19 # IO5
-# # Ant_sw: 23 # IO3
-# spidev: spidev0.1
-# # CS: 7
\ No newline at end of file
+ # CS: 8
\ No newline at end of file
diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml
new file mode 100644
index 000000000..cbc794d39
--- /dev/null
+++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml
@@ -0,0 +1,8 @@
+Lora:
+ ### RAK13300in Slot 2 pins
+ IRQ: 18 #IO6
+ Reset: 24 # IO4
+ Busy: 19 # IO5
+ # Ant_sw: 23 # IO3
+ spidev: spidev0.1
+ # CS: 7
\ No newline at end of file
diff --git a/bin/device-update.sh b/bin/device-update.sh
index ce0b5e434..2196d3af9 100755
--- a/bin/device-update.sh
+++ b/bin/device-update.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
CHANGE_MODE=false
diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py
index 0ce6b0f6b..aaa76aa45 100755
--- a/bin/generate_ci_matrix.py
+++ b/bin/generate_ci_matrix.py
@@ -2,50 +2,71 @@
"""Generate the CI matrix."""
-import configparser
import json
-import os
import sys
import random
-
-rootdir = "variants/"
+import re
+from platformio.project.config import ProjectConfig
options = sys.argv[1:]
outlist = []
if len(options) < 1:
- print(json.dumps(outlist))
- exit()
+ print(json.dumps(outlist))
+ exit(1)
-for subdir, dirs, files in os.walk(rootdir):
- for file in files:
- if file == "platformio.ini":
- config = configparser.ConfigParser()
- config.read(subdir + "/" + file)
- for c in config.sections():
- if c.startswith("env:"):
- section = config[c].name[4:]
- if "extends" in config[config[c].name]:
- if options[0] + "_base" in config[config[c].name]["extends"]:
- if "board_level" in config[config[c].name]:
- if (
- config[config[c].name]["board_level"] == "extra"
- ) & ("extra" in options):
- outlist.append(section)
- else:
- outlist.append(section)
- # Add the TFT variants if the base variant is selected
- elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
- outlist.append(section)
- elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
- outlist.append(section)
- if "board_check" in config[config[c].name]:
- if (config[config[c].name]["board_check"] == "true") & (
- "check" in options
- ):
- outlist.append(section)
-if ("quick" in options) & (len(outlist) > 3):
- print(json.dumps(random.sample(outlist, 3)))
+cfg = ProjectConfig.get_instance()
+pio_envs = cfg.envs()
+
+# Gather all PlatformIO environments for filtering later
+all_envs = []
+for pio_env in pio_envs:
+ env_build_flags = cfg.get(f"env:{pio_env}", 'build_flags')
+ env_platform = None
+ for flag in env_build_flags:
+ # Extract the platform from the build flags
+ # Example flag: -I variants/esp32s3/heltec-v3
+ match = re.search(r"-I\s?variants/([^/]+)", flag)
+ if match:
+ env_platform = match.group(1)
+ break
+ # Intentionally fail if platform cannot be determined
+ if not env_platform:
+ print(f"Error: Could not determine platform for environment '{pio_env}'")
+ exit(1)
+ # Store env details as a dictionary, and add to 'all_envs' list
+ env = {
+ 'name': pio_env,
+ 'platform': env_platform,
+ 'board_level': cfg.get(f"env:{pio_env}", 'board_level', default=None),
+ 'board_check': bool(cfg.get(f"env:{pio_env}", 'board_check', default=False))
+ }
+ all_envs.append(env)
+
+# Filter outputs based on options
+# Check is mutually exclusive with other options (except 'pr')
+if "check" in options:
+ for env in all_envs:
+ if env['board_check']:
+ if "pr" in options:
+ if env['board_level'] == 'pr':
+ outlist.append(env['name'])
+ else:
+ outlist.append(env['name'])
+# Filter (non-check) builds by platform
else:
- print(json.dumps(outlist))
\ No newline at end of file
+ for env in all_envs:
+ if options[0] == env['platform']:
+ # Always include board_level = 'pr'
+ if env['board_level'] == 'pr':
+ outlist.append(env['name'])
+ # Include board_level = 'extra' when requested
+ elif "extra" in options and env['board_level'] == "extra":
+ outlist.append(env['name'])
+ # If no board level is specified, include in release builds (not PR)
+ elif "pr" not in options and not env['board_level']:
+ outlist.append(env['name'])
+
+# Return as a JSON list
+print(json.dumps(outlist))
diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml
index 291fe7a7c..1d97e2a66 100644
--- a/bin/org.meshtastic.meshtasticd.metainfo.xml
+++ b/bin/org.meshtastic.meshtasticd.metainfo.xml
@@ -87,6 +87,18 @@
+
+ https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7
+
+
+ https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6
+
+
+ https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5
+
+
+ https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.4
+
https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.3
diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py
index be2a9ab71..fc1b4bc2e 100644
--- a/bin/platformio-custom.py
+++ b/bin/platformio-custom.py
@@ -3,6 +3,7 @@
# trunk-ignore-all(flake8/F821): For SConstruct imports
import sys
from os.path import join
+import subprocess
import json
import re
@@ -92,6 +93,17 @@ prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc)
print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV"))
+# get repository owner if git is installed
+try:
+ r_owner = (
+ subprocess.check_output(["git", "config", "--get", "remote.origin.url"])
+ .decode("utf-8")
+ .strip().split("/")
+ )
+ repo_owner = r_owner[-2] + "/" + r_owner[-1].replace(".git", "")
+except subprocess.CalledProcessError:
+ repo_owner = "unknown"
+
jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc"
with open(jsonLoc) as f:
jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE)
@@ -117,6 +129,7 @@ flags = [
"-DAPP_VERSION=" + verObj["long"],
"-DAPP_VERSION_SHORT=" + verObj["short"],
"-DAPP_ENV=" + env.get("PIOENV"),
+ "-DAPP_REPO=" + repo_owner,
] + pref_flags
print ("Using flags:")
diff --git a/boards/heltec_mesh_solar.json b/boards/heltec_mesh_solar.json
new file mode 100644
index 000000000..9e551c082
--- /dev/null
+++ b/boards/heltec_mesh_solar.json
@@ -0,0 +1,54 @@
+{
+ "build": {
+ "arduino": {
+ "ldscript": "nrf52840_s140_v6.ld"
+ },
+ "core": "nRF5",
+ "cpu": "cortex-m4",
+ "extra_flags": "-DNRF52840_XXAA",
+ "f_cpu": "64000000L",
+ "hwids": [
+ ["0x239A", "0x4405"],
+ ["0x239A", "0x0029"],
+ ["0x239A", "0x002A"],
+ ["0x239A", "0x0071"]
+ ],
+ "usb_product": "HT-n5262",
+ "mcu": "nrf52840",
+ "variant": "heltec_mesh_solar",
+ "variants_dir": "variants",
+ "bsp": {
+ "name": "adafruit"
+ },
+ "softdevice": {
+ "sd_flags": "-DS140",
+ "sd_name": "s140",
+ "sd_version": "6.1.1",
+ "sd_fwid": "0x00B6"
+ },
+ "bootloader": {
+ "settings_addr": "0xFF000"
+ }
+ },
+ "connectivity": ["bluetooth"],
+ "debug": {
+ "jlink_device": "nRF52840_xxAA",
+ "onboard_tools": ["jlink"],
+ "svd_path": "nrf52840.svd",
+ "openocd_target": "nrf52840-mdk-rs"
+ },
+ "frameworks": ["arduino"],
+ "name": "Heltec nrf (Adafruit BSP)",
+ "upload": {
+ "maximum_ram_size": 248832,
+ "maximum_size": 815104,
+ "speed": 115200,
+ "protocol": "nrfutil",
+ "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
+ "use_1200bps_touch": true,
+ "require_upload_port": true,
+ "wait_for_upload_port": true
+ },
+ "url": "https://heltec.org/project/meshsolar/",
+ "vendor": "Heltec"
+}
diff --git a/boards/meshtiny.json b/boards/meshtiny.json
new file mode 100644
index 000000000..1473388ea
--- /dev/null
+++ b/boards/meshtiny.json
@@ -0,0 +1,52 @@
+{
+ "build": {
+ "arduino": {
+ "ldscript": "nrf52840_s140_v6.ld"
+ },
+ "core": "nRF5",
+ "cpu": "cortex-m4",
+ "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
+ "f_cpu": "64000000L",
+ "hwids": [
+ ["0x239A", "0x8029"],
+ ["0x239A", "0x0029"],
+ ["0x239A", "0x002A"],
+ ["0x239A", "0x802A"]
+ ],
+ "usb_product": "MeshTiny",
+ "mcu": "nrf52840",
+ "variant": "meshtiny",
+ "bsp": {
+ "name": "adafruit"
+ },
+ "softdevice": {
+ "sd_flags": "-DS140",
+ "sd_name": "s140",
+ "sd_version": "6.1.1",
+ "sd_fwid": "0x00B6"
+ },
+ "bootloader": {
+ "settings_addr": "0xFF000"
+ }
+ },
+ "connectivity": ["bluetooth"],
+ "debug": {
+ "jlink_device": "nRF52840_xxAA",
+ "svd_path": "nrf52840.svd",
+ "openocd_target": "nrf52840-mdk-rs"
+ },
+ "frameworks": ["arduino", "freertos"],
+ "name": "MeshTiny",
+ "upload": {
+ "maximum_ram_size": 248832,
+ "maximum_size": 815104,
+ "speed": 115200,
+ "protocol": "nrfutil",
+ "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
+ "use_1200bps_touch": true,
+ "require_upload_port": true,
+ "wait_for_upload_port": true
+ },
+ "url": "https://github.com/meshtastic/firmware",
+ "vendor": "MTools Tec"
+}
diff --git a/boards/t-deck-pro.json b/boards/t-deck-pro.json
new file mode 100644
index 000000000..2f4bd594a
--- /dev/null
+++ b/boards/t-deck-pro.json
@@ -0,0 +1,43 @@
+{
+ "build": {
+ "arduino": {
+ "ldscript": "esp32s3_out.ld",
+ "memory_type": "qio_qspi",
+ "partitions": "default_16MB.csv"
+ },
+ "core": "esp32",
+ "extra_flags": [
+ "-DBOARD_HAS_PSRAM",
+ "-DARDUINO_USB_CDC_ON_BOOT=1",
+ "-DARDUINO_USB_MODE=1",
+ "-DARDUINO_RUNNING_CORE=1",
+ "-DARDUINO_EVENT_RUNNING_CORE=1"
+ ],
+ "f_cpu": "240000000L",
+ "f_flash": "80000000L",
+ "flash_mode": "qio",
+ "hwids": [["0x303A", "0x1001"]],
+ "mcu": "esp32s3",
+ "variant": "esp32s3"
+ },
+ "connectivity": ["wifi", "bluetooth", "lora"],
+ "debug": {
+ "default_tool": "esp-builtin",
+ "onboard_tools": ["esp-builtin"],
+ "openocd_target": "esp32s3.cfg"
+ },
+ "frameworks": ["arduino", "espidf"],
+ "name": "LilyGo T-Deck Pro S3 (16M Flash 8M QSPI PSRAM )",
+ "upload": {
+ "flash_size": "16MB",
+ "maximum_ram_size": 327680,
+ "maximum_size": 16777216,
+ "require_upload_port": true,
+ "speed": 921600
+ },
+ "monitor": {
+ "speed": 115200
+ },
+ "url": "https://lilygo.cc/products/t-deck-pro",
+ "vendor": "LilyGo"
+}
diff --git a/debian/changelog b/debian/changelog
index b5009028a..ff59db89e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-meshtasticd (2.7.3.0) UNRELEASED; urgency=medium
+meshtasticd (2.7.7.0) UNRELEASED; urgency=medium
[ Austin Lane ]
* Initial packaging
@@ -31,4 +31,14 @@ meshtasticd (2.7.3.0) UNRELEASED; urgency=medium
[ Ubuntu ]
* GitHub Actions Automatic version bump
- -- Ubuntu Thu, 10 Jul 2025 16:29:27 +0000
+ [ ]
+ * GitHub Actions Automatic version bump
+
+ [ ]
+ * GitHub Actions Automatic version bump
+
+ [ ]
+ * GitHub Actions Automatic version bump
+ * GitHub Actions Automatic version bump
+
+ -- Ubuntu Thu, 28 Aug 2025 10:33:25 +0000
diff --git a/debian/meshtasticd.postinst b/debian/meshtasticd.postinst
index fe0dbc332..d569cb43e 100755
--- a/debian/meshtasticd.postinst
+++ b/debian/meshtasticd.postinst
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
# postinst script for meshtasticd
#
# see: dh_installdeb(1)
diff --git a/debian/meshtasticd.postrm b/debian/meshtasticd.postrm
index bb2c32a5b..dc25680a8 100755
--- a/debian/meshtasticd.postrm
+++ b/debian/meshtasticd.postrm
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
# postrm script for meshtasticd
#
# see: dh_installdeb(1)
diff --git a/platformio.ini b/platformio.ini
index b1f89e5b4..ef0fef791 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -6,7 +6,8 @@ default_envs = tbeam
extra_configs =
arch/*/*.ini
- variants/*/platformio.ini
+ variants/*/*/platformio.ini
+ variants/*/diy/*/platformio.ini
src/graphics/niche/InkHUD/PlatformioConfig.ini
description = Meshtastic
@@ -59,9 +60,9 @@ 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/0119501e9983bd894830b02f545c377ee08d66fe.zip
- # renovate: datasource=custom.pio depName=OneButton packageName=mathertel/library/OneButton
- mathertel/OneButton@2.6.1
+ https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.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
https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip
# renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master
@@ -101,6 +102,14 @@ lib_deps =
# 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]
+lib_deps =
+ # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
+ thingsboard/TBPubSubClient@2.12.1
+ # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
+ arduino-libraries/NTPClient@3.2.1
+
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
@@ -109,7 +118,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/86a09a7360f92d10053fbbf8d74f67f85b0ceb09.zip
+ https://github.com/meshtastic/device-ui/archive/a3e0e1be372d069f47b4c19d718f5267251744d7.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -177,7 +186,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
- adafruit/Adafruit SHTC3 Library@1.0.1
+ adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
diff --git a/protobufs b/protobufs
index 584f0a3a3..4c4427c4a 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 584f0a3a359103acf0bfce506c1b1fc32c639841
+Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81
diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h
index f6bb43cc2..680aec929 100644
--- a/src/BluetoothStatus.h
+++ b/src/BluetoothStatus.h
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED
+#ifdef BLE_LED_INVERTED
+ digitalWrite(BLE_LED, LOW);
+#else
digitalWrite(BLE_LED, HIGH);
+#endif
#endif
break;
case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED
+#ifdef BLE_LED_INVERTED
+ digitalWrite(BLE_LED, HIGH);
+#else
digitalWrite(BLE_LED, LOW);
+#endif
#endif
break;
}
diff --git a/src/Power.cpp b/src/Power.cpp
index 9c67977bd..bf74f6e53 100644
--- a/src/Power.cpp
+++ b/src/Power.cpp
@@ -20,6 +20,11 @@
#include "meshUtils.h"
#include "sleep.h"
+#if defined(ARCH_PORTDUINO)
+#include "api/WiFiServerAPI.h"
+#include "input/LinuxInputImpl.h"
+#endif
+
// Working USB detection for powered/charging states on the RAK platform
#ifdef NRF_APM
#include "nrfx_power.h"
@@ -120,6 +125,15 @@ NullSensor max17048Sensor;
RAK9154Sensor rak9154Sensor;
#endif
+#ifdef HAS_PPM
+// note: XPOWERS_CHIP_XXX must be defined in variant.h
+#include
+#endif
+
+#ifdef HAS_BQ27220
+#include "bq27220.h"
+#endif
+
#ifdef HAS_PMU
XPowersLibInterface *PMU = NULL;
#else
@@ -665,6 +679,10 @@ bool Power::setup()
found = true;
} else if (lipoInit()) {
found = true;
+ } else if (lipoChargerInit()) {
+ found = true;
+ } else if (meshSolarInit()) {
+ found = true;
} else if (analogInit()) {
found = true;
}
@@ -679,9 +697,61 @@ bool Power::setup()
return found;
}
+void Power::powerCommandsCheck()
+{
+ if (rebootAtMsec && millis() > rebootAtMsec) {
+ LOG_INFO("Rebooting");
+ reboot();
+ }
+
+ if (shutdownAtMsec && millis() > shutdownAtMsec) {
+ shutdownAtMsec = 0;
+ shutdown();
+ }
+}
+
+void Power::reboot()
+{
+ notifyReboot.notifyObservers(NULL);
+#if defined(ARCH_ESP32)
+ ESP.restart();
+#elif defined(ARCH_NRF52)
+ NVIC_SystemReset();
+#elif defined(ARCH_RP2040)
+ rp2040.reboot();
+#elif defined(ARCH_PORTDUINO)
+ deInitApiServer();
+ if (aLinuxInputImpl)
+ aLinuxInputImpl->deInit();
+ SPI.end();
+ Wire.end();
+ Serial1.end();
+ if (screen) {
+ delete screen;
+ screen = nullptr;
+ }
+ LOG_DEBUG("final reboot!");
+ ::reboot();
+#elif defined(ARCH_STM32WL)
+ HAL_NVIC_SystemReset();
+#else
+ rebootAtMsec = -1;
+ LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
+#endif
+}
+
void Power::shutdown()
{
- LOG_INFO("Shutting Down");
+
+#if HAS_SCREEN
+ if (screen) {
+ screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
+ }
+#endif
+#if !defined(ARCH_STM32WL)
+ playShutdownMelody();
+#endif
+ nodeDB->saveToDisk();
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1
@@ -693,7 +763,11 @@ void Power::shutdown()
#ifdef PIN_LED3
ledOff(PIN_LED3);
#endif
- doDeepSleep(DELAY_FOREVER, false, false);
+ doDeepSleep(DELAY_FOREVER, false, true);
+#elif defined(ARCH_PORTDUINO)
+ exit(EXIT_SUCCESS);
+#else
+ LOG_WARN("FIXME implement shutdown for this platform");
#endif
}
@@ -1237,3 +1311,216 @@ bool Power::lipoInit()
return false;
}
#endif
+
+#if defined(HAS_PPM) && HAS_PPM
+
+/**
+ * Adapter class for BQ25896/BQ27220 Lipo battery charger.
+ */
+class LipoCharger : public HasBatteryLevel
+{
+ private:
+ XPowersPPM *ppm = nullptr;
+ BQ27220 *bq = nullptr;
+
+ public:
+ /**
+ * Init the I2C BQ25896 Lipo battery charger
+ */
+ bool runOnce()
+ {
+ if (ppm == nullptr) {
+ ppm = new XPowersPPM;
+ bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
+ if (result) {
+ LOG_INFO("PPM BQ25896 init succeeded");
+ // Set the minimum operating voltage. Below this voltage, the PPM will protect
+ // ppm->setSysPowerDownVoltage(3100);
+
+ // Set input current limit, default is 500mA
+ // ppm->setInputCurrentLimit(800);
+
+ // Disable current limit pin
+ // ppm->disableCurrentLimitPin();
+
+ // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
+ ppm->setChargeTargetVoltage(4288);
+
+ // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
+ // ppm->setPrechargeCurr(64);
+
+ // The premise is that limit pin is disabled, or it will
+ // only follow the maximum charging current set by limit pin.
+ // Set the charging current , Range:0~5056mA ,step:64mA
+ ppm->setChargerConstantCurr(1024);
+
+ // To obtain voltage data, the ADC must be enabled first
+ ppm->enableMeasure();
+
+ // Turn on charging function
+ // If there is no battery connected, do not turn on the charging function
+ ppm->enableCharge();
+ } else {
+ LOG_WARN("PPM BQ25896 init failed");
+ delete ppm;
+ ppm = nullptr;
+ return false;
+ }
+ }
+ if (bq == nullptr) {
+ bq = new BQ27220;
+ bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
+
+ bool result = bq->init();
+ if (result) {
+ LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
+ LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
+ LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
+ return true;
+ } else {
+ LOG_WARN("BQ27220 init failed");
+ delete bq;
+ bq = nullptr;
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Battery state of charge, from 0 to 100 or -1 for unknown
+ */
+ virtual int getBatteryPercent() override
+ {
+ return -1;
+ // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
+ }
+
+ /**
+ * The raw voltage of the battery in millivolts, or NAN if unknown
+ */
+ virtual uint16_t getBattVoltage() override { return bq->getVoltage(); }
+
+ /**
+ * return true if there is a battery installed in this unit
+ */
+ virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; }
+
+ /**
+ * return true if there is an external power source detected
+ */
+ virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; }
+
+ /**
+ * return true if the battery is currently charging
+ */
+ virtual bool isCharging() override
+ {
+ bool isCharging = ppm->isCharging();
+ if (isCharging) {
+ LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
+ } else {
+ if (!ppm->isVbusIn()) {
+ LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
+ }
+ }
+ return isCharging;
+ }
+};
+
+LipoCharger lipoCharger;
+
+/**
+ * Init the Lipo battery charger
+ */
+bool Power::lipoChargerInit()
+{
+ bool result = lipoCharger.runOnce();
+ LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet");
+ if (!result)
+ return false;
+ batteryLevel = &lipoCharger;
+ return true;
+}
+
+#else
+/**
+ * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
+ */
+bool Power::lipoChargerInit()
+{
+ return false;
+}
+#endif
+
+
+
+#ifdef HELTEC_MESH_SOLAR
+#include "meshSolarApp.h"
+
+/**
+ * meshSolar class for an SMBUS battery sensor.
+ */
+class meshSolarBatteryLevel : public HasBatteryLevel
+{
+
+ public:
+ /**
+ * Init the I2C meshSolar battery level sensor
+ */
+ bool runOnce()
+ {
+ meshSolarStart();
+ return true;
+ }
+
+ /**
+ * Battery state of charge, from 0 to 100 or -1 for unknown
+ */
+ virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); }
+
+ /**
+ * The raw voltage of the battery in millivolts, or NAN if unknown
+ */
+ virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); }
+
+ /**
+ * return true if there is a battery installed in this unit
+ */
+ virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); }
+
+ /**
+ * return true if there is an external power source detected
+ */
+ virtual bool isVbusIn() override { return meshSolarIsVbusIn();}
+
+ /**
+ * return true if the battery is currently charging
+ */
+ virtual bool isCharging() override { return meshSolarIsCharging(); }
+};
+
+meshSolarBatteryLevel meshSolarLevel;
+
+/**
+ * Init the meshSolar battery level sensor
+ */
+bool Power::meshSolarInit()
+{
+ bool result = meshSolarLevel.runOnce();
+ LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet");
+ if (!result)
+ return false;
+ batteryLevel = &meshSolarLevel;
+ return true;
+}
+
+#else
+/**
+ * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
+ */
+bool Power::meshSolarInit()
+{
+ return false;
+}
+#endif
\ No newline at end of file
diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp
index 3b3f8080d..322b877ff 100644
--- a/src/PowerFSM.cpp
+++ b/src/PowerFSM.cpp
@@ -72,7 +72,7 @@ extern Power *power;
static void shutdownEnter()
{
LOG_DEBUG("State: SHUTDOWN");
- power->shutdown();
+ shutdownAtMsec = millis();
}
#include "error.h"
diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp
index 68c41980d..093a24678 100644
--- a/src/SerialConsole.cpp
+++ b/src/SerialConsole.cpp
@@ -64,6 +64,14 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
int32_t SerialConsole::runOnce()
{
+#ifdef HELTEC_MESH_SOLAR
+ //After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
+ if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port
+ && moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)
+ {
+ return 250;
+ }
+#endif
return runOncePart();
}
diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp
index 2bd3158a3..ce762c764 100644
--- a/src/buzz/BuzzerFeedbackThread.cpp
+++ b/src/buzz/BuzzerFeedbackThread.cpp
@@ -47,10 +47,6 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
playComboTune(); // Ping sent feedback
break;
- case INPUT_BROKER_SHUTDOWN:
- playShutdownMelody(); // Shutdown feedback
- break;
-
default:
// For other events, check if it's a printable character
if (event->kbchar >= 32 && event->kbchar <= 126) {
@@ -69,10 +65,7 @@ int32_t BuzzerFeedbackThread::runOnce()
// This thread is primarily event-driven, but we can use runOnce
// for any periodic tasks if needed in the future
- if (needsUpdate) {
- needsUpdate = false;
- // Could add any periodic processing here
- }
+ needsUpdate = false;
// Run every 100ms when active, less frequently when idle
return needsUpdate ? 100 : 1000;
diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp
index b09d7a82c..b0d162a44 100644
--- a/src/buzz/buzz.cpp
+++ b/src/buzz/buzz.cpp
@@ -140,6 +140,10 @@ bool playNextLeadUpNote()
playTones(¬e, 1); // Play single note using existing playTones function
leadUpNoteIndex++;
+
+ if (leadUpNoteIndex >= leadUpNotesCount) {
+ return false; // this was the final note
+ }
return true; // Note was played (playTones handles buzzer availability internally)
}
diff --git a/src/configuration.h b/src/configuration.h
index cddc7ba7a..8b4fd82c7 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -135,7 +135,7 @@ along with this program. If not, see .
// -----------------------------------------------------------------------------
// OLED & Input
// -----------------------------------------------------------------------------
-#if defined(SEEED_WIO_TRACKER_L1)
+#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
#define SSD1306_ADDRESS 0x3D
#define USE_SH1106
#else
@@ -150,11 +150,12 @@ along with this program. If not, see .
// Define if screen should be mirrored left to right
// #define SCREEN_MIRROR
-// I2C Keyboards (M5Stack, RAK14004, T-Deck)
+// I2C Keyboards (M5Stack, RAK14004, T-Deck, T-Deck Pro, T-Lora Pager, CardKB, BBQ10, MPR121, TCA8418)
#define CARDKB_ADDR 0x5F
#define TDECK_KB_ADDR 0x55
#define BBQ10_KB_ADDR 0x1F
#define MPR121_KB_ADDR 0x5A
+#define TCA8418_KB_ADDR 0x34
// -----------------------------------------------------------------------------
// SENSOR
@@ -193,8 +194,11 @@ along with this program. If not, see .
#define MLX90614_ADDR_DEF 0x5A
#define CGRADSENS_ADDR 0x66
#define LTR390UV_ADDR 0x53
-#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418
+#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418_KB
#define PCT2075_ADDR 0x37
+#define BQ27220_ADDR 0x55 // same address as TDECK_KB
+#define BQ25896_ADDR 0x6B
+#define LTR553ALS_ADDR 0x23
// -----------------------------------------------------------------------------
// ACCELEROMETER
@@ -208,6 +212,7 @@ along with this program. If not, see .
#define BMX160_ADDR 0x69
#define ICM20948_ADDR 0x69
#define ICM20948_ADDR_ALT 0x68
+#define BHI260AP_ADDR 0x28
#define BMM150_ADDR 0x13
// -----------------------------------------------------------------------------
@@ -230,6 +235,7 @@ along with this program. If not, see .
// Touchscreen
// -----------------------------------------------------------------------------
#define FT6336U_ADDR 0x48
+#define CST328_ADDR 0x1A
// -----------------------------------------------------------------------------
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index dd290db98..e46c6f623 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -74,7 +74,13 @@ class ScanI2C
RAK12035,
TCA8418KB,
PCT2075,
+ CST328,
+ BQ25896,
+ BQ27220,
+ LTR553ALS,
+ BHI260AP,
BMM150,
+ DRV2605
} DeviceType;
// typedef uint8_t DeviceAddress;
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index 9e9441123..9aef9defe 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -184,8 +184,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = RTC_RV3028;
logFoundDevice("RV3028", (uint8_t)addr.address);
rtc.initI2C(*i2cBus);
- rtc.writeToRegister(0x35, 0x07); // no Clkout
- rtc.writeToRegister(0x37, 0xB4);
+ // Update RTC EEPROM settings, if necessary
+ if (rtc.readEEPROMRegister(0x35) != 0x07) {
+ rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout
+ }
+ if (rtc.readEEPROMRegister(0x37) != 0xB4) {
+ rtc.writeEEPROMRegister(0x37, 0xB4);
+ }
break;
#endif
@@ -206,7 +211,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
- SCAN_SIMPLE_CASE(TDECK_KB_ADDR, TDECKKB, "T-Deck keyboard", (uint8_t)addr.address);
+ case TDECK_KB_ADDR:
+ // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor?
+ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
+ if (registerValue != 0) {
+ logFoundDevice("BQ27220", (uint8_t)addr.address);
+ type = BQ27220;
+ } else {
+ logFoundDevice("TDECKKB", (uint8_t)addr.address);
+ type = TDECKKB;
+ }
+ break;
SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
@@ -396,6 +411,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
logFoundDevice("BQ24295", (uint8_t)addr.address);
break;
}
+ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID
+ if ((registerValue & 0b00000011) == 0b00000010) {
+ type = BQ25896;
+ logFoundDevice("BQ25896", (uint8_t)addr.address);
+ break;
+ }
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID
if (registerValue == 0x6A) {
type = LSM6DS3;
@@ -447,6 +468,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
+ SCAN_SIMPLE_CASE(CST328_ADDR, CST328, "CST328", (uint8_t)addr.address);
+ SCAN_SIMPLE_CASE(LTR553ALS_ADDR, LTR553ALS, "LTR553ALS", (uint8_t)addr.address);
+ SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
#ifdef HAS_TPS65233
@@ -459,8 +483,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = MLX90614;
logFoundDevice("MLX90614", (uint8_t)addr.address);
} else {
- type = MPR121KB;
- logFoundDevice("MPR121KB", (uint8_t)addr.address);
+ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS
+ if (registerValue == 0xe0) {
+ type = DRV2605;
+ logFoundDevice("DRV2605", (uint8_t)addr.address);
+ } else {
+ type = MPR121KB;
+ logFoundDevice("MPR121KB", (uint8_t)addr.address);
+ }
}
break;
diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp
index 345c738d6..881021975 100644
--- a/src/gps/GPS.cpp
+++ b/src/gps/GPS.cpp
@@ -39,9 +39,9 @@ template std::size_t array_count(const T (&)[N])
return N;
}
-#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
-#if defined(RAK2560)
-HardwareSerial *GPS::_serial_gps = &Serial2;
+#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
+#if defined(GPS_SERIAL_PORT)
+HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT;
#else
HardwareSerial *GPS::_serial_gps = &Serial1;
#endif
@@ -643,8 +643,16 @@ bool GPS::setup()
delay(250);
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
- _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
-
+ if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
+ config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
+ _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
+ // GPS GLONASS GALILEO BDS QZSS NAVIC
+ // 1 0 1 0 0 1
+ } else {
+ _serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
+ // GPS GLONASS GALILEO BDS QZSS NAVIC
+ // 1 1 1 1 0 0
+ }
// Configure NMEA (sentences will output once per fix)
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
@@ -1496,14 +1504,14 @@ static int32_t toDegInt(RawDegrees d)
* Perform any processing that should be done only while the GPS is awake and looking for a fix.
* Override this method to check for new locations
*
- * @return true if we've acquired a new location
+ * @return true if we've set a new time
*/
bool GPS::lookForTime()
{
#ifdef GNSS_AIROHA
uint8_t fix = reader.fixQuality();
- if (fix > 0) {
+ if (fix >= 1 && fix <= 5) {
if (lastFixStartMsec > 0) {
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
return false;
@@ -1536,11 +1544,12 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
if (t.tm_mon > -1) {
LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
t.tm_sec, ti.age());
- if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) {
- // Clear the GPS buffer if we got an invalid time
- clearBuffer();
+ if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) {
+ LOG_DEBUG("Time set.");
+ return true;
+ } else {
+ return false;
}
- return true;
} else
return false;
} else
@@ -1558,7 +1567,7 @@ bool GPS::lookForLocation()
#ifdef GNSS_AIROHA
if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) {
uint8_t fix = reader.fixQuality();
- if (fix > 0) {
+ if (fix >= 1 && fix <= 5) {
if (lastFixStartMsec > 0) {
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
return false;
diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index 5054be3f0..185adacd9 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -23,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda
* Reads the current date and time from the RTC module and updates the system time.
* @return True if the RTC was successfully read and the system time was updated, false otherwise.
*/
-void readFromRTC()
+RTCSetResult readFromRTC()
{
struct timeval tv; /* btw settimeofday() is helpful here too*/
#ifdef RV3028_RTC
@@ -44,8 +44,15 @@ void readFromRTC()
t.tm_sec = rtc.getSecond();
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
-
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
+
+#ifdef BUILD_EPOCH
+ if (tv.tv_sec < BUILD_EPOCH) {
+ LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+ return RTCSetResultInvalidTime;
+ }
+#endif
+
LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
timeStartMsec = now;
@@ -53,6 +60,7 @@ void readFromRTC()
if (currentQuality == RTCQualityNone) {
currentQuality = RTCQualityDevice;
}
+ return RTCSetResultSuccess;
}
#elif defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
@@ -75,8 +83,15 @@ void readFromRTC()
t.tm_sec = tc.second;
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
-
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
+
+#ifdef BUILD_EPOCH
+ if (tv.tv_sec < BUILD_EPOCH) {
+ LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+ return RTCSetResultInvalidTime;
+ }
+#endif
+
LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
timeStartMsec = now;
@@ -84,6 +99,7 @@ void readFromRTC()
if (currentQuality == RTCQualityNone) {
currentQuality = RTCQualityDevice;
}
+ return RTCSetResultSuccess;
}
#else
if (!gettimeofday(&tv, NULL)) {
@@ -92,8 +108,10 @@ void readFromRTC()
LOG_DEBUG("Read RTC time as %ld", printableEpoch);
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
+ return RTCSetResultSuccess;
}
#endif
+ return RTCSetResultNotSet;
}
/**
@@ -101,7 +119,7 @@ void readFromRTC()
*
* @param q The quality of the provided time.
* @param tv A pointer to a timeval struct containing the time to potentially set the RTC to.
- * @return True if the RTC was set, false otherwise.
+ * @return RTCSetResult
*
* If we haven't yet set our RTC this boot, set it from a GPS derived time
*/
@@ -226,7 +244,14 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
time_t res = gm_mktime(&t);
struct timeval tv;
tv.tv_sec = res;
- tv.tv_usec = 0; // time.centisecond() * (10 / 1000);
+ tv.tv_usec = 0; // time.centisecond() * (10 / 1000);
+ uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
+#ifdef BUILD_EPOCH
+ if (tv.tv_sec < BUILD_EPOCH) {
+ LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+ return RTCSetResultInvalidTime;
+ }
+#endif
// LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
if (t.tm_year < 0 || t.tm_year >= 300) {
diff --git a/src/gps/RTC.h b/src/gps/RTC.h
index 96dec575b..010be6886 100644
--- a/src/gps/RTC.h
+++ b/src/gps/RTC.h
@@ -48,7 +48,7 @@ uint32_t getTime(bool local = false);
/// Return time since 1970 in secs. If quality is RTCQualityNone return zero
uint32_t getValidTime(RTCQuality minQuality, bool local = false);
-void readFromRTC();
+RTCSetResult readFromRTC();
time_t gm_mktime(struct tm *tm);
diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp
index 1685766c9..6b909eab7 100644
--- a/src/graphics/EInkDisplay2.cpp
+++ b/src/graphics/EInkDisplay2.cpp
@@ -68,20 +68,28 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
// FIXME - only draw bits have changed (use backbuf similar to the other displays)
const bool flipped = config.display.flip_screen;
+ // HACK for L1 EInk
+#if defined(SEEED_WIO_TRACKER_L1_EINK)
+ // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes
+ for (uint32_t y = 0; y < displayHeight; y++) {
+ for (uint32_t x = 0; x < displayWidth; x++) {
+ auto b = buffer[x + (y / 8) * displayWidth];
+ auto isset = b & (1 << (y & 7));
+ adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
+ }
+ }
+#else
for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) {
- // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient
auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7));
-
- // Handle flip here, rather than with setRotation(),
- // Avoids issues when display width is not a multiple of 8
if (flipped)
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
else
adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE);
}
}
+#endif
// Trigger the refresh in GxEPD2
LOG_DEBUG("Update E-Paper");
@@ -141,17 +149,32 @@ bool EInkDisplay::connect()
#endif
#endif
-#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1)
+#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
{
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
adafruitDisplay = new GxEPD2_BW(*lowLevel);
adafruitDisplay->init();
-#ifdef ELECROW_ThinkNode_M1
+#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
adafruitDisplay->setRotation(4);
#else
adafruitDisplay->setRotation(3);
#endif
+ adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
+ }
+#elif defined(ELECROW_ThinkNode_M5)
+ {
+ // Start HSPI
+ hspi = new SPIClass(HSPI);
+ hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
+
+ auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
+
+ adafruitDisplay = new GxEPD2_BW(*lowLevel);
+ adafruitDisplay->init();
+
+ adafruitDisplay->setRotation(4);
+
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
}
#elif defined(MESHLINK)
@@ -213,7 +236,7 @@ bool EInkDisplay::connect()
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EPD_WIDTH, EPD_HEIGHT);
-#elif defined(M5_COREINK)
+#elif defined(M5_COREINK) || defined(T_DECK_PRO)
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
adafruitDisplay = new GxEPD2_BW(*lowLevel);
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
@@ -227,7 +250,7 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
-#elif defined(HELTEC_MESH_POCKET)
+#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
{
spi1 = &SPI1;
spi1->begin();
@@ -241,6 +264,7 @@ bool EInkDisplay::connect()
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
+ adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h
index 284337627..b4cee81fe 100644
--- a/src/graphics/EInkDisplay2.h
+++ b/src/graphics/EInkDisplay2.h
@@ -80,11 +80,11 @@ class EInkDisplay : public OLEDDisplay
// If display uses HSPI
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
- defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
+ defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5)
SPIClass *hspi = NULL;
#endif
-#if defined(HELTEC_MESH_POCKET)
+#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
SPIClass *spi1 = NULL;
#endif
diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index 1f2e7e4d9..dea08d5ba 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -318,7 +318,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
- defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
+ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
@@ -365,9 +365,6 @@ void Screen::doDeepSleep()
{
#ifdef USE_EINK
setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
-#ifdef PIN_EINK_EN
- digitalWrite(PIN_EINK_EN, LOW); // power off backlight
-#endif
#else
// Without E-Ink display:
setOn(false);
@@ -386,13 +383,19 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#ifdef T_WATCH_S3
PMU->enablePowerOutput(XPOWERS_ALDO2);
#endif
-#ifdef HELTEC_TRACKER_V1_X
- uint8_t tft_vext_enabled = digitalRead(VEXT_ENABLE);
-#endif
+
#if !ARCH_PORTDUINO
dispdev->displayOn();
#endif
+#ifdef PIN_EINK_EN
+ if (uiconfig.screen_brightness == 1)
+ digitalWrite(PIN_EINK_EN, HIGH);
+#elif defined(PCA_PIN_EINK_EN)
+ if (uiconfig.screen_brightness == 1)
+ io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
+#endif
+
#if defined(ST7789_CS) && \
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
static_cast(dispdev)->setDisplayBrightness(brightness);
@@ -400,10 +403,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
dispdev->displayOn();
#ifdef HELTEC_TRACKER_V1_X
- // If the TFT VEXT power is not enabled, initialize the UI.
- if (!tft_vext_enabled) {
- ui->init();
- }
+ ui->init();
#endif
#ifdef USE_ST7789
pinMode(VTFT_CTRL, OUTPUT);
@@ -425,11 +425,13 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
setScreensaverFrames(einkScreensaver);
#endif
-#ifdef ELECROW_ThinkNode_M1
- if (digitalRead(PIN_EINK_EN) == HIGH) {
- digitalWrite(PIN_EINK_EN, LOW);
- }
+
+#ifdef PIN_EINK_EN
+ digitalWrite(PIN_EINK_EN, LOW);
+#elif defined(PCA_PIN_EINK_EN)
+ io.digitalWrite(PCA_PIN_EINK_EN, LOW);
#endif
+
dispdev->displayOff();
#ifdef USE_ST7789
SPI1.end();
@@ -548,7 +550,7 @@ void Screen::setup()
#else
if (!config.display.flip_screen) {
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
- defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
+ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
static_cast(dispdev)->flipScreenVertically();
#elif defined(USE_ST7789)
static_cast(dispdev)->flipScreenVertically();
@@ -584,7 +586,7 @@ void Screen::setup()
touchScreenImpl1->init();
}
}
-#elif HAS_TOUCHSCREEN
+#elif HAS_TOUCHSCREEN && !defined(USE_EINK)
touchScreenImpl1 =
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch);
touchScreenImpl1->init();
@@ -690,7 +692,7 @@ int32_t Screen::runOnce()
#ifndef DISABLE_WELCOME_UNSET
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
- menuHandler::LoraRegionPicker(0);
+ menuHandler::OnboardMessage();
}
#endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -869,6 +871,8 @@ void Screen::setFrames(FrameFocus focus)
uint8_t previousFrameCount = framesetInfo.frameCount;
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
+ graphics::UIRenderer::rebuildFavoritedNodes();
+
LOG_DEBUG("Show standard frames");
showingNormalScreen = true;
@@ -1004,7 +1008,7 @@ void Screen::setFrames(FrameFocus focus)
// Insert favorite frames *after* collecting them all
if (!favoriteFrames.empty()) {
fsi.positions.firstFavorite = numframes;
- for (auto &f : favoriteFrames) {
+ for (const auto &f : favoriteFrames) {
normalFrames[numframes++] = f;
indicatorIcons.push_back(icon_node);
}
@@ -1267,40 +1271,39 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw
-
- // === 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(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++) {
- if (msgRaw[i] == '\x07') {
- isAlert = true;
- break;
- }
- }
-
- if (isAlert) {
- if (longName && longName[0]) {
- snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
- } else {
- strcpy(banner, "Alert Received");
- }
- } else {
- if (longName && longName[0]) {
- snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
- } else {
- strcpy(banner, "New Message");
- }
- }
-
- screen->showSimpleBanner(banner, 3000);
}
+ // === 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(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++) {
+ if (msgRaw[i] == '\x07') {
+ isAlert = true;
+ break;
+ }
+ }
+
+ if (isAlert) {
+ if (longName && longName[0]) {
+ snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
+ } else {
+ strcpy(banner, "Alert Received");
+ }
+ } else {
+ if (longName && longName[0]) {
+ snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
+ } else {
+ strcpy(banner, "New Message");
+ }
+ }
+
+ screen->showSimpleBanner(banner, 3000);
}
}
@@ -1379,9 +1382,12 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::LoraRegionPicker();
- } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
- devicestate.rx_text_message.from) {
- menuHandler::messageResponseMenu();
+ } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
+ if (devicestate.rx_text_message.from) {
+ menuHandler::messageResponseMenu();
+ } else {
+ menuHandler::textMessageBaseMenu();
+ }
} else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h
index 3373a47a7..a25417b05 100644
--- a/src/graphics/ScreenFonts.h
+++ b/src/graphics/ScreenFonts.h
@@ -16,7 +16,7 @@
#include "graphics/fonts/OLEDDisplayFontsCS.h"
#endif
-#ifdef CROWPANEL_ESP32S3_5_EPAPER
+#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
#include "graphics/fonts/EinkDisplayFonts.h"
#endif
@@ -40,6 +40,9 @@
#ifdef OLED_PL
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
#else
+#ifdef OLED_RU
+#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_RU // Height: 19
+#else
#ifdef OLED_UA
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19
#else
@@ -50,9 +53,13 @@
#endif
#endif
#endif
+#endif
#ifdef OLED_PL
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
#else
+#ifdef OLED_RU
+#define FONT_LARGE_LOCAL ArialMT_Plain_24_RU // Height: 28
+#else
#ifdef OLED_UA
#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28
#else
@@ -63,9 +70,10 @@
#endif
#endif
#endif
+#endif
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
@@ -77,7 +85,7 @@
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#endif
-#if defined(CROWPANEL_ESP32S3_5_EPAPER)
+#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
#undef FONT_SMALL
#undef FONT_MEDIUM
#undef FONT_LARGE
diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp
index 7cd876ac5..b458e54e4 100644
--- a/src/graphics/SharedUIDisplay.cpp
+++ b/src/graphics/SharedUIDisplay.cpp
@@ -206,7 +206,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
timeX = screenW - xOffset - timeStrWidth + 3;
// === Show Mail or Mute Icon to the Left of Time ===
- int iconRightEdge = timeX - 1;
+ int iconRightEdge = timeX - 2;
bool showMail = false;
diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp
index 3e9bafc6c..b1814005e 100644
--- a/src/graphics/TFTDisplay.cpp
+++ b/src/graphics/TFTDisplay.cpp
@@ -562,6 +562,91 @@ class LGFX : public lgfx::LGFX_Device
static LGFX *tft = nullptr;
+#elif defined(ST7796_CS)
+#include // Graphics and font library for ST7796 driver chip
+
+class LGFX : public lgfx::LGFX_Device
+{
+ lgfx::Panel_ST7796 _panel_instance;
+ lgfx::Bus_SPI _bus_instance;
+ lgfx::Light_PWM _light_instance;
+
+ public:
+ LGFX(void)
+ {
+ {
+ auto cfg = _bus_instance.config();
+
+ // SPI
+ cfg.spi_host = ST7796_SPI_HOST;
+ cfg.spi_mode = 0;
+ cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
+ // 80MHz by an integer)
+ cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
+ cfg.spi_3wire = false;
+ cfg.use_lock = true; // Set to true to use transaction locking
+ cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
+ // SPI_DMA_CH_AUTO=auto setting)
+ cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number
+ cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number
+ cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable)
+ cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable)
+
+ _bus_instance.config(cfg); // applies the set value to the bus.
+ _panel_instance.setBus(&_bus_instance); // set the bus on the panel.
+ }
+
+ { // Set the display panel control.
+ auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
+
+ cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable)
+ cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable)
+ cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable)
+
+ // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
+ // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
+ cfg.panel_width = TFT_WIDTH; // actual displayable width
+ cfg.panel_height = TFT_HEIGHT; // actual displayable height
+ cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction
+ cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction
+ cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored)
+#ifdef TFT_DUMMY_READ_PIXELS
+ cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout
+#else
+ cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout
+#endif
+ cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read
+ cfg.readable = true; // Set to true if data can be read
+ cfg.invert = true; // Set to true if the light/darkness of the panel is reversed
+ cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
+ cfg.dlen_16bit =
+ false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
+ cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
+
+ _panel_instance.config(cfg);
+ }
+
+#ifdef ST7796_BL
+ // Set the backlight control. (delete if not necessary)
+ {
+ auto cfg = _light_instance.config(); // Gets a structure for backlight settings.
+
+ cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected
+ cfg.invert = false; // true to invert the brightness of the backlight
+ cfg.freq = 44100;
+ cfg.pwm_channel = 7;
+
+ _light_instance.config(cfg);
+ _panel_instance.setLight(&_light_instance); // Set the backlight on the panel.
+ }
+#endif
+
+ setPanel(&_panel_instance); // Sets the panel to use.
+ }
+};
+
+static LGFX *tft = nullptr;
+
#elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER)
#include // Graphics and font library for ILI9341/ILI9342 driver chip
@@ -667,15 +752,19 @@ static LGFX *tft = nullptr;
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
#elif ARCH_PORTDUINO
#include // Graphics and font library for ST7735 driver chip
+#if defined(LGFX_SDL)
+#include
+#endif
class LGFX : public lgfx::LGFX_Device
{
- lgfx::Panel_Device *_panel_instance;
lgfx::Bus_SPI _bus_instance;
lgfx::ITouch *_touch_instance;
public:
+ lgfx::Panel_Device *_panel_instance;
+
LGFX(void)
{
if (settingsMap[displayPanel] == st7789)
@@ -694,6 +783,11 @@ class LGFX : public lgfx::LGFX_Device
_panel_instance = new lgfx::Panel_ILI9488;
else if (settingsMap[displayPanel] == hx8357d)
_panel_instance = new lgfx::Panel_HX8357D;
+#if defined(LGFX_SDL)
+ else if (settingsMap[displayPanel] == x11) {
+ _panel_instance = new lgfx::Panel_sdl;
+ }
+#endif
else {
_panel_instance = new lgfx::Panel_NULL;
LOG_ERROR("Unknown display panel configured!");
@@ -754,7 +848,13 @@ class LGFX : public lgfx::LGFX_Device
_touch_instance->config(touch_cfg);
_panel_instance->setTouch(_touch_instance);
}
-
+#if defined(LGFX_SDL)
+ if (settingsMap[displayPanel] == x11) {
+ lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
+ sdl_panel_->setup();
+ sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
+ }
+#endif
setPanel(_panel_instance); // Sets the panel to use.
}
};
@@ -849,9 +949,29 @@ static LGFX *tft = nullptr;
#include
#include
+class PanelInit_ST7701 : public lgfx::Panel_ST7701
+{
+ public:
+ const uint8_t *getInitCommands(uint8_t listno) const override
+ {
+ // 180 degree hw rotation: vertical flip, horizontal flip
+ static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip
+ 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL
+ 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip)
+ 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS
+ 0xFF, 0xFF};
+ switch (listno) {
+ case 1:
+ return list1;
+ default:
+ return lgfx::Panel_ST7701::getInitCommands(listno);
+ }
+ }
+};
+
class LGFX : public lgfx::LGFX_Device
{
- lgfx::Panel_ST7701 _panel_instance;
+ PanelInit_ST7701 _panel_instance;
lgfx::Bus_RGB _bus_instance;
lgfx::Light_PWM _light_instance;
lgfx::Touch_FT5x06 _touch_instance;
@@ -962,8 +1082,9 @@ static LGFX *tft = nullptr;
#endif
-#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
- defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0)
+#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \
+ defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \
+ (ARCH_PORTDUINO && HAS_SCREEN != 0)
#include "SPILock.h"
#include "TFTDisplay.h"
#include
@@ -1012,32 +1133,140 @@ void TFTDisplay::display(bool fromBlank)
{
if (fromBlank)
tft->fillScreen(TFT_BLACK);
- // tft->clear();
+
concurrency::LockGuard g(spiLock);
- uint16_t x, y;
+ uint32_t x, y;
+ uint32_t y_byteIndex;
+ uint8_t y_byteMask;
+ uint32_t x_FirstPixelUpdate;
+ uint32_t x_LastPixelUpdate;
+ bool isset, dblbuf_isset;
+ uint16_t colorTftMesh, colorTftBlack;
+ bool somethingChanged = false;
- for (y = 0; y < displayHeight; y++) {
- for (x = 0; x < displayWidth; x++) {
- auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7));
+ // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
+ colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8);
+ colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
+
+ y = 0;
+ while (y < displayHeight) {
+ y_byteIndex = (y / 8) * displayWidth;
+ y_byteMask = (1 << (y & 7));
+
+ // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
+ if (y_byteMask == 1) {
if (!fromBlank) {
- // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent
- auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7));
- if (isset != dblbuf_isset) {
- tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK);
+ for (x = 0; x < displayWidth; x++) {
+ if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
+ break;
}
- } else if (isset) {
- tft->drawPixel(x, y, TFT_MESH);
+ } else {
+ for (x = 0; x < displayWidth; x++) {
+ if (buffer[x + y_byteIndex] != 0)
+ break;
+ }
+ }
+ if (x >= displayWidth) {
+ // No changed pixels found in these 8 rows, fast-forward to the next 8
+ y = y + 8;
+ continue;
}
}
+
+ // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
+ for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
+ isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
+
+ if (!fromBlank) {
+ // get src pixel in the page based ordering the OLED lib uses
+ dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
+ if (isset != dblbuf_isset) {
+ break;
+ }
+ } else if (isset) {
+ break;
+ }
+ }
+
+ // Did we find a pixel that needs updating on this row?
+ if (x_FirstPixelUpdate < displayWidth) {
+
+ // Quickly write out the first changed pixel (saves another array lookup)
+ linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
+ x_LastPixelUpdate = x_FirstPixelUpdate;
+
+ // Step 3: copy all remaining pixels in this row into the pixel line buffer,
+ // while also recording the last pixel in the row that needs updating
+ for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) {
+ isset = buffer[x + y_byteIndex] & y_byteMask;
+ linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
+
+ if (!fromBlank) {
+ dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
+ if (isset != dblbuf_isset) {
+ x_LastPixelUpdate = x;
+ }
+ } else if (isset) {
+ x_LastPixelUpdate = x;
+ }
+ }
+
+ // Step 4: Send the changed pixels on this line to the screen as a single block transfer.
+ // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
+ tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
+ &linePixelBuffer[x_FirstPixelUpdate]);
+
+ somethingChanged = true;
+ }
+ y++;
}
// Copy the Buffer to the Back Buffer
- for (y = 0; y < (displayHeight / 8); y++) {
- for (x = 0; x < displayWidth; x++) {
- uint16_t pos = x + y * displayWidth;
- buffer_back[pos] = buffer[pos];
+ if (somethingChanged)
+ memcpy(buffer_back, buffer, displayBufferSize);
+}
+
+void TFTDisplay::sdlLoop()
+{
+#if defined(LGFX_SDL)
+ static int lastPressed = 0;
+ static int shuttingDown = false;
+ if (settingsMap[displayPanel] == x11) {
+ lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
+ if (sdl_panel_->loop() && !shuttingDown) {
+ LOG_WARN("Window Closed!");
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ }
+
+ // debounce
+ if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed))
+ return;
+ if (!lgfx::v1::gpio_in(37)) {
+ lastPressed = 37;
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ } else if (!lgfx::v1::gpio_in(36)) {
+ lastPressed = 36;
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ } else if (!lgfx::v1::gpio_in(38)) {
+ lastPressed = 38;
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ } else if (!lgfx::v1::gpio_in(39)) {
+ lastPressed = 39;
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ } else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) {
+ lastPressed = SDL_SCANCODE_KP_ENTER;
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
+ } else {
+ lastPressed = 0;
}
}
+#endif
}
// Send a command to the display (low level function)
@@ -1184,15 +1413,23 @@ bool TFTDisplay::connect()
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
tft->setRotation(1); // T-Deck has the TFT in landscape
-#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
+#elif defined(T_WATCH_S3)
tft->setRotation(2); // T-Watch S3 left-handed orientation
-#elif ARCH_PORTDUINO
+#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER)
tft->setRotation(0); // use config.yaml to set rotation
#else
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
#endif
tft->fillScreen(TFT_BLACK);
+ if (this->linePixelBuffer == NULL) {
+ this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
+
+ if (!this->linePixelBuffer) {
+ LOG_ERROR("Not enough memory to create TFT line buffer\n");
+ return false;
+ }
+ }
return true;
}
diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h
index 38cd53ebb..27672ad29 100644
--- a/src/graphics/TFTDisplay.h
+++ b/src/graphics/TFTDisplay.h
@@ -23,6 +23,7 @@ class TFTDisplay : public OLEDDisplay
// Write the buffer to the display memory
virtual void display() override { display(false); };
virtual void display(bool fromBlank);
+ void sdlLoop();
// Turn the display upside down
virtual void flipScreenVertically();
@@ -57,4 +58,6 @@ class TFTDisplay : public OLEDDisplay
// Connect to the display
virtual bool connect() override;
+
+ uint16_t *linePixelBuffer = nullptr;
};
\ No newline at end of file
diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp
index 8d7e91000..08466662c 100644
--- a/src/graphics/draw/ClockRenderer.cpp
+++ b/src/graphics/draw/ClockRenderer.cpp
@@ -186,7 +186,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
- int line = 1;
+
// === Set Title, Blank for Clock
const char *titleStr = "";
// === Header ===
@@ -218,7 +218,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
hour %= 12;
if (hour == 0)
hour = 12;
- bool isPM = hour >= 12;
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
} else {
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
@@ -230,6 +229,8 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
#ifdef T_WATCH_S3
float scale = 1.5;
+#elif defined(CHATTER_2)
+ float scale = 1.1;
#else
float scale = 0.75;
if (isHighResolution) {
@@ -285,6 +286,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
int yOffset = (isHighResolution) ? 3 : 1;
#ifdef SENSECAP_INDICATOR
yOffset -= 3;
+#endif
+#ifdef T_DECK
+ yOffset -= 5;
#endif
if (config.display.use_12h_clock) {
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
@@ -362,7 +366,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// hour hand radius and y coordinate
int16_t hourHandRadius = radius * 0.35;
if (isHighResolution) {
- int16_t hourHandRadius = radius * 0.55;
+ hourHandRadius = radius * 0.55;
}
int16_t hourHandNoonY = centerY - hourHandRadius;
@@ -381,7 +385,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
bool isPM = hour >= 12;
if (config.display.use_12h_clock) {
- bool isPM = hour >= 12;
+ isPM = hour >= 12;
display->setFont(FONT_SMALL);
int yOffset = isHighResolution ? 1 : 0;
#ifdef USE_EINK
diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp
index b1a901f99..a0f29f10d 100644
--- a/src/graphics/draw/DebugRenderer.cpp
+++ b/src/graphics/draw/DebugRenderer.cpp
@@ -94,7 +94,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat,
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
+ ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
8, imgQuestionL1);
@@ -106,7 +107,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
#endif
} else {
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
8, imgSFL1);
@@ -121,7 +122,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
} else {
// TODO: Raspberry Pi supports more than just the one screen size
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
+ ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
imgInfoL1);
@@ -412,9 +414,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
float freq = RadioLibInterface::instance->getFreq();
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
if (config.lora.channel_num == 0) {
- snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr);
+ snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
} else {
- snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num);
+ snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
}
size_t len = strlen(frequencyslot);
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
@@ -483,7 +485,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
}
// ****************************
-// * Memory Screen *
+// * System Screen *
// ****************************
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
@@ -593,7 +595,19 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
}
line += 1;
char appversionstr[35];
- snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION));
+ snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
+ char appversionstr_formatted[40];
+ char *lastDot = strrchr(appversionstr, '.');
+ if (lastDot) {
+ size_t prefixLen = lastDot - appversionstr;
+ strncpy(appversionstr_formatted, appversionstr, prefixLen);
+ appversionstr_formatted[prefixLen] = '\0';
+ strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
+ strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
+ strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
+ strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
+ appversionstr[sizeof(appversionstr) - 1] = '\0';
+ }
int textWidth = display->getStringWidth(appversionstr);
int nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
index f6b250ebc..bcd8d8ee8 100644
--- a/src/graphics/draw/MenuHandler.cpp
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -15,6 +15,9 @@
#include "modules/CannedMessageModule.h"
#include "modules/KeyVerificationModule.h"
+#include "modules/TraceRouteModule.h"
+#include
+
extern uint16_t TFT_MESH;
namespace graphics
@@ -23,6 +26,27 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
+void menuHandler::OnboardMessage()
+{
+ static const char *optionsArray[] = {"OK", "Got it!"};
+ enum optionsNumbers { OK, got };
+ BannerOverlayOptions bannerOptions;
+#if HAS_TFT
+ bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu.";
+#elif defined(BUTTON_PIN)
+ bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu.";
+#else
+ bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections.";
+#endif
+ bannerOptions.optionsArrayPtr = optionsArray;
+ bannerOptions.optionsCount = 2;
+ bannerOptions.bannerCallback = [](int selected) -> void {
+ menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
+ screen->runNow();
+ };
+ screen->showOverlayBanner(bannerOptions);
+}
+
void menuHandler::LoraRegionPicker(uint32_t duration)
{
static const char *optionsArray[] = {"Back",
@@ -51,12 +75,14 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
"PH_915",
"ANZ_433",
"KZ_433",
- "KZ_863"};
+ "KZ_863",
+ "NP_865",
+ "BR_902"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Set the LoRa region";
bannerOptions.durationMs = duration;
bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 25;
+ bannerOptions.optionsCount = 27;
bannerOptions.InitialSelected = 0;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
@@ -115,6 +141,22 @@ void menuHandler::TwelveHourPicker()
screen->showOverlayBanner(bannerOptions);
}
+// Reusable confirmation prompt function
+void menuHandler::showConfirmationBanner(const char *message, std::function onConfirm)
+{
+ static const char *confirmOptions[] = {"No", "Yes"};
+ BannerOverlayOptions confirmBanner;
+ confirmBanner.message = message;
+ confirmBanner.optionsArrayPtr = confirmOptions;
+ confirmBanner.optionsCount = 2;
+ confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void {
+ if (confirmSelected == 1) {
+ onConfirm();
+ }
+ };
+ screen->showOverlayBanner(confirmBanner);
+}
+
void menuHandler::ClockFacePicker()
{
static const char *optionsArray[] = {"Back", "Digital", "Analog"};
@@ -151,6 +193,7 @@ void menuHandler::TZPicker()
"US/Mountain",
"US/Central",
"US/Eastern",
+ "BR/Brasilia",
"UTC",
"EU/Western",
"EU/"
@@ -165,7 +208,7 @@ void menuHandler::TZPicker()
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Pick Timezone";
bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 17;
+ bannerOptions.optionsCount = 19;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuHandler::menuQueue = menuHandler::clock_menu;
@@ -184,25 +227,27 @@ void menuHandler::TZPicker()
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
} else if (selected == 7) { // Eastern
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
- } else if (selected == 8) { // UTC
- strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
- } else if (selected == 9) { // EU/Western
+ } else if (selected == 8) { // Brazil
+ strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef));
+ } else if (selected == 9) { // UTC
+ strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef));
+ } else if (selected == 10) { // EU/Western
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
- } else if (selected == 10) { // EU/Central
+ } else if (selected == 11) { // EU/Central
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
- } else if (selected == 11) { // EU/Eastern
+ } else if (selected == 12) { // EU/Eastern
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
- } else if (selected == 12) { // Asia/Kolkata
+ } else if (selected == 13) { // Asia/Kolkata
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
- } else if (selected == 13) { // China
+ } else if (selected == 14) { // China
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
- } else if (selected == 14) { // AU/AWST
+ } else if (selected == 15) { // AU/AWST
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
- } else if (selected == 15) { // AU/ACST
+ } else if (selected == 16) { // AU/ACST
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
- } else if (selected == 16) { // AU/AEST
+ } else if (selected == 17) { // AU/AEST
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
- } else if (selected == 17) { // NZ
+ } else if (selected == 18) { // NZ
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
}
if (selected != 0) {
@@ -288,13 +333,13 @@ void menuHandler::messageResponseMenu()
void menuHandler::homeBaseMenu()
{
- enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Bluetooth, Sleep, enumEnd };
+ enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
-#ifdef PIN_EINK_EN
+#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
optionsArray[options] = "Toggle Backlight";
optionsEnumArray[options++] = Backlight;
#else
@@ -310,8 +355,6 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
- optionsArray[options] = "Bluetooth Toggle";
- optionsEnumArray[options++] = Bluetooth;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Home Action";
@@ -320,12 +363,24 @@ void menuHandler::homeBaseMenu()
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Backlight) {
-#ifdef PIN_EINK_EN
- if (digitalRead(PIN_EINK_EN) == HIGH) {
+#if defined(PIN_EINK_EN)
+ if (uiconfig.screen_brightness == 1) {
+ uiconfig.screen_brightness = 0;
digitalWrite(PIN_EINK_EN, LOW);
} else {
+ uiconfig.screen_brightness = 1;
digitalWrite(PIN_EINK_EN, HIGH);
}
+ saveUIConfig();
+#elif defined(PCA_PIN_EINK_EN)
+ if (uiconfig.screen_brightness == 1) {
+ uiconfig.screen_brightness = 0;
+ io.digitalWrite(PCA_PIN_EINK_EN, LOW);
+ } else {
+ uiconfig.screen_brightness = 1;
+ io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
+ }
+ saveUIConfig();
#endif
} else if (selected == Sleep) {
screen->setOn(false);
@@ -336,9 +391,35 @@ void menuHandler::homeBaseMenu()
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
- } else if (selected == Bluetooth) {
- menuQueue = bluetooth_toggle_menu;
- screen->runNow();
+ }
+ };
+ screen->showOverlayBanner(bannerOptions);
+}
+
+void menuHandler::textMessageBaseMenu()
+{
+ enum optionsNumbers { Back, Preset, Freetext, enumEnd };
+
+ static const char *optionsArray[enumEnd] = {"Back"};
+ static int optionsEnumArray[enumEnd] = {Back};
+ int options = 1;
+ optionsArray[options] = "New Preset Msg";
+ optionsEnumArray[options++] = Preset;
+ if (kb_found) {
+ optionsArray[options] = "New Freetext Msg";
+ optionsEnumArray[options++] = Freetext;
+ }
+
+ BannerOverlayOptions bannerOptions;
+ bannerOptions.message = "Message Action";
+ bannerOptions.optionsArrayPtr = optionsArray;
+ bannerOptions.optionsEnumPtr = optionsEnumArray;
+ bannerOptions.optionsCount = options;
+ bannerOptions.bannerCallback = [](int selected) -> void {
+ if (selected == Preset) {
+ cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
+ } else if (selected == Freetext) {
+ cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
}
};
screen->showOverlayBanner(bannerOptions);
@@ -346,25 +427,22 @@ void menuHandler::homeBaseMenu()
void menuHandler::systemBaseMenu()
{
- // Check if brightness is supported
- bool hasSupportBrightness = false;
-#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || HAS_TFT
- hasSupportBrightness = true;
-#endif
-
- enum optionsNumbers { Back, Notifications, ScreenOptions, PowerMenu, 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;
optionsArray[options] = "Notifications";
optionsEnumArray[options++] = Notifications;
-#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \
- defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
+#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \
+ defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
optionsArray[options] = "Screen Options";
optionsEnumArray[options++] = ScreenOptions;
#endif
+ optionsArray[options] = "Bluetooth Toggle";
+ optionsEnumArray[options++] = Bluetooth;
+
optionsArray[options] = "Reboot/Shutdown";
optionsEnumArray[options++] = PowerMenu;
@@ -391,6 +469,9 @@ void menuHandler::systemBaseMenu()
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow();
+ } else if (selected == Bluetooth) {
+ menuQueue = bluetooth_toggle_menu;
+ screen->runNow();
} else if (selected == Back && !test_enabled) {
test_count++;
if (test_count > 4) {
@@ -403,7 +484,7 @@ void menuHandler::systemBaseMenu()
void menuHandler::favoriteBaseMenu()
{
- enum optionsNumbers { Back, Preset, Freetext, Remove, enumEnd };
+ enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
static int optionsEnumArray[enumEnd] = {Back, Preset};
int options = 2;
@@ -412,6 +493,8 @@ void menuHandler::favoriteBaseMenu()
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
+ optionsArray[options] = "Trace Route";
+ optionsEnumArray[options++] = TraceRoute;
optionsArray[options] = "Remove Favorite";
optionsEnumArray[options++] = Remove;
@@ -421,13 +504,17 @@ void menuHandler::favoriteBaseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
- if (selected == 1) {
+ if (selected == Preset) {
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
- } else if (selected == 2 && kb_found) {
+ } else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
- } else if ((!kb_found && selected == 2) || (selected == 3 && kb_found)) {
+ } else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow();
+ } else if (selected == TraceRoute) {
+ if (traceRouteModule) {
+ traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum);
+ }
}
};
screen->showOverlayBanner(bannerOptions);
@@ -466,12 +553,12 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu()
{
- enum optionsNumbers { Back, Favorite, Verify, Reset };
- static const char *optionsArray[] = {"Back", "Add Favorite", "Key Verification", "Reset NodeDB"};
+ enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
+ static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 4;
+ bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) {
menuQueue = add_favorite;
@@ -482,6 +569,9 @@ void menuHandler::nodeListMenu()
} else if (selected == Reset) {
menuQueue = reset_node_db_menu;
screen->runNow();
+ } else if (selected == TraceRoute) {
+ menuQueue = trace_route_menu;
+ screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
@@ -635,7 +725,7 @@ void menuHandler::BrightnessPickerMenu()
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
// For HELTEC devices, use analogWrite to control backlight
analogWrite(VTFT_LEDA, uiconfig.screen_brightness);
-#elif defined(ST7789_CS)
+#elif defined(ST7789_CS) || defined(ST7796_CS)
static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness);
@@ -678,6 +768,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 10;
bannerOptions.bannerCallback = [display](int selected) -> void {
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
uint8_t TFT_MESH_r = 0;
uint8_t TFT_MESH_g = 0;
uint8_t TFT_MESH_b = 0;
@@ -729,7 +820,6 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
screen->runNow();
}
-#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
if (selected != 0) {
display->setColor(BLACK);
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
@@ -792,9 +882,8 @@ void menuHandler::shutdownMenu()
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
- IF_SCREEN(screen->showSimpleBanner("Shutting Down...", 0));
- nodeDB->saveToDisk();
- power->shutdown();
+ InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
+ inputBroker->injectInputEvent(&event);
} else {
menuQueue = power_menu;
screen->runNow();
@@ -827,13 +916,24 @@ void menuHandler::removeFavoriteMenu()
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
+ LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum);
nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum);
- screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ screen->setFrames(graphics::Screen::FOCUS_DEFAULT);
}
};
screen->showOverlayBanner(bannerOptions);
}
+void menuHandler::traceRouteMenu()
+{
+ screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void {
+ LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule);
+ if (traceRouteModule) {
+ traceRouteModule->startTraceRoute(nodenum);
+ }
+ });
+}
+
void menuHandler::testMenu()
{
@@ -924,23 +1024,28 @@ void menuHandler::screenOptionsMenu()
{
// Check if brightness is supported
bool hasSupportBrightness = false;
-#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || HAS_TFT
+#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
hasSupportBrightness = true;
#endif
+#if defined(T_DECK)
+ // TDeck Doesn't seem to support brightness at all, at least not reliably
+ hasSupportBrightness = false;
+#endif
+
enum optionsNumbers { Back, Brightness, ScreenColor };
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
int options = 1;
// Only show brightness for B&W displays
- if (hasSupportBrightness && !HAS_TFT) {
+ if (hasSupportBrightness) {
optionsArray[options] = "Brightness";
optionsEnumArray[options++] = Brightness;
}
// Only show screen color for TFT displays
-#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = ScreenColor;
#endif
@@ -1048,6 +1153,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case lora_picker:
LoraRegionPicker();
break;
+ case no_timeout_lora_picker:
+ LoraRegionPicker(0);
+ break;
case TZ_picker:
TZPicker();
break;
@@ -1101,6 +1209,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case remove_favorite:
removeFavoriteMenu();
break;
+ case trace_route_menu:
+ traceRouteMenu();
+ break;
case test_menu:
testMenu();
break;
diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h
index 2273dbbed..b15cf237d 100644
--- a/src/graphics/draw/MenuHandler.h
+++ b/src/graphics/draw/MenuHandler.h
@@ -10,6 +10,7 @@ class menuHandler
enum screenMenus {
menu_none,
lora_picker,
+ no_timeout_lora_picker,
TZ_picker,
twelve_hour_picker,
clock_face_picker,
@@ -36,18 +37,22 @@ class menuHandler
system_base_menu,
key_verification_init,
key_verification_final_prompt,
- throttle_message
+ trace_route_menu,
+ throttle_message,
};
static screenMenus menuQueue;
+ static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000);
static void handleMenuSwitch(OLEDDisplay *display);
+ static void showConfirmationBanner(const char *message, std::function onConfirm);
static void clockMenu();
static void TZPicker();
static void TwelveHourPicker();
static void ClockFacePicker();
static void messageResponseMenu();
static void homeBaseMenu();
+ static void textMessageBaseMenu();
static void systemBaseMenu();
static void favoriteBaseMenu();
static void positionBaseMenu();
@@ -63,6 +68,7 @@ class menuHandler
static void shutdownMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
+ static void traceRouteMenu();
static void testMenu();
static void numberTest();
static void wifiBaseMenu();
diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp
index 3df8a003c..117829167 100644
--- a/src/graphics/draw/MessageRenderer.cpp
+++ b/src/graphics/draw/MessageRenderer.cpp
@@ -137,7 +137,11 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
display->drawString(cursorX + 1, fontY, textChunk.c_str());
}
display->drawString(cursorX, fontY, textChunk.c_str());
+#if defined(OLED_UA) || defined(OLED_RU)
+ cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true);
+#else
cursorX += display->getStringWidth(textChunk.c_str());
+#endif
i = nextControl;
continue;
}
@@ -155,7 +159,12 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
display->drawString(cursorX + 1, fontY, remaining.c_str());
}
display->drawString(cursorX, fontY, remaining.c_str());
+#if defined(OLED_UA) || defined(OLED_RU)
+ cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true);
+#else
cursorX += display->getStringWidth(remaining.c_str());
+#endif
+
break;
}
}
@@ -273,7 +282,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
currentKey ^= ((size_t)mp.id << 24);
if (cachedKey != currentKey) {
- LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
+ LOG_INFO("Onscreen message scroll cache key needs updating: cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
// Cache miss - regenerate lines and heights
cachedLines = generateLines(display, headerStr, messageBuf, textWidth);
@@ -374,10 +383,16 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS
} else {
word += ch;
std::string test = line + word;
- // Keep these lines for diagnostics
- // LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
- // LOG_INFO("Current String: %s", test.c_str());
- if (display->getStringWidth(test.c_str()) > textWidth) {
+// Keep these lines for diagnostics
+// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
+// LOG_INFO("Current String: %s", test.c_str());
+// Note: there are boolean comparison uint16 (getStringWidth) with int (textWidth), hope textWidth is always positive :)
+#if defined(OLED_UA) || defined(OLED_RU)
+ uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true);
+#else
+ uint16_t strWidth = display->getStringWidth(test.c_str());
+#endif
+ if (strWidth > textWidth) {
if (!line.empty())
lines.push_back(line);
line = word;
diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp
index 7350c204f..3d635e588 100644
--- a/src/graphics/draw/NotificationRenderer.cpp
+++ b/src/graphics/draw/NotificationRenderer.cpp
@@ -156,7 +156,7 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
resetBanner();
return;
}
- if (curSelected == numDigits) {
+ if (curSelected == static_cast(numDigits)) {
alertBannerCallback(currentNumber);
resetBanner();
return;
@@ -383,7 +383,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
uint8_t firstOptionToShow = 0;
if (alertBannerOptions > 0) {
- if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
+ if (visibleTotalLines - lineCount == 1) {
+ firstOptionToShow = curSelected;
+ } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
else
@@ -392,6 +394,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
firstOptionToShow = 0;
}
}
+ // Useful log line for troubleshooting:
+ /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u",
+ alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) {
diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp
index 9be8b04f4..049722df8 100644
--- a/src/graphics/draw/UIRenderer.cpp
+++ b/src/graphics/draw/UIRenderer.cpp
@@ -24,6 +24,23 @@ extern graphics::Screen *screen;
namespace graphics
{
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
+std::vector graphics::UIRenderer::favoritedNodes;
+
+void graphics::UIRenderer::rebuildFavoritedNodes()
+{
+ favoritedNodes.clear();
+ size_t total = nodeDB->getNumMeshNodes();
+ for (size_t i = 0; i < total; i++) {
+ meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
+ if (!n || n->num == nodeDB->getNodeNum())
+ continue;
+ if (n->is_favorite)
+ favoritedNodes.push_back(n);
+ }
+
+ std::sort(favoritedNodes.begin(), favoritedNodes.end(),
+ [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; });
+}
#if !MESHTASTIC_EXCLUDE_GPS
// GeoCoord object for coordinate conversions
@@ -177,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
}
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
if (isHighResolution) {
@@ -201,27 +218,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
// **********************
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
{
- // --- Cache favorite nodes for the current frame only, to save computation ---
- static std::vector favoritedNodes;
- static int prevFrame = -1;
- // --- Only rebuild favorites list if we're on a new frame ---
- if (state->currentFrame != prevFrame) {
- prevFrame = state->currentFrame;
- favoritedNodes.clear();
- size_t total = nodeDB->getNumMeshNodes();
- for (size_t i = 0; i < total; i++) {
- meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
- // Skip nulls and ourself
- if (!n || n->num == nodeDB->getNodeNum())
- continue;
- if (n->is_favorite)
- favoritedNodes.push_back(n);
- }
- // Keep a stable, consistent display order
- std::sort(favoritedNodes.begin(), favoritedNodes.end(),
- [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; });
- }
if (favoritedNodes.empty())
return;
@@ -657,7 +654,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char combinedName[50];
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble);
- if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) {
+ if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) {
size_t len = strlen(combinedName);
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
combinedName[len - 3] = '\0'; // Remove the last three characters
@@ -668,7 +665,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName);
} else {
// === LongName Centered ===
- textWidth = display->getStringWidth(longName);
+ textWidth = display->getStringWidth(longNameStr.c_str());
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h
index 9e5e8c4b4..3c8e1dd9d 100644
--- a/src/graphics/draw/UIRenderer.h
+++ b/src/graphics/draw/UIRenderer.h
@@ -61,6 +61,8 @@ class UIRenderer
static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static NodeNum currentFavoriteNodeNum;
+ static std::vector favoritedNodes;
+ static void rebuildFavoritedNodes();
// OEM screens
#ifdef USERPREFS_OEM_TEXT
diff --git a/src/graphics/fonts/EinkDisplayFonts.cpp b/src/graphics/fonts/EinkDisplayFonts.cpp
index cfe2c931f..497b3b389 100644
--- a/src/graphics/fonts/EinkDisplayFonts.cpp
+++ b/src/graphics/fonts/EinkDisplayFonts.cpp
@@ -1,3 +1,5 @@
+#ifdef USE_EINK
+
#include "EinkDisplayFonts.h"
// Created by https://oleddisplay.squix.ch/ Consider a donation
@@ -1182,3 +1184,5 @@ const uint8_t Monospaced_plain_30[] PROGMEM = {
0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F,
0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255
};
+
+#endif // USE_EINK
diff --git a/src/graphics/fonts/EinkDisplayFonts.h b/src/graphics/fonts/EinkDisplayFonts.h
index 342525a19..a4a44ba47 100644
--- a/src/graphics/fonts/EinkDisplayFonts.h
+++ b/src/graphics/fonts/EinkDisplayFonts.h
@@ -1,6 +1,8 @@
#ifndef EINKDISPLAYFONTS_h
#define EINKDISPLAYFONTS_h
+#ifdef USE_EINK
+
#ifdef ARDUINO
#include
#elif __MBED__
@@ -11,4 +13,7 @@
* Monospaced Plain 30
*/
extern const uint8_t Monospaced_plain_30[] PROGMEM;
+
+#endif // USE_EINK
+
#endif
diff --git a/src/graphics/fonts/OLEDDisplayFontsCS.cpp b/src/graphics/fonts/OLEDDisplayFontsCS.cpp
index 67208b4d9..c8045285e 100644
--- a/src/graphics/fonts/OLEDDisplayFontsCS.cpp
+++ b/src/graphics/fonts/OLEDDisplayFontsCS.cpp
@@ -1,3 +1,5 @@
+#ifdef OLED_CS
+
#include "OLEDDisplayFontsCS.h"
// Font generated or edited with the glyphEditor
@@ -1860,4 +1862,6 @@ const uint8_t ArialMT_Plain_24_CS[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00,
0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00,
0x06, // 255
-};
\ No newline at end of file
+};
+
+#endif // OLED_CS
\ No newline at end of file
diff --git a/src/graphics/fonts/OLEDDisplayFontsPL.cpp b/src/graphics/fonts/OLEDDisplayFontsPL.cpp
index 0767e24e7..00f0913fe 100644
--- a/src/graphics/fonts/OLEDDisplayFontsPL.cpp
+++ b/src/graphics/fonts/OLEDDisplayFontsPL.cpp
@@ -1,4 +1,5 @@
// trunk-ignore-all(clang-format): Preserve long lines
+#ifdef OLED_PL
#include "OLEDDisplayFontsPL.h"
const uint8_t ArialMT_Plain_10_PL[] PROGMEM = {
@@ -1310,4 +1311,6 @@ const uint8_t ArialMT_Plain_24_PL[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254
0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255
-};
\ No newline at end of file
+};
+
+#endif // OLED_PL
\ No newline at end of file
diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp
index fa055d8b5..3a1159511 100644
--- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp
+++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp
@@ -1,3 +1,5 @@
+#ifdef OLED_RU
+
#include "OLEDDisplayFontsRU.h"
// Font generated or edited with the glyphEditor
@@ -423,4 +425,1345 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = {
0x00, 0x00, 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253
0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254
0x00, 0x00, 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0xE0, 0x03, // 255
-};
\ No newline at end of file
+};
+
+// Font generated or edited with the glyphEditor (@mrekin)
+const uint8_t ArialMT_Plain_16_RU[] PROGMEM = {
+ 0x10, // Width: 16
+ 0x13, // Height: 19
+ 0x20, // First char: 32
+ 0xE0, // Number of chars: 224
+ // Jump Table:
+ 0xFF, 0xFF, 0x00, 0x04, // 32
+ 0x00, 0x00, 0x08, 0x05, // 33
+ 0x00, 0x08, 0x0D, 0x06, // 34
+ 0x00, 0x15, 0x1A, 0x09, // 35
+ 0x00, 0x2F, 0x17, 0x09, // 36
+ 0x00, 0x46, 0x26, 0x0E, // 37
+ 0x00, 0x6C, 0x1D, 0x0B, // 38
+ 0x00, 0x89, 0x04, 0x03, // 39
+ 0x00, 0x8D, 0x0C, 0x05, // 40
+ 0x00, 0x99, 0x0B, 0x05, // 41
+ 0x00, 0xA4, 0x0D, 0x06, // 42
+ 0x00, 0xB1, 0x17, 0x09, // 43
+ 0x00, 0xC8, 0x09, 0x04, // 44
+ 0x00, 0xD1, 0x0B, 0x05, // 45
+ 0x00, 0xDC, 0x08, 0x04, // 46
+ 0x00, 0xE4, 0x0A, 0x04, // 47
+ 0x00, 0xEE, 0x17, 0x09, // 48
+ 0x01, 0x05, 0x11, 0x09, // 49
+ 0x01, 0x16, 0x17, 0x09, // 50
+ 0x01, 0x2D, 0x17, 0x09, // 51
+ 0x01, 0x44, 0x17, 0x09, // 52
+ 0x01, 0x5B, 0x17, 0x09, // 53
+ 0x01, 0x72, 0x17, 0x09, // 54
+ 0x01, 0x89, 0x16, 0x09, // 55
+ 0x01, 0x9F, 0x17, 0x09, // 56
+ 0x01, 0xB6, 0x17, 0x09, // 57
+ 0x01, 0xCD, 0x05, 0x04, // 58
+ 0x01, 0xD2, 0x06, 0x04, // 59
+ 0x01, 0xD8, 0x17, 0x09, // 60
+ 0x01, 0xEF, 0x17, 0x09, // 61
+ 0x02, 0x06, 0x17, 0x09, // 62
+ 0x02, 0x1D, 0x16, 0x09, // 63
+ 0x02, 0x33, 0x2F, 0x10, // 64
+ 0x02, 0x62, 0x1D, 0x0B, // 65
+ 0x02, 0x7F, 0x1D, 0x0B, // 66
+ 0x02, 0x9C, 0x20, 0x0C, // 67
+ 0x02, 0xBC, 0x20, 0x0C, // 68
+ 0x02, 0xDC, 0x1D, 0x0B, // 69
+ 0x02, 0xF9, 0x19, 0x0A, // 70
+ 0x03, 0x12, 0x20, 0x0C, // 71
+ 0x03, 0x32, 0x1D, 0x0B, // 72
+ 0x03, 0x4F, 0x05, 0x03, // 73
+ 0x03, 0x54, 0x14, 0x08, // 74
+ 0x03, 0x68, 0x1D, 0x0B, // 75
+ 0x03, 0x85, 0x17, 0x09, // 76
+ 0x03, 0x9C, 0x23, 0x0D, // 77
+ 0x03, 0xBF, 0x1D, 0x0B, // 78
+ 0x03, 0xDC, 0x20, 0x0C, // 79
+ 0x03, 0xFC, 0x1C, 0x0B, // 80
+ 0x04, 0x18, 0x20, 0x0C, // 81
+ 0x04, 0x38, 0x1D, 0x0B, // 82
+ 0x04, 0x55, 0x1D, 0x0B, // 83
+ 0x04, 0x72, 0x19, 0x09, // 84
+ 0x04, 0x8B, 0x1D, 0x0B, // 85
+ 0x04, 0xA8, 0x1C, 0x0B, // 86
+ 0x04, 0xC4, 0x2B, 0x0F, // 87
+ 0x04, 0xEF, 0x20, 0x0B, // 88
+ 0x05, 0x0F, 0x19, 0x09, // 89
+ 0x05, 0x28, 0x1A, 0x09, // 90
+ 0x05, 0x42, 0x0C, 0x04, // 91
+ 0x05, 0x4E, 0x0B, 0x04, // 92
+ 0x05, 0x59, 0x09, 0x04, // 93
+ 0x05, 0x62, 0x14, 0x07, // 94
+ 0x05, 0x76, 0x1B, 0x09, // 95
+ 0x05, 0x91, 0x07, 0x05, // 96
+ 0x05, 0x98, 0x17, 0x09, // 97
+ 0x05, 0xAF, 0x17, 0x09, // 98
+ 0x05, 0xC6, 0x14, 0x08, // 99
+ 0x05, 0xDA, 0x17, 0x09, // 100
+ 0x05, 0xF1, 0x17, 0x09, // 101
+ 0x06, 0x08, 0x0A, 0x04, // 102
+ 0x06, 0x12, 0x17, 0x09, // 103
+ 0x06, 0x29, 0x14, 0x08, // 104
+ 0x06, 0x3D, 0x05, 0x04, // 105
+ 0x06, 0x42, 0x06, 0x03, // 106
+ 0x06, 0x48, 0x17, 0x08, // 107
+ 0x06, 0x5F, 0x05, 0x03, // 108
+ 0x06, 0x64, 0x23, 0x0D, // 109
+ 0x06, 0x87, 0x14, 0x08, // 110
+ 0x06, 0x9B, 0x17, 0x09, // 111
+ 0x06, 0xB2, 0x17, 0x09, // 112
+ 0x06, 0xC9, 0x18, 0x09, // 113
+ 0x06, 0xE1, 0x0D, 0x05, // 114
+ 0x06, 0xEE, 0x14, 0x08, // 115
+ 0x07, 0x02, 0x0B, 0x04, // 116
+ 0x07, 0x0D, 0x14, 0x08, // 117
+ 0x07, 0x21, 0x13, 0x07, // 118
+ 0x07, 0x34, 0x1F, 0x0B, // 119
+ 0x07, 0x53, 0x14, 0x07, // 120
+ 0x07, 0x67, 0x13, 0x07, // 121
+ 0x07, 0x7A, 0x14, 0x07, // 122
+ 0x07, 0x8E, 0x0F, 0x05, // 123
+ 0x07, 0x9D, 0x06, 0x03, // 124
+ 0x07, 0xA3, 0x0E, 0x05, // 125
+ 0x07, 0xB1, 0x17, 0x09, // 126
+ 0xFF, 0xFF, 0x00, 0x10, // 127
+ 0xFF, 0xFF, 0x00, 0x10, // 128
+ 0xFF, 0xFF, 0x00, 0x10, // 129
+ 0xFF, 0xFF, 0x00, 0x10, // 130
+ 0xFF, 0xFF, 0x00, 0x10, // 131
+ 0xFF, 0xFF, 0x00, 0x10, // 132
+ 0xFF, 0xFF, 0x00, 0x10, // 133
+ 0xFF, 0xFF, 0x00, 0x10, // 134
+ 0xFF, 0xFF, 0x00, 0x10, // 135
+ 0xFF, 0xFF, 0x00, 0x10, // 136
+ 0xFF, 0xFF, 0x00, 0x10, // 137
+ 0xFF, 0xFF, 0x00, 0x10, // 138
+ 0xFF, 0xFF, 0x00, 0x10, // 139
+ 0xFF, 0xFF, 0x00, 0x10, // 140
+ 0xFF, 0xFF, 0x00, 0x10, // 141
+ 0xFF, 0xFF, 0x00, 0x10, // 142
+ 0xFF, 0xFF, 0x00, 0x10, // 143
+ 0xFF, 0xFF, 0x00, 0x10, // 144
+ 0xFF, 0xFF, 0x00, 0x10, // 145
+ 0xFF, 0xFF, 0x00, 0x10, // 146
+ 0xFF, 0xFF, 0x00, 0x10, // 147
+ 0xFF, 0xFF, 0x00, 0x10, // 148
+ 0xFF, 0xFF, 0x00, 0x10, // 149
+ 0xFF, 0xFF, 0x00, 0x10, // 150
+ 0xFF, 0xFF, 0x00, 0x10, // 151
+ 0xFF, 0xFF, 0x00, 0x10, // 152
+ 0xFF, 0xFF, 0x00, 0x10, // 153
+ 0xFF, 0xFF, 0x00, 0x10, // 154
+ 0xFF, 0xFF, 0x00, 0x10, // 155
+ 0xFF, 0xFF, 0x00, 0x10, // 156
+ 0xFF, 0xFF, 0x00, 0x10, // 157
+ 0xFF, 0xFF, 0x00, 0x10, // 158
+ 0xFF, 0xFF, 0x00, 0x10, // 159
+ 0xFF, 0xFF, 0x00, 0x10, // 160
+ 0x07, 0xC8, 0x09, 0x05, // 161
+ 0x07, 0xD1, 0x17, 0x09, // 162
+ 0x07, 0xE8, 0x17, 0x09, // 163
+ 0x07, 0xFF, 0x14, 0x09, // 164
+ 0x08, 0x13, 0x1A, 0x09, // 165
+ 0x08, 0x2D, 0x06, 0x03, // 166
+ 0x08, 0x33, 0x17, 0x09, // 167
+ 0x08, 0x4A, 0x1D, 0x0B, // 168
+ 0x08, 0x67, 0x23, 0x0C, // 169
+ 0x08, 0x8A, 0x0E, 0x05, // 170
+ 0x08, 0x98, 0x14, 0x09, // 171
+ 0x08, 0xAC, 0x17, 0x09, // 172
+ 0x08, 0xC3, 0x0B, 0x05, // 173
+ 0x08, 0xCE, 0x23, 0x0C, // 174
+ 0x08, 0xF1, 0x19, 0x09, // 175
+ 0x09, 0x0A, 0x0D, 0x06, // 176
+ 0x09, 0x17, 0x17, 0x09, // 177
+ 0x09, 0x2E, 0x0E, 0x05, // 178
+ 0x09, 0x3C, 0x0D, 0x05, // 179
+ 0x09, 0x49, 0x0A, 0x05, // 180
+ 0x09, 0x53, 0x17, 0x09, // 181
+ 0x09, 0x6A, 0x19, 0x09, // 182
+ 0x09, 0x83, 0x08, 0x05, // 183
+ 0x09, 0x8B, 0x17, 0x09, // 184
+ 0x09, 0xA2, 0x0B, 0x05, // 185
+ 0x09, 0xAD, 0x0D, 0x05, // 186
+ 0x09, 0xBA, 0x17, 0x09, // 187
+ 0x09, 0xD1, 0x26, 0x0D, // 188
+ 0x09, 0xF7, 0x26, 0x0D, // 189
+ 0x0A, 0x1D, 0x26, 0x0D, // 190
+ 0x0A, 0x43, 0x1B, 0x0C, // 191
+ 0x0A, 0x5E, 0x1D, 0x0B, // 192
+ 0x0A, 0x7B, 0x1A, 0x0B, // 193
+ 0x0A, 0x95, 0x1D, 0x0B, // 194
+ 0x0A, 0xB2, 0x19, 0x09, // 195
+ 0x0A, 0xCB, 0x1E, 0x0B, // 196
+ 0x0A, 0xE9, 0x1D, 0x0B, // 197
+ 0x0B, 0x06, 0x2C, 0x0F, // 198
+ 0x0B, 0x32, 0x1A, 0x0A, // 199
+ 0x0B, 0x4C, 0x20, 0x0C, // 200
+ 0x0B, 0x6C, 0x20, 0x0C, // 201
+ 0x0B, 0x8C, 0x1A, 0x09, // 202
+ 0x0B, 0xA6, 0x1A, 0x0B, // 203
+ 0x0B, 0xC0, 0x23, 0x0D, // 204
+ 0x0B, 0xE3, 0x1D, 0x0B, // 205
+ 0x0C, 0x00, 0x20, 0x0C, // 206
+ 0x0C, 0x20, 0x1D, 0x0C, // 207
+ 0x0C, 0x3D, 0x1C, 0x0B, // 208
+ 0x0C, 0x59, 0x20, 0x0C, // 209
+ 0x0C, 0x79, 0x19, 0x09, // 210
+ 0x0C, 0x92, 0x1C, 0x0A, // 211
+ 0x0C, 0xAE, 0x23, 0x0D, // 212
+ 0x0C, 0xD1, 0x20, 0x0B, // 213
+ 0x0C, 0xF1, 0x21, 0x0C, // 214
+ 0x0D, 0x12, 0x1A, 0x0B, // 215
+ 0x0D, 0x2C, 0x26, 0x0F, // 216
+ 0x0D, 0x52, 0x2A, 0x0F, // 217
+ 0x0D, 0x7C, 0x23, 0x0D, // 218
+ 0x0D, 0x9F, 0x23, 0x0E, // 219
+ 0x0D, 0xC2, 0x1A, 0x0A, // 220
+ 0x0D, 0xDC, 0x1D, 0x0C, // 221
+ 0x0D, 0xF9, 0x2C, 0x10, // 222
+ 0x0E, 0x25, 0x20, 0x0C, // 223
+ 0x0E, 0x45, 0x17, 0x09, // 224
+ 0x0E, 0x5C, 0x1A, 0x09, // 225
+ 0x0E, 0x76, 0x14, 0x09, // 226
+ 0x0E, 0x8A, 0x10, 0x06, // 227
+ 0x0E, 0x9A, 0x1B, 0x09, // 228
+ 0x0E, 0xB5, 0x17, 0x09, // 229
+ 0x0E, 0xCC, 0x20, 0x0B, // 230
+ 0x0E, 0xEC, 0x11, 0x07, // 231
+ 0x0E, 0xFD, 0x17, 0x09, // 232
+ 0x0F, 0x14, 0x17, 0x09, // 233
+ 0x0F, 0x2B, 0x14, 0x07, // 234
+ 0x0F, 0x3F, 0x17, 0x09, // 235
+ 0x0F, 0x56, 0x1D, 0x0B, // 236
+ 0x0F, 0x73, 0x17, 0x09, // 237
+ 0x0F, 0x8A, 0x17, 0x09, // 238
+ 0x0F, 0xA1, 0x17, 0x09, // 239
+ 0x0F, 0xB8, 0x17, 0x09, // 240
+ 0x0F, 0xCF, 0x14, 0x08, // 241
+ 0x0F, 0xE3, 0x13, 0x07, // 242
+ 0x0F, 0xF6, 0x13, 0x07, // 243
+ 0x10, 0x09, 0x23, 0x0D, // 244
+ 0x10, 0x2C, 0x17, 0x09, // 245
+ 0x10, 0x43, 0x1A, 0x0A, // 246
+ 0x10, 0x5D, 0x14, 0x08, // 247
+ 0x10, 0x71, 0x23, 0x0D, // 248
+ 0x10, 0x94, 0x23, 0x0D, // 249
+ 0x10, 0xB7, 0x1A, 0x0A, // 250
+ 0x10, 0xD1, 0x1D, 0x0C, // 251
+ 0x10, 0xEE, 0x17, 0x09, // 252
+ 0x11, 0x05, 0x14, 0x08, // 253
+ 0x11, 0x19, 0x20, 0x0C, // 254
+ 0x11, 0x39, 0x17, 0x09, // 255
+ // Font Data:
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33
+ 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34
+ 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00,
+ 0xB8, 0x08, 0x00, 0x80, 0x08, // 35
+ 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00,
+ 0x30, 0x1C, // 36
+ 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00,
+ 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37
+ 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00,
+ 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38
+ 0x00, 0x00, 0x00, 0x78, // 39
+ 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40
+ 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41
+ 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42
+ 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00,
+ 0x00, 0x02, // 43
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44
+ 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46
+ 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47
+ 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00,
+ 0xE0, 0x1F, // 48
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49
+ 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00,
+ 0xE0, 0x40, // 50
+ 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00,
+ 0x00, 0x1C, // 51
+ 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00,
+ 0x00, 0x08, // 52
+ 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00,
+ 0x08, 0x1E, // 53
+ 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00,
+ 0x20, 0x1E, // 54
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00,
+ 0x18, // 55
+ 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00,
+ 0x60, 0x1C, // 56
+ 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00,
+ 0xE0, 0x1F, // 57
+ 0x00, 0x00, 0x00, 0x40, 0x40, // 58
+ 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59
+ 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00,
+ 0x40, 0x10, // 60
+ 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00,
+ 0x80, 0x08, // 61
+ 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00,
+ 0x00, 0x02, // 62
+ 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00,
+ 0xE0, // 63
+ 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02,
+ 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01,
+ 0x60, 0x10, 0x01, 0x80, 0x8F, // 64
+ 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00,
+ 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00,
+ 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00,
+ 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00,
+ 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00,
+ 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00,
+ 0x08, 0x02, 0x00, 0x08, // 70
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00,
+ 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73
+ 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00,
+ 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00,
+ 0x00, 0x40, // 76
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00,
+ 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00,
+ 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00,
+ 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00,
+ 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00,
+ 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x5F, // 81
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00,
+ 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82
+ 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00,
+ 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83
+ 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x08, 0x00, 0x00, 0x08, // 84
+ 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85
+ 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00,
+ 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86
+ 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00,
+ 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00,
+ 0x18, // 87
+ 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00,
+ 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88
+ 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x08, // 89
+ 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00,
+ 0x18, 0x40, 0x00, 0x08, 0x40, // 90
+ 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91
+ 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92
+ 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93
+ 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94
+ 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02,
+ 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95
+ 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96
+ 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00,
+ 0x80, 0x7F, // 97
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0x00, 0x1F, // 98
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0xF8, 0x7F, // 100
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00,
+ 0x00, 0x17, // 101
+ 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01,
+ 0xC0, 0xFF, // 103
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104
+ 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105
+ 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00,
+ 0x40, 0x40, // 107
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00,
+ 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0x00, 0x1F, // 111
+ 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0x00, 0x1F, // 112
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0xC0, 0xFF, 0x03, // 113
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114
+ 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115
+ 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116
+ 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117
+ 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118
+ 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00,
+ 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119
+ 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120
+ 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121
+ 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122
+ 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123
+ 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124
+ 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125
+ 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00,
+ 0x00, 0x01, // 126
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00,
+ 0x00, 0x11, // 162
+ 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00,
+ 0x20, 0x20, // 163
+ 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164
+ 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00,
+ 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165
+ 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166
+ 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01,
+ 0x00, 0x0C, // 167
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00,
+ 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 168
+ 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00,
+ 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169
+ 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170
+ 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171
+ 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00,
+ 0x80, 0x0F, // 172
+ 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173
+ 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00,
+ 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174
+ 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00,
+ 0x04, 0x00, 0x00, 0x04, // 175
+ 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176
+ 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00,
+ 0x00, 0x41, // 177
+ 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178
+ 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180
+ 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00,
+ 0xC0, 0x7F, // 181
+ 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0xF8, 0xFF, 0x03, 0x08, // 182
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00,
+ 0x00, 0x17, // 184
+ 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185
+ 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00,
+ 0x00, 0x04, // 187
+ 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00,
+ 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188
+ 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00,
+ 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189
+ 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00,
+ 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02,
+ 0x00, 0x00, 0x03, 0x00, 0x80, 0x01, // 191
+ 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00,
+ 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00,
+ 0x08, 0x66, 0x00, 0x08, 0x3C, // 193
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00,
+ 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 194
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x08, 0x00, 0x00, 0x18, // 195
+ 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00,
+ 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00,
+ 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 197
+ 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00,
+ 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00,
+ 0x08, 0x40, // 198
+ 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00,
+ 0x10, 0x22, 0x00, 0xE0, 0x1D, // 199
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00,
+ 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 200
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00,
+ 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 201
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00,
+ 0x08, 0x20, 0x00, 0x08, 0x40, // 202
+ 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x08, 0x00, 0x00, 0xF8, 0x7F, // 203
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00,
+ 0x00, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00,
+ 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 206
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00,
+ 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 208
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00,
+ 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 209
+ 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x08, 0x00, 0x00, 0x08, // 210
+ 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00,
+ 0xC0, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 211
+ 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00,
+ 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212
+ 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00,
+ 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, 0x40, // 213
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 214
+ 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00,
+ 0x00, 0x04, 0x00, 0xF8, 0x7F, // 215
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00,
+ 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 216
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00,
+ 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0,
+ 0x01, // 217
+ 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00,
+ 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00,
+ 0x00, 0x63, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00,
+ 0x00, 0x63, 0x00, 0x00, 0x3E, // 220
+ 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00,
+ 0x10, 0x61, 0x00, 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221
+ 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00,
+ 0x30, 0x30, 0x00, 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00,
+ 0x80, 0x0F, // 222
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00,
+ 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 223
+ 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00,
+ 0x80, 0x7F, // 224
+ 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00,
+ 0x18, 0x3F, 0x00, 0x00, 0x0C, // 225
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, 0x3B, // 226
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227
+ 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00,
+ 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 228
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00,
+ 0x00, 0x17, // 229
+ 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00,
+ 0x00, 0x0E, 0x00, 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, // 230
+ 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00,
+ 0xC0, 0x7F, // 232
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00,
+ 0xC0, 0x7F, // 233
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, 0x40, // 234
+ 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00,
+ 0xC0, 0x7F, // 235
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00,
+ 0xC0, 0x07, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00,
+ 0xC0, 0x7F, // 237
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0x00, 0x1F, // 238
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00,
+ 0xC0, 0x7F, // 239
+ 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00,
+ 0x00, 0x1F, // 240
+ 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 241
+ 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 242
+ 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 243
+ 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03,
+ 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244
+ 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00,
+ 0x40, 0x40, // 245
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00,
+ 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 246
+ 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 247
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249
+ 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00,
+ 0x00, 0x64, 0x00, 0x00, 0x38, // 250
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00,
+ 0x00, 0x38, // 252
+ 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, 0x1F, // 253
+ 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00,
+ 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, 0x1F, // 254
+ 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00,
+ 0xC0, 0x7F, // 255
+};
+
+// Font generated or edited with the glyphEditor (@mrekin)
+const uint8_t ArialMT_Plain_24_RU[] PROGMEM = {
+ 0x18, // Width: 24
+ 0x1C, // Height: 28
+ 0x20, // First char: 32
+ 0xE0, // Number of chars: 224
+ // Jump Table:
+ 0xFF, 0xFF, 0x00, 0x07, // 32
+ 0x00, 0x00, 0x13, 0x08, // 33
+ 0x00, 0x13, 0x1A, 0x09, // 34
+ 0x00, 0x2D, 0x33, 0x0D, // 35
+ 0x00, 0x60, 0x2F, 0x0D, // 36
+ 0x00, 0x8F, 0x4F, 0x15, // 37
+ 0x00, 0xDE, 0x3B, 0x10, // 38
+ 0x01, 0x19, 0x0A, 0x05, // 39
+ 0x01, 0x23, 0x1C, 0x08, // 40
+ 0x01, 0x3F, 0x1B, 0x08, // 41
+ 0x01, 0x5A, 0x21, 0x09, // 42
+ 0x01, 0x7B, 0x32, 0x0E, // 43
+ 0x01, 0xAD, 0x10, 0x07, // 44
+ 0x01, 0xBD, 0x1B, 0x08, // 45
+ 0x01, 0xD8, 0x0F, 0x07, // 46
+ 0x01, 0xE7, 0x19, 0x07, // 47
+ 0x02, 0x00, 0x2F, 0x0D, // 48
+ 0x02, 0x2F, 0x23, 0x0D, // 49
+ 0x02, 0x52, 0x2F, 0x0D, // 50
+ 0x02, 0x81, 0x2F, 0x0D, // 51
+ 0x02, 0xB0, 0x2F, 0x0D, // 52
+ 0x02, 0xDF, 0x2F, 0x0D, // 53
+ 0x03, 0x0E, 0x2F, 0x0D, // 54
+ 0x03, 0x3D, 0x2D, 0x0D, // 55
+ 0x03, 0x6A, 0x2F, 0x0D, // 56
+ 0x03, 0x99, 0x2F, 0x0D, // 57
+ 0x03, 0xC8, 0x0F, 0x07, // 58
+ 0x03, 0xD7, 0x10, 0x07, // 59
+ 0x03, 0xE7, 0x2F, 0x0E, // 60
+ 0x04, 0x16, 0x2F, 0x0E, // 61
+ 0x04, 0x45, 0x2E, 0x0E, // 62
+ 0x04, 0x73, 0x2E, 0x0D, // 63
+ 0x04, 0xA1, 0x5B, 0x18, // 64
+ 0x04, 0xFC, 0x3B, 0x0F, // 65
+ 0x05, 0x37, 0x3B, 0x10, // 66
+ 0x05, 0x72, 0x3F, 0x11, // 67
+ 0x05, 0xB1, 0x3F, 0x11, // 68
+ 0x05, 0xF0, 0x3B, 0x10, // 69
+ 0x06, 0x2B, 0x35, 0x0F, // 70
+ 0x06, 0x60, 0x43, 0x13, // 71
+ 0x06, 0xA3, 0x3B, 0x11, // 72
+ 0x06, 0xDE, 0x0F, 0x06, // 73
+ 0x06, 0xED, 0x27, 0x0C, // 74
+ 0x07, 0x14, 0x3F, 0x10, // 75
+ 0x07, 0x53, 0x2F, 0x0D, // 76
+ 0x07, 0x82, 0x43, 0x13, // 77
+ 0x07, 0xC5, 0x3B, 0x11, // 78
+ 0x08, 0x00, 0x47, 0x13, // 79
+ 0x08, 0x47, 0x3A, 0x10, // 80
+ 0x08, 0x81, 0x47, 0x13, // 81
+ 0x08, 0xC8, 0x3F, 0x11, // 82
+ 0x09, 0x07, 0x3B, 0x10, // 83
+ 0x09, 0x42, 0x35, 0x0E, // 84
+ 0x09, 0x77, 0x3B, 0x11, // 85
+ 0x09, 0xB2, 0x39, 0x0F, // 86
+ 0x09, 0xEB, 0x59, 0x17, // 87
+ 0x0A, 0x44, 0x3B, 0x0F, // 88
+ 0x0A, 0x7F, 0x3D, 0x10, // 89
+ 0x0A, 0xBC, 0x37, 0x0F, // 90
+ 0x0A, 0xF3, 0x14, 0x07, // 91
+ 0x0B, 0x07, 0x1B, 0x07, // 92
+ 0x0B, 0x22, 0x18, 0x07, // 93
+ 0x0B, 0x3A, 0x2A, 0x0C, // 94
+ 0x0B, 0x64, 0x34, 0x0D, // 95
+ 0x0B, 0x98, 0x11, 0x08, // 96
+ 0x0B, 0xA9, 0x2F, 0x0D, // 97
+ 0x0B, 0xD8, 0x33, 0x0E, // 98
+ 0x0C, 0x0B, 0x2B, 0x0C, // 99
+ 0x0C, 0x36, 0x2F, 0x0E, // 100
+ 0x0C, 0x65, 0x2F, 0x0D, // 101
+ 0x0C, 0x94, 0x1A, 0x07, // 102
+ 0x0C, 0xAE, 0x2F, 0x0E, // 103
+ 0x0C, 0xDD, 0x2F, 0x0E, // 104
+ 0x0D, 0x0C, 0x0F, 0x05, // 105
+ 0x0D, 0x1B, 0x10, 0x06, // 106
+ 0x0D, 0x2B, 0x2F, 0x0C, // 107
+ 0x0D, 0x5A, 0x0F, 0x06, // 108
+ 0x0D, 0x69, 0x47, 0x14, // 109
+ 0x0D, 0xB0, 0x2F, 0x0E, // 110
+ 0x0D, 0xDF, 0x2F, 0x0D, // 111
+ 0x0E, 0x0E, 0x33, 0x0E, // 112
+ 0x0E, 0x41, 0x30, 0x0E, // 113
+ 0x0E, 0x71, 0x1E, 0x08, // 114
+ 0x0E, 0x8F, 0x2B, 0x0C, // 115
+ 0x0E, 0xBA, 0x1B, 0x07, // 116
+ 0x0E, 0xD5, 0x2F, 0x0E, // 117
+ 0x0F, 0x04, 0x2A, 0x0B, // 118
+ 0x0F, 0x2E, 0x42, 0x11, // 119
+ 0x0F, 0x70, 0x2B, 0x0B, // 120
+ 0x0F, 0x9B, 0x2A, 0x0C, // 121
+ 0x0F, 0xC5, 0x2B, 0x0C, // 122
+ 0x0F, 0xF0, 0x1C, 0x08, // 123
+ 0x10, 0x0C, 0x10, 0x06, // 124
+ 0x10, 0x1C, 0x1B, 0x08, // 125
+ 0x10, 0x37, 0x32, 0x0E, // 126
+ 0xFF, 0xFF, 0x00, 0x18, // 127
+ 0xFF, 0xFF, 0x00, 0x18, // 128
+ 0xFF, 0xFF, 0x00, 0x18, // 129
+ 0xFF, 0xFF, 0x00, 0x18, // 130
+ 0xFF, 0xFF, 0x00, 0x18, // 131
+ 0xFF, 0xFF, 0x00, 0x18, // 132
+ 0xFF, 0xFF, 0x00, 0x18, // 133
+ 0xFF, 0xFF, 0x00, 0x18, // 134
+ 0xFF, 0xFF, 0x00, 0x18, // 135
+ 0xFF, 0xFF, 0x00, 0x18, // 136
+ 0xFF, 0xFF, 0x00, 0x18, // 137
+ 0xFF, 0xFF, 0x00, 0x18, // 138
+ 0xFF, 0xFF, 0x00, 0x18, // 139
+ 0xFF, 0xFF, 0x00, 0x18, // 140
+ 0xFF, 0xFF, 0x00, 0x18, // 141
+ 0xFF, 0xFF, 0x00, 0x18, // 142
+ 0xFF, 0xFF, 0x00, 0x18, // 143
+ 0xFF, 0xFF, 0x00, 0x18, // 144
+ 0xFF, 0xFF, 0x00, 0x18, // 145
+ 0xFF, 0xFF, 0x00, 0x18, // 146
+ 0xFF, 0xFF, 0x00, 0x18, // 147
+ 0xFF, 0xFF, 0x00, 0x18, // 148
+ 0xFF, 0xFF, 0x00, 0x18, // 149
+ 0xFF, 0xFF, 0x00, 0x18, // 150
+ 0xFF, 0xFF, 0x00, 0x18, // 151
+ 0xFF, 0xFF, 0x00, 0x18, // 152
+ 0xFF, 0xFF, 0x00, 0x18, // 153
+ 0xFF, 0xFF, 0x00, 0x18, // 154
+ 0xFF, 0xFF, 0x00, 0x18, // 155
+ 0xFF, 0xFF, 0x00, 0x18, // 156
+ 0xFF, 0xFF, 0x00, 0x18, // 157
+ 0xFF, 0xFF, 0x00, 0x18, // 158
+ 0xFF, 0xFF, 0x00, 0x18, // 159
+ 0xFF, 0xFF, 0x00, 0x07, // 160
+ 0x10, 0x69, 0x14, 0x08, // 161
+ 0x10, 0x7D, 0x2B, 0x0D, // 162
+ 0x10, 0xA8, 0x2F, 0x0D, // 163
+ 0x10, 0xD7, 0x33, 0x0D, // 164
+ 0x11, 0x0A, 0x31, 0x0D, // 165
+ 0x11, 0x3B, 0x10, 0x06, // 166
+ 0x11, 0x4B, 0x2F, 0x0D, // 167
+ 0x11, 0x7A, 0x3B, 0x10, // 168
+ 0x11, 0xB5, 0x46, 0x12, // 169
+ 0x11, 0xFB, 0x1A, 0x09, // 170
+ 0x12, 0x15, 0x27, 0x0D, // 171
+ 0x12, 0x3C, 0x2F, 0x0E, // 172
+ 0x12, 0x6B, 0x1B, 0x08, // 173
+ 0x12, 0x86, 0x46, 0x12, // 174
+ 0x12, 0xCC, 0x31, 0x0D, // 175
+ 0x12, 0xFD, 0x1E, 0x0A, // 176
+ 0x13, 0x1B, 0x33, 0x0D, // 177
+ 0x13, 0x4E, 0x1A, 0x08, // 178
+ 0x13, 0x68, 0x1A, 0x08, // 179
+ 0x13, 0x82, 0x19, 0x08, // 180
+ 0x13, 0x9B, 0x2F, 0x0E, // 181
+ 0x13, 0xCA, 0x31, 0x0D, // 182
+ 0x13, 0xFB, 0x12, 0x08, // 183
+ 0x14, 0x0D, 0x2F, 0x0D, // 184
+ 0x14, 0x3C, 0x16, 0x08, // 185
+ 0x14, 0x52, 0x1E, 0x09, // 186
+ 0x14, 0x70, 0x2E, 0x0D, // 187
+ 0x14, 0x9E, 0x4F, 0x14, // 188
+ 0x14, 0xED, 0x4B, 0x14, // 189
+ 0x15, 0x38, 0x4B, 0x14, // 190
+ 0x15, 0x83, 0x3B, 0x12, // 191
+ 0x15, 0xBE, 0x3B, 0x0F, // 192
+ 0x15, 0xF9, 0x3B, 0x10, // 193
+ 0x16, 0x34, 0x3B, 0x10, // 194
+ 0x16, 0x6F, 0x31, 0x0D, // 195
+ 0x16, 0xA0, 0x3C, 0x10, // 196
+ 0x16, 0xDC, 0x3B, 0x10, // 197
+ 0x17, 0x17, 0x57, 0x16, // 198
+ 0x17, 0x6E, 0x33, 0x0F, // 199
+ 0x17, 0xA1, 0x3B, 0x11, // 200
+ 0x17, 0xDC, 0x3B, 0x11, // 201
+ 0x18, 0x17, 0x37, 0x0E, // 202
+ 0x18, 0x4E, 0x37, 0x10, // 203
+ 0x18, 0x85, 0x43, 0x13, // 204
+ 0x18, 0xC8, 0x3B, 0x11, // 205
+ 0x19, 0x03, 0x47, 0x13, // 206
+ 0x19, 0x4A, 0x3B, 0x11, // 207
+ 0x19, 0x85, 0x3A, 0x10, // 208
+ 0x19, 0xBF, 0x3F, 0x11, // 209
+ 0x19, 0xFE, 0x35, 0x0E, // 210
+ 0x1A, 0x33, 0x39, 0x0F, // 211
+ 0x1A, 0x6C, 0x42, 0x12, // 212
+ 0x1A, 0xAE, 0x3B, 0x0F, // 213
+ 0x1A, 0xE9, 0x43, 0x12, // 214
+ 0x1B, 0x2C, 0x37, 0x10, // 215
+ 0x1B, 0x63, 0x4F, 0x16, // 216
+ 0x1B, 0xB2, 0x58, 0x17, // 217
+ 0x1C, 0x0A, 0x47, 0x13, // 218
+ 0x1C, 0x51, 0x4B, 0x15, // 219
+ 0x1C, 0x9C, 0x3B, 0x10, // 220
+ 0x1C, 0xD7, 0x3F, 0x11, // 221
+ 0x1D, 0x16, 0x5B, 0x18, // 222
+ 0x1D, 0x71, 0x3B, 0x11, // 223
+ 0x1D, 0xAC, 0x2F, 0x0D, // 224
+ 0x1D, 0xDB, 0x33, 0x0E, // 225
+ 0x1E, 0x0E, 0x2F, 0x0D, // 226
+ 0x1E, 0x3D, 0x22, 0x09, // 227
+ 0x1E, 0x5F, 0x33, 0x0E, // 228
+ 0x1E, 0x92, 0x2F, 0x0D, // 229
+ 0x1E, 0xC1, 0x3F, 0x10, // 230
+ 0x1F, 0x00, 0x27, 0x0B, // 231
+ 0x1F, 0x27, 0x2F, 0x0D, // 232
+ 0x1F, 0x56, 0x2F, 0x0D, // 233
+ 0x1F, 0x85, 0x27, 0x0B, // 234
+ 0x1F, 0xAC, 0x2F, 0x0E, // 235
+ 0x1F, 0xDB, 0x3B, 0x11, // 236
+ 0x20, 0x16, 0x2F, 0x0D, // 237
+ 0x20, 0x45, 0x2F, 0x0D, // 238
+ 0x20, 0x74, 0x2B, 0x0D, // 239
+ 0x20, 0x9F, 0x33, 0x0E, // 240
+ 0x20, 0xD2, 0x2B, 0x0C, // 241
+ 0x20, 0xFD, 0x2A, 0x0B, // 242
+ 0x21, 0x27, 0x2A, 0x0C, // 243
+ 0x21, 0x51, 0x4B, 0x14, // 244
+ 0x21, 0x9C, 0x2B, 0x0B, // 245
+ 0x21, 0xC7, 0x33, 0x0E, // 246
+ 0x21, 0xFA, 0x2B, 0x0D, // 247
+ 0x22, 0x25, 0x47, 0x13, // 248
+ 0x22, 0x6C, 0x4B, 0x14, // 249
+ 0x22, 0xB7, 0x37, 0x0F, // 250
+ 0x22, 0xEE, 0x3B, 0x11, // 251
+ 0x23, 0x29, 0x2F, 0x0D, // 252
+ 0x23, 0x58, 0x2B, 0x0C, // 253
+ 0x23, 0x83, 0x43, 0x12, // 254
+ 0x23, 0xC6, 0x2B, 0x0D, // 255
+ // Font Data:
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33
+ 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0,
+ 0x07, 0x00, 0x00, 0xE0, 0x07, // 34
+ 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0,
+ 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F,
+ 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1,
+ 0x1F, 0x00, 0x80, 0x81, 0x07, // 36
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20,
+ 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0,
+ 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20,
+ 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0,
+ 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03,
+ 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38
+ 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60,
+ 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00,
+ 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0,
+ 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00,
+ 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60,
+ 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44
+ 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00,
+ 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46
+ 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0,
+ 0x03, 0x00, 0x00, 0x60, // 47
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60,
+ 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF,
+ 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
+ 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60,
+ 0x00, 0x33, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F,
+ 0x30, 0x00, 0x00, 0x0F, 0x30, // 50
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7,
+ 0x0F, 0x00, 0x00, 0x80, 0x07, // 51
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00,
+ 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00,
+ 0x03, 0x00, 0x00, 0x00, 0x03, // 52
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60,
+ 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0,
+ 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60,
+ 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1,
+ 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54
+ 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60,
+ 0x80, 0x3F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01,
+ 0x00, 0x00, 0x60, // 55
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7,
+ 0x1F, 0x00, 0x00, 0x80, 0x07, // 56
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60,
+ 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF,
+ 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00,
+ 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06,
+ 0x03, 0x00, 0x00, 0x03, 0x06, // 60
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00,
+ 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C,
+ 0x01, 0x00, 0x00, 0x8C, 0x01, // 61
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00,
+ 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70,
+ 0x00, 0x00, 0x00, 0x20, // 62
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60,
+ 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F,
+ 0x00, 0x00, 0x00, 0x07, // 63
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80,
+ 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06,
+ 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31,
+ 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01,
+ 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64
+ 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80,
+ 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE,
+ 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30,
+ 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0,
+ 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02,
+ 0x03, // 67
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60,
+ 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC,
+ 0x01, // 68
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30,
+ 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60,
+ 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30,
+ 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0,
+ 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60,
+ 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F,
+ 0x00, 0x00, 0xE2, 0x0F, // 71
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30,
+ 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00,
+ 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83,
+ 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x20, // 75
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x30, // 76
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0,
+ 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F,
+ 0x00, 0xE0, 0xFF, 0x3F, // 77
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80,
+ 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80,
+ 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0,
+ 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F,
+ 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60,
+ 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60,
+ 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0,
+ 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F,
+ 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60,
+ 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0,
+ 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00,
+ 0x20, // 82
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60,
+ 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70,
+ 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83
+ 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60,
+ 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00,
+ 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85
+ 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00,
+ 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8,
+ 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86
+ 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03,
+ 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F,
+ 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00,
+ 0xE0, 0x07, 0x00, 0x00, 0x60, // 87
+ 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00,
+ 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83,
+ 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88
+ 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
+ 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E,
+ 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89
+ 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60,
+ 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07,
+ 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90
+ 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91
+ 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00,
+ 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0,
+ 0xFF, 0xFF, 0x07, // 93
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0,
+ 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00,
+ 0x20, // 94
+ 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00,
+ 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00,
+ 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00,
+ 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8,
+ 0x3F, 0x00, 0x00, 0x00, 0x20, // 97
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00,
+ 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C,
+ 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 98
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18,
+ 0x0C, // 99
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF,
+ 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00,
+ 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8,
+ 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101
+ 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60,
+ 0x06, 0x00, 0x00, 0x60, 0x06, // 102
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00,
+ 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE,
+ 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
+ 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105
+ 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00,
+ 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02,
+ 0x30, 0x00, 0x00, 0x00, 0x20, // 107
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00,
+ 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8,
+ 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00,
+ 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
+ 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8,
+ 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00,
+ 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C,
+ 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 112
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE,
+ 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00,
+ 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00,
+ 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18,
+ 0x0F, // 115
+ 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE,
+ 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117
+ 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00,
+ 0x06, // 118
+ 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00,
+ 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0,
+ 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00,
+ 0x00, 0x00, 0x0E, // 119
+ 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00,
+ 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02,
+ 0x20, // 120
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00,
+ 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00,
+ 0x06, // 121
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00,
+ 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06,
+ 0x30, // 122
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60,
+ 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124
+ 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00,
+ 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0,
+ 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00,
+ 0x06, 0x3F, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10,
+ 0x06, // 162
+ 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60,
+ 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01,
+ 0x38, 0x00, 0x00, 0x00, 0x10, // 163
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00,
+ 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE,
+ 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164
+ 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00,
+ 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61,
+ 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60,
+ 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1,
+ 0xE7, 0x01, 0x00, 0x80, 0x03, // 167
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C,
+ 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30,
+ 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168
+ 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0,
+ 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03,
+ 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07,
+ 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169
+ 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0,
+ 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00,
+ 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00,
+ 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC,
+ 0x01, 0x00, 0x00, 0xFC, 0x01, // 172
+ 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00,
+ 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173
+ 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0,
+ 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE,
+ 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07,
+ 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174
+ 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C,
+ 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00,
+ 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20,
+ 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00,
+ 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60,
+ 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177
+ 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0,
+ 0x23, 0x00, 0x00, 0xC0, 0x21, // 178
+ 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0,
+ 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60,
+ 0x00, 0x00, 0x00, 0x20, // 180
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE,
+ 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181
+ 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0,
+ 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF,
+ 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00,
+ 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8,
+ 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0,
+ 0x3F, // 185
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0,
+ 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00,
+ 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0,
+ 0x03, 0x00, 0x00, 0x80, // 187
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0,
+ 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78,
+ 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B,
+ 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0,
+ 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70,
+ 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C,
+ 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189
+ 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0,
+ 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70,
+ 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09,
+ 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00,
+ 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191
+ 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80,
+ 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE,
+ 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30,
+ 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30,
+ 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60,
+ 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00,
+ 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 195
+ 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0,
+ 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60,
+ 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30,
+ 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197
+ 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00,
+ 0xCF, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF,
+ 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00,
+ 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00,
+ 0x60, 0x00, 0x20, // 198
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60,
+ 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C,
+ 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, 0x00, 0xC7, 0x0F, // 199
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00,
+ 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E,
+ 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06,
+ 0x00, 0x0F, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E,
+ 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00,
+ 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202
+ 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0,
+ 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00,
+ 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0,
+ 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
+ 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F,
+ 0x00, 0xE0, 0xFF, 0x3F, // 204
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30,
+ 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0,
+ 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F,
+ 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60,
+ 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00,
+ 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60,
+ 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60,
+ 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 208
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0,
+ 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00,
+ 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02,
+ 0x03, // 209
+ 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60,
+ 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00,
+ 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 210
+ 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00,
+ 0x78, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C,
+ 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x20, // 211
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80,
+ 0x01, 0x06, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01,
+ 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01,
+ 0x00, 0x00, 0xF8, // 212
+ 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00,
+ 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83,
+ 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0,
+ 0x00, 0x00, 0x00, 0xF0, // 214
+ 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00,
+ 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0,
+ 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF,
+ 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30,
+ 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF,
+ 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30,
+ 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01,
+ 0x00, 0x00, 0xF0, 0x01, // 217
+ 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0,
+ 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30,
+ 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C,
+ 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
+ 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30,
+ 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 219
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
+ 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30,
+ 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60,
+ 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30,
+ 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC,
+ 0x03, // 221
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00,
+ 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03,
+ 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30,
+ 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00,
+ 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0,
+ 0xB8, 0x0F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30,
+ 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00,
+ 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8,
+ 0x3F, 0x00, 0x00, 0x00, 0x20, // 224
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60,
+ 0x0C, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78,
+ 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, 0x10, 0xC0, 0x03, // 225
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00,
+ 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78,
+ 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227
+ 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00,
+ 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0xF0, // 228
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00,
+ 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8,
+ 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229
+ 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00,
+ 0xC0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0,
+ 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04,
+ 0x20, // 230
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00,
+ 0x86, 0x20, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, 0x1F, // 231
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00,
+ 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80,
+ 0x00, 0x0F, 0x00, 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00,
+ 0xC0, 0x01, 0x00, 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, 0x30, // 234
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00,
+ 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00,
+ 0x78, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0,
+ 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+ 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8,
+ 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC,
+ 0x3F, // 239
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00,
+ 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C,
+ 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 240
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18,
+ 0x0C, // 241
+ 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00,
+ 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x04, // 242
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00,
+ 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00,
+ 0x06, // 243
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00,
+ 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF,
+ 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30,
+ 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244
+ 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00,
+ 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02,
+ 0x20, // 245
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 246
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
+ 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC,
+ 0x3F, // 247
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30,
+ 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
+ 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC,
+ 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30,
+ 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 249
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00,
+ 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0,
+ 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00,
+ 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80,
+ 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00,
+ 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00,
+ 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00,
+ 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0,
+ 0x07, // 253
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+ 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06,
+ 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F,
+ 0x00, 0x00, 0xE0, 0x07, // 254
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00,
+ 0x84, 0x03, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC,
+ 0x3F, // 255
+};
+
+#endif // OLED_RU
\ No newline at end of file
diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.h b/src/graphics/fonts/OLEDDisplayFontsRU.h
index 7510dcdfc..0437517dd 100644
--- a/src/graphics/fonts/OLEDDisplayFontsRU.h
+++ b/src/graphics/fonts/OLEDDisplayFontsRU.h
@@ -8,4 +8,6 @@
#endif
extern const uint8_t ArialMT_Plain_10_RU[] PROGMEM;
+extern const uint8_t ArialMT_Plain_16_RU[] PROGMEM;
+extern const uint8_t ArialMT_Plain_24_RU[] PROGMEM;
#endif
\ No newline at end of file
diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp
index 2a97526ef..8bc56ea94 100644
--- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp
+++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp
@@ -1,3 +1,5 @@
+#ifdef OLED_UA
+
#include "OLEDDisplayFontsUA.h"
// Font generated or edited with the glyphEditor
@@ -1920,4 +1922,6 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00,
0x18, 0x0E, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8,
0xFF, // 1103
-};
\ No newline at end of file
+};
+
+#endif // OLED_UA
\ No newline at end of file
diff --git a/src/graphics/images.h b/src/graphics/images.h
index beef3a1b2..c66e4b992 100644
--- a/src/graphics/images.h
+++ b/src/graphics/images.h
@@ -27,7 +27,8 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
+ ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff};
const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};
diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp
new file mode 100644
index 000000000..e83588905
--- /dev/null
+++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp
@@ -0,0 +1,68 @@
+#include "./ZJY122250_0213BAAMFGN.h"
+
+#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+
+using namespace NicheGraphics::Drivers;
+
+// Map the display controller IC's output to the connected panel
+void ZJY122250_0213BAAMFGN::configScanning()
+{
+ // "Driver output control"
+ // Scan gates from 0 to 249 (vertical resolution 250px)
+ sendCommand(0x01);
+ sendData(0xF9);
+ sendData(0x00);
+ sendData(0x00);
+}
+
+// Specify which information is used to control the sequence of voltages applied to move the pixels
+// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
+// the controller IC's OTP memory, when the update procedure begins.
+void ZJY122250_0213BAAMFGN::configWaveform()
+{
+ switch (updateType) {
+ case FAST:
+ sendCommand(0x3C); // Border waveform:
+ sendData(0x80); // VCOM
+ break;
+ case FULL:
+ default:
+ sendCommand(0x3C); // Border waveform:
+ sendData(0x01); // Follow LUT 1 (blink same as white pixels)
+ break;
+ }
+
+ sendCommand(0x18); // Temperature sensor:
+ sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
+}
+
+void ZJY122250_0213BAAMFGN::configUpdateSequence()
+{
+ switch (updateType) {
+ case FAST:
+ sendCommand(0x22); // Set "update sequence"
+ sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
+ break;
+
+ case FULL:
+ default:
+ sendCommand(0x22); // Set "update sequence"
+ sendData(0xF7); // Will load LUT from OTP memory
+ break;
+ }
+}
+
+// Once the refresh operation has been started,
+// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
+// Only used when refresh is "async"
+void ZJY122250_0213BAAMFGN::detachFromUpdate()
+{
+ switch (updateType) {
+ case FAST:
+ return beginPolling(50, 500); // At least 500ms for fast refresh
+ case FULL:
+ default:
+ return beginPolling(100, 2000); // At least 2 seconds for full refresh
+ }
+}
+#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
\ No newline at end of file
diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h
new file mode 100644
index 000000000..82c4ec107
--- /dev/null
+++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h
@@ -0,0 +1,42 @@
+/*
+
+E-Ink display driver
+ - ZJY122250_0213BAAMFGN
+ - Manufacturer: Zhongjingyuan
+ - Size: 2.13 inch
+ - Resolution: 250px x 122px
+ - Flex connector marking (not a unique identifier): FPC-A002
+
+*/
+
+#pragma once
+
+#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+
+#include "configuration.h"
+
+#include "./SSD16XX.h"
+
+namespace NicheGraphics::Drivers
+{
+class ZJY122250_0213BAAMFGN : public SSD16XX
+{
+ // Display properties
+ private:
+ static constexpr uint32_t width = 122;
+ static constexpr uint32_t height = 250;
+ static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
+
+ public:
+ ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {}
+
+ protected:
+ virtual void configScanning() override;
+ virtual void configWaveform() override;
+ virtual void configUpdateSequence() override;
+ void detachFromUpdate() override;
+};
+
+} // namespace NicheGraphics::Drivers
+
+#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
index a1f79a28f..7876276a8 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
@@ -223,7 +223,7 @@ void InkHUD::MenuApplet::execute(MenuItem item)
case SHUTDOWN:
LOG_INFO("Shutting down from menu");
- power->shutdown();
+ shutdownAtMsec = millis();
// Menu is then sent to background via onShutdown
break;
diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp
index 7e1accafd..e8849b72e 100644
--- a/src/graphics/niche/InkHUD/DisplayHealth.cpp
+++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp
@@ -7,12 +7,7 @@ using namespace NicheGraphics;
// Timing for "maintenance"
// Paying off full-refresh debt with unprovoked updates, if the display is not very active
-
-#ifdef SEEED_WIO_TRACKER_L1
-static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL;
-#else
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL;
-#endif
static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL;
InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator")
diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini
index e5a0e67df..80984f399 100644
--- a/src/graphics/niche/InkHUD/PlatformioConfig.ini
+++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini
@@ -1,7 +1,6 @@
[inkhud]
build_src_filter =
+; Include the nicheGraphics directory
- +<../variants/$PIOENV>; Include nicheGraphics.h from our variant folder
build_flags =
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI)
diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp
index ad667f003..32882f7ae 100644
--- a/src/input/ButtonThread.cpp
+++ b/src/input/ButtonThread.cpp
@@ -53,23 +53,21 @@ bool ButtonThread::initButton(const ButtonConfig &config)
},
this);
- if (config.longPress != INPUT_BROKER_NONE) {
- _longPress = config.longPress;
- userButton.attachLongPressStart(
- [](void *callerThread) -> void {
- ButtonThread *thread = (ButtonThread *)callerThread;
- // if (millis() > 30000) // hold off 30s after boot
- thread->btnEvent = BUTTON_EVENT_LONG_PRESSED;
- },
- this);
- userButton.attachLongPressStop(
- [](void *callerThread) -> void {
- ButtonThread *thread = (ButtonThread *)callerThread;
- // if (millis() > 30000) // hold off 30s after boot
- thread->btnEvent = BUTTON_EVENT_LONG_RELEASED;
- },
- this);
- }
+ _longPress = config.longPress;
+ userButton.attachLongPressStart(
+ [](void *callerThread) -> void {
+ ButtonThread *thread = (ButtonThread *)callerThread;
+ // if (millis() > 30000) // hold off 30s after boot
+ thread->btnEvent = BUTTON_EVENT_LONG_PRESSED;
+ },
+ this);
+ userButton.attachLongPressStop(
+ [](void *callerThread) -> void {
+ ButtonThread *thread = (ButtonThread *)callerThread;
+ // if (millis() > 30000) // hold off 30s after boot
+ thread->btnEvent = BUTTON_EVENT_LONG_RELEASED;
+ },
+ this);
if (config.doublePress != INPUT_BROKER_NONE) {
_doublePress = config.doublePress;
@@ -94,8 +92,11 @@ bool ButtonThread::initButton(const ButtonConfig &config)
if (config.shortLong != INPUT_BROKER_NONE) {
_shortLong = config.shortLong;
}
-
+#ifdef USE_EINK
+ userButton.setDebounceMs(0);
+#else
userButton.setDebounceMs(1);
+#endif
userButton.setPressMs(_longPressTime);
if (screen) {
@@ -139,8 +140,7 @@ int32_t ButtonThread::runOnce()
}
// Progressive lead-up sound system
- if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS &&
- (millis() - buttonPressStartTime) < _longLongPressTime) {
+ if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
// Start the progressive sequence if not already active
if (!leadUpSequenceActive) {
@@ -152,13 +152,14 @@ int32_t ButtonThread::runOnce()
else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes
if (playNextLeadUpNote()) {
lastLeadUpNoteTime = millis();
+ } else {
+ leadUpPlayed = true;
}
}
}
// Reset when button is released
if (!buttonCurrentlyPressed && buttonWasPressed) {
- leadUpPlayed = false;
leadUpSequenceActive = false;
resetLeadUpSequence();
}
@@ -202,11 +203,11 @@ int32_t ButtonThread::runOnce()
break;
}
-
- // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event)
- evt.inputEvent = _longPress;
- this->notifyObservers(&evt);
-
+ if (_longPress != INPUT_BROKER_NONE) {
+ // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event)
+ evt.inputEvent = _longPress;
+ this->notifyObservers(&evt);
+ }
// Reset combination tracking
waitingForLongPress = false;
@@ -253,14 +254,15 @@ int32_t ButtonThread::runOnce()
// may wake the board immediatedly.
case BUTTON_EVENT_LONG_RELEASED: {
- LOG_INFO("LONG PRESS RELEASE");
+ LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime);
if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE &&
- (millis() - buttonPressStartTime) >= _longLongPressTime) {
+ (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) {
evt.inputEvent = _longLongPress;
this->notifyObservers(&evt);
}
// Reset combination tracking
waitingForLongPress = false;
+ leadUpPlayed = false;
break;
}
diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h
index 949048de1..c6d6557e2 100644
--- a/src/input/ButtonThread.h
+++ b/src/input/ButtonThread.h
@@ -18,13 +18,13 @@ struct ButtonConfig {
uint16_t longPressTime = 500;
input_broker_event doublePress = INPUT_BROKER_NONE;
input_broker_event longLongPress = INPUT_BROKER_NONE;
- uint16_t longLongPressTime = 5000;
+ uint16_t longLongPressTime = 3900;
input_broker_event triplePress = INPUT_BROKER_NONE;
input_broker_event shortLong = INPUT_BROKER_NONE;
bool touchQuirk = false;
// Constructor to set required parameter
- ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {}
+ explicit ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {}
};
#ifndef BUTTON_CLICK_MS
@@ -62,7 +62,7 @@ class ButtonThread : public Observable, public concurrency::
BUTTON_EVENT_COMBO_SHORT_LONG,
};
- ButtonThread(const char *name);
+ explicit ButtonThread(const char *name);
int32_t runOnce() override;
OneButton userButton;
void attachButtonInterrupts();
@@ -92,7 +92,7 @@ class ButtonThread : public Observable, public concurrency::
voidFuncPtr _intRoutine = nullptr;
uint16_t _longPressTime = 500;
- uint16_t _longLongPressTime = 5000;
+ uint16_t _longLongPressTime = 3900;
int _pinNum = 0;
bool _activeLow = true;
bool _touchQuirk = false;
diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp
index 53bcedc63..776b9001d 100644
--- a/src/input/ExpressLRSFiveWay.cpp
+++ b/src/input/ExpressLRSFiveWay.cpp
@@ -199,7 +199,7 @@ void ExpressLRSFiveWay::sendKey(input_broker_event key)
void ExpressLRSFiveWay::toggleGPS()
{
#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS
- if (!config.device.disable_triple_click && (gps != nullptr)) {
+ if (gps != nullptr) {
gps->toggleGpsMode();
screen->startAlert("GPS Toggled");
alerting = true;
@@ -233,14 +233,7 @@ void ExpressLRSFiveWay::sendAdhocPing()
// Contained as one method for easier remapping of buttons by user
void ExpressLRSFiveWay::shutdown()
{
- LOG_INFO("Shutdown from long press");
- powerFSM.trigger(EVENT_PRESS);
- screen->startAlert("Shutting Down...");
- // Don't set alerting = true. We don't want to auto-dismiss this alert.
-
- playShutdownMelody(); // In case user adds a buzzer
-
- shutdownAtMsec = millis() + 3000;
+ sendKey(INPUT_BROKER_SHUTDOWN);
}
void ExpressLRSFiveWay::click()
diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp
new file mode 100644
index 000000000..d3fcbbf9d
--- /dev/null
+++ b/src/input/RotaryEncoderImpl.cpp
@@ -0,0 +1,76 @@
+#ifdef T_LORA_PAGER
+
+#include "RotaryEncoderImpl.h"
+#include "InputBroker.h"
+#include "RotaryEncoder.h"
+
+#define ORIGIN_NAME "RotaryEncoder"
+
+RotaryEncoderImpl *rotaryEncoderImpl;
+
+RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME)
+{
+ rotary = nullptr;
+}
+
+bool RotaryEncoderImpl::init()
+{
+ if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 ||
+ moduleConfig.canned_message.inputbroker_pin_b == 0) {
+ // Input device is disabled.
+ disable();
+ return false;
+ }
+
+ eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw);
+ eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw);
+ eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press);
+
+ rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
+ moduleConfig.canned_message.inputbroker_pin_press);
+ rotary->resetButton();
+
+ inputBroker->registerSource(this);
+
+ LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
+ moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
+ eventPressed);
+ return true;
+}
+
+int32_t RotaryEncoderImpl::runOnce()
+{
+ InputEvent e;
+ e.inputEvent = INPUT_BROKER_NONE;
+ e.source = this->originName;
+
+ static uint32_t lastPressed = millis();
+ if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
+ if (lastPressed + 200 < millis()) {
+ LOG_DEBUG("Rotary event Press");
+ lastPressed = millis();
+ e.inputEvent = this->eventPressed;
+ }
+ } else {
+ switch (rotary->process()) {
+ case RotaryEncoder::DIRECTION_CW:
+ LOG_DEBUG("Rotary event CW");
+ e.inputEvent = this->eventCw;
+ break;
+ case RotaryEncoder::DIRECTION_CCW:
+ LOG_DEBUG("Rotary event CCW");
+ e.inputEvent = this->eventCcw;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (e.inputEvent != INPUT_BROKER_NONE) {
+ this->notifyObservers(&e);
+ }
+
+ return 20;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h
new file mode 100644
index 000000000..ae2a7c6fd
--- /dev/null
+++ b/src/input/RotaryEncoderImpl.h
@@ -0,0 +1,28 @@
+#pragma once
+
+// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library)
+
+#include "InputBroker.h"
+#include "concurrency/OSThread.h"
+#include "mesh/NodeDB.h"
+
+class RotaryEncoder;
+
+class RotaryEncoderImpl : public Observable, public concurrency::OSThread
+{
+ public:
+ RotaryEncoderImpl();
+ bool init(void);
+
+ protected:
+ virtual int32_t runOnce() override;
+
+ input_broker_event eventCw = INPUT_BROKER_NONE;
+ input_broker_event eventCcw = INPUT_BROKER_NONE;
+ input_broker_event eventPressed = INPUT_BROKER_NONE;
+
+ RotaryEncoder *rotary;
+ const char *originName;
+};
+
+extern RotaryEncoderImpl *rotaryEncoderImpl;
diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp
index 0557bc180..88b07a389 100644
--- a/src/input/RotaryEncoderInterruptBase.cpp
+++ b/src/input/RotaryEncoderInterruptBase.cpp
@@ -18,14 +18,23 @@ void RotaryEncoderInterruptBase::init(
this->_eventCcw = eventCcw;
this->_eventPressed = eventPressed;
- pinMode(pinPress, INPUT_PULLUP);
- pinMode(this->_pinA, INPUT_PULLUP);
- pinMode(this->_pinB, INPUT_PULLUP);
+ bool isRAK = false;
+#ifdef RAK_4631
+ isRAK = true;
+#endif
- // attachInterrupt(pinPress, onIntPress, RISING);
- attachInterrupt(pinPress, onIntPress, RISING);
- attachInterrupt(this->_pinA, onIntA, CHANGE);
- attachInterrupt(this->_pinB, onIntB, CHANGE);
+ if (!isRAK || pinPress != 0) {
+ pinMode(pinPress, INPUT_PULLUP);
+ attachInterrupt(pinPress, onIntPress, RISING);
+ }
+ if (!isRAK || this->_pinA != 0) {
+ pinMode(this->_pinA, INPUT_PULLUP);
+ attachInterrupt(this->_pinA, onIntA, CHANGE);
+ }
+ if (!isRAK || this->_pinA != 0) {
+ pinMode(this->_pinB, INPUT_PULLUP);
+ attachInterrupt(this->_pinB, onIntB, CHANGE);
+ }
this->rotaryLevelA = digitalRead(this->_pinA);
this->rotaryLevelB = digitalRead(this->_pinB);
diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp
index d99379b23..bd8338acf 100644
--- a/src/input/TCA8418Keyboard.cpp
+++ b/src/input/TCA8418Keyboard.cpp
@@ -1,116 +1,18 @@
-// Based on the MPR121 Keyboard and Adafruit TCA8418 library
-
#include "TCA8418Keyboard.h"
-#include "configuration.h"
-
-#include
-
-// REGISTERS
-// #define _TCA8418_REG_RESERVED 0x00
-#define _TCA8418_REG_CFG 0x01 // Configuration register
-#define _TCA8418_REG_INT_STAT 0x02 // Interrupt status
-#define _TCA8418_REG_KEY_LCK_EC 0x03 // Key lock and event counter
-#define _TCA8418_REG_KEY_EVENT_A 0x04 // Key event register A
-#define _TCA8418_REG_KEY_EVENT_B 0x05 // Key event register B
-#define _TCA8418_REG_KEY_EVENT_C 0x06 // Key event register C
-#define _TCA8418_REG_KEY_EVENT_D 0x07 // Key event register D
-#define _TCA8418_REG_KEY_EVENT_E 0x08 // Key event register E
-#define _TCA8418_REG_KEY_EVENT_F 0x09 // Key event register F
-#define _TCA8418_REG_KEY_EVENT_G 0x0A // Key event register G
-#define _TCA8418_REG_KEY_EVENT_H 0x0B // Key event register H
-#define _TCA8418_REG_KEY_EVENT_I 0x0C // Key event register I
-#define _TCA8418_REG_KEY_EVENT_J 0x0D // Key event register J
-#define _TCA8418_REG_KP_LCK_TIMER 0x0E // Keypad lock1 to lock2 timer
-#define _TCA8418_REG_UNLOCK_1 0x0F // Unlock register 1
-#define _TCA8418_REG_UNLOCK_2 0x10 // Unlock register 2
-#define _TCA8418_REG_GPIO_INT_STAT_1 0x11 // GPIO interrupt status 1
-#define _TCA8418_REG_GPIO_INT_STAT_2 0x12 // GPIO interrupt status 2
-#define _TCA8418_REG_GPIO_INT_STAT_3 0x13 // GPIO interrupt status 3
-#define _TCA8418_REG_GPIO_DAT_STAT_1 0x14 // GPIO data status 1
-#define _TCA8418_REG_GPIO_DAT_STAT_2 0x15 // GPIO data status 2
-#define _TCA8418_REG_GPIO_DAT_STAT_3 0x16 // GPIO data status 3
-#define _TCA8418_REG_GPIO_DAT_OUT_1 0x17 // GPIO data out 1
-#define _TCA8418_REG_GPIO_DAT_OUT_2 0x18 // GPIO data out 2
-#define _TCA8418_REG_GPIO_DAT_OUT_3 0x19 // GPIO data out 3
-#define _TCA8418_REG_GPIO_INT_EN_1 0x1A // GPIO interrupt enable 1
-#define _TCA8418_REG_GPIO_INT_EN_2 0x1B // GPIO interrupt enable 2
-#define _TCA8418_REG_GPIO_INT_EN_3 0x1C // GPIO interrupt enable 3
-#define _TCA8418_REG_KP_GPIO_1 0x1D // Keypad/GPIO select 1
-#define _TCA8418_REG_KP_GPIO_2 0x1E // Keypad/GPIO select 2
-#define _TCA8418_REG_KP_GPIO_3 0x1F // Keypad/GPIO select 3
-#define _TCA8418_REG_GPI_EM_1 0x20 // GPI event mode 1
-#define _TCA8418_REG_GPI_EM_2 0x21 // GPI event mode 2
-#define _TCA8418_REG_GPI_EM_3 0x22 // GPI event mode 3
-#define _TCA8418_REG_GPIO_DIR_1 0x23 // GPIO data direction 1
-#define _TCA8418_REG_GPIO_DIR_2 0x24 // GPIO data direction 2
-#define _TCA8418_REG_GPIO_DIR_3 0x25 // GPIO data direction 3
-#define _TCA8418_REG_GPIO_INT_LVL_1 0x26 // GPIO edge/level detect 1
-#define _TCA8418_REG_GPIO_INT_LVL_2 0x27 // GPIO edge/level detect 2
-#define _TCA8418_REG_GPIO_INT_LVL_3 0x28 // GPIO edge/level detect 3
-#define _TCA8418_REG_DEBOUNCE_DIS_1 0x29 // Debounce disable 1
-#define _TCA8418_REG_DEBOUNCE_DIS_2 0x2A // Debounce disable 2
-#define _TCA8418_REG_DEBOUNCE_DIS_3 0x2B // Debounce disable 3
-#define _TCA8418_REG_GPIO_PULL_1 0x2C // GPIO pull-up disable 1
-#define _TCA8418_REG_GPIO_PULL_2 0x2D // GPIO pull-up disable 2
-#define _TCA8418_REG_GPIO_PULL_3 0x2E // GPIO pull-up disable 3
-// #define _TCA8418_REG_RESERVED 0x2F
-
-// FIELDS CONFIG REGISTER 1
-#define _TCA8418_REG_CFG_AI 0x80 // Auto-increment for read/write
-#define _TCA8418_REG_CFG_GPI_E_CGF 0x40 // Event mode config
-#define _TCA8418_REG_CFG_OVR_FLOW_M 0x20 // Overflow mode enable
-#define _TCA8418_REG_CFG_INT_CFG 0x10 // Interrupt config
-#define _TCA8418_REG_CFG_OVR_FLOW_IEN 0x08 // Overflow interrupt enable
-#define _TCA8418_REG_CFG_K_LCK_IEN 0x04 // Keypad lock interrupt enable
-#define _TCA8418_REG_CFG_GPI_IEN 0x02 // GPI interrupt enable
-#define _TCA8418_REG_CFG_KE_IEN 0x01 // Key events interrupt enable
-
-// FIELDS INT_STAT REGISTER 2
-#define _TCA8418_REG_STAT_CAD_INT 0x10 // Ctrl-alt-del seq status
-#define _TCA8418_REG_STAT_OVR_FLOW_INT 0x08 // Overflow interrupt status
-#define _TCA8418_REG_STAT_K_LCK_INT 0x04 // Key lock interrupt status
-#define _TCA8418_REG_STAT_GPI_INT 0x02 // GPI interrupt status
-#define _TCA8418_REG_STAT_K_INT 0x01 // Key events interrupt status
-
-// FIELDS KEY_LCK_EC REGISTER 3
-#define _TCA8418_REG_LCK_EC_K_LCK_EN 0x40 // Key lock enable
-#define _TCA8418_REG_LCK_EC_LCK_2 0x20 // Keypad lock status 2
-#define _TCA8418_REG_LCK_EC_LCK_1 0x10 // Keypad lock status 1
-#define _TCA8418_REG_LCK_EC_KLEC_3 0x08 // Key event count bit 3
-#define _TCA8418_REG_LCK_EC_KLEC_2 0x04 // Key event count bit 2
-#define _TCA8418_REG_LCK_EC_KLEC_1 0x02 // Key event count bit 1
-#define _TCA8418_REG_LCK_EC_KLEC_0 0x01 // Key event count bit 0
-
-// Pin IDs for matrix rows/columns
-enum {
- _TCA8418_ROW0, // Pin ID for row 0
- _TCA8418_ROW1, // Pin ID for row 1
- _TCA8418_ROW2, // Pin ID for row 2
- _TCA8418_ROW3, // Pin ID for row 3
- _TCA8418_ROW4, // Pin ID for row 4
- _TCA8418_ROW5, // Pin ID for row 5
- _TCA8418_ROW6, // Pin ID for row 6
- _TCA8418_ROW7, // Pin ID for row 7
- _TCA8418_COL0, // Pin ID for column 0
- _TCA8418_COL1, // Pin ID for column 1
- _TCA8418_COL2, // Pin ID for column 2
- _TCA8418_COL3, // Pin ID for column 3
- _TCA8418_COL4, // Pin ID for column 4
- _TCA8418_COL5, // Pin ID for column 5
- _TCA8418_COL6, // Pin ID for column 6
- _TCA8418_COL7, // Pin ID for column 7
- _TCA8418_COL8, // Pin ID for column 8
- _TCA8418_COL9 // Pin ID for column 9
-};
#define _TCA8418_COLS 3
#define _TCA8418_ROWS 4
#define _TCA8418_NUM_KEYS 12
-uint8_t TCA8418TapMod[_TCA8418_NUM_KEYS] = {13, 7, 7, 7, 7, 7,
- 9, 7, 9, 2, 2, 2}; // Num chars per key, Modulus for rotating through characters
+#define _TCA8418_LONG_PRESS_THRESHOLD 2000
+#define _TCA8418_MULTI_TAP_THRESHOLD 750
-unsigned char TCA8418TapMap[_TCA8418_NUM_KEYS][13] = {
+using Key = TCA8418KeyboardBase::TCA8418Key;
+
+// Num chars per key, Modulus for rotating through characters
+static uint8_t TCA8418TapMod[_TCA8418_NUM_KEYS] = {13, 7, 7, 7, 7, 7, 9, 7, 9, 2, 2, 2};
+
+static unsigned char TCA8418TapMap[_TCA8418_NUM_KEYS][13] = {
{'1', '.', ',', '?', '!', ':', ';', '-', '_', '\\', '/', '(', ')'}, // 1
{'2', 'a', 'b', 'c', 'A', 'B', 'C'}, // 2
{'3', 'd', 'e', 'f', 'D', 'E', 'F'}, // 3
@@ -125,176 +27,35 @@ unsigned char TCA8418TapMap[_TCA8418_NUM_KEYS][13] = {
{'#', '@'}, // #
};
-unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
- _TCA8418_ESC, // 1
- _TCA8418_UP, // 2
- _TCA8418_NONE, // 3
- _TCA8418_LEFT, // 4
- _TCA8418_NONE, // 5
- _TCA8418_RIGHT, // 6
- _TCA8418_NONE, // 7
- _TCA8418_DOWN, // 8
- _TCA8418_NONE, // 9
- _TCA8418_BSP, // *
- _TCA8418_NONE, // 0
- _TCA8418_NONE, // #
+static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
+ Key::ESC, // 1
+ Key::UP, // 2
+ Key::NONE, // 3
+ Key::LEFT, // 4
+ Key::NONE, // 5
+ Key::RIGHT, // 6
+ Key::NONE, // 7
+ Key::DOWN, // 8
+ Key::NONE, // 9
+ Key::BSP, // *
+ Key::NONE, // 0
+ Key::NONE, // #
};
-#define _TCA8418_LONG_PRESS_THRESHOLD 2000
-#define _TCA8418_MULTI_TAP_THRESHOLD 750
-
-TCA8418Keyboard::TCA8418Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr)
+TCA8418Keyboard::TCA8418Keyboard()
+ : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0),
+ should_backspace(false)
{
- state = Init;
- last_key = -1;
- should_backspace = false;
- last_tap = 0L;
- char_idx = 0;
- tap_interval = 0;
- backlight_on = true;
- queue = "";
-}
-
-void TCA8418Keyboard::begin(uint8_t addr, TwoWire *wire)
-{
- m_addr = addr;
- m_wire = wire;
-
- m_wire->begin();
-
- reset();
-}
-
-void TCA8418Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr)
-{
- m_addr = addr;
- m_wire = nullptr;
- writeCallback = w;
- readCallback = r;
- reset();
}
void TCA8418Keyboard::reset()
{
- LOG_DEBUG("TCA8418 Reset");
- // GPIO
- // set default all GIO pins to INPUT
- writeRegister(_TCA8418_REG_GPIO_DIR_1, 0x00);
- writeRegister(_TCA8418_REG_GPIO_DIR_2, 0x00);
+ TCA8418KeyboardBase::reset();
+
// Set COL9 as GPIO output
- writeRegister(_TCA8418_REG_GPIO_DIR_3, 0x02);
+ writeRegister(TCA8418_REG_GPIO_DIR_3, 0x02);
// Switch off keyboard backlight (COL9 = LOW)
- writeRegister(_TCA8418_REG_GPIO_DAT_OUT_3, 0x00);
-
- // add all pins to key events
- writeRegister(_TCA8418_REG_GPI_EM_1, 0xFF);
- writeRegister(_TCA8418_REG_GPI_EM_2, 0xFF);
- writeRegister(_TCA8418_REG_GPI_EM_3, 0xFF);
-
- // set all pins to FALLING interrupts
- writeRegister(_TCA8418_REG_GPIO_INT_LVL_1, 0x00);
- writeRegister(_TCA8418_REG_GPIO_INT_LVL_2, 0x00);
- writeRegister(_TCA8418_REG_GPIO_INT_LVL_3, 0x00);
-
- // add all pins to interrupts
- writeRegister(_TCA8418_REG_GPIO_INT_EN_1, 0xFF);
- writeRegister(_TCA8418_REG_GPIO_INT_EN_2, 0xFF);
- writeRegister(_TCA8418_REG_GPIO_INT_EN_3, 0xFF);
-
- // Set keyboard matrix size
- matrix(_TCA8418_ROWS, _TCA8418_COLS);
- enableDebounce();
- flush();
- state = Idle;
-}
-
-bool TCA8418Keyboard::matrix(uint8_t rows, uint8_t columns)
-{
- if ((rows > 8) || (columns > 10))
- return false;
-
- // Skip zero size matrix
- if ((rows != 0) && (columns != 0)) {
- // Setup the keypad matrix.
- uint8_t mask = 0x00;
- for (int r = 0; r < rows; r++) {
- mask <<= 1;
- mask |= 1;
- }
- writeRegister(_TCA8418_REG_KP_GPIO_1, mask);
-
- mask = 0x00;
- for (int c = 0; c < columns && c < 8; c++) {
- mask <<= 1;
- mask |= 1;
- }
- writeRegister(_TCA8418_REG_KP_GPIO_2, mask);
-
- if (columns > 8) {
- if (columns == 9)
- mask = 0x01;
- else
- mask = 0x03;
- writeRegister(_TCA8418_REG_KP_GPIO_3, mask);
- }
- }
-
- return true;
-}
-
-uint8_t TCA8418Keyboard::keyCount() const
-{
- uint8_t eventCount = readRegister(_TCA8418_REG_KEY_LCK_EC);
- eventCount &= 0x0F; // lower 4 bits only
- return eventCount;
-}
-
-bool TCA8418Keyboard::hasEvent()
-{
- return queue.length() > 0;
-}
-
-void TCA8418Keyboard::queueEvent(char next)
-{
- if (next == _TCA8418_NONE) {
- return;
- }
- queue.concat(next);
-}
-
-char TCA8418Keyboard::dequeueEvent()
-{
- if (queue.length() < 1) {
- return _TCA8418_NONE;
- }
- char next = queue.charAt(0);
- queue.remove(0, 1);
- return next;
-}
-
-void TCA8418Keyboard::trigger()
-{
- if (keyCount() == 0) {
- return;
- }
- if (state != Init) {
- // Read the key register
- uint8_t k = readRegister(_TCA8418_REG_KEY_EVENT_A);
- uint8_t key = k & 0x7F;
- if (k & 0x80) {
- if (state == Idle)
- pressed(key);
- return;
- } else {
- if (state == Held) {
- released();
- }
- state = Idle;
- return;
- }
- } else {
- reset();
- }
+ writeRegister(TCA8418_REG_GPIO_DAT_OUT_3, 0x00);
}
void TCA8418Keyboard::pressed(uint8_t key)
@@ -354,7 +115,7 @@ void TCA8418Keyboard::released()
int32_t held_interval = now - last_tap;
last_tap = now;
if (tap_interval < _TCA8418_MULTI_TAP_THRESHOLD && should_backspace) {
- queueEvent(_TCA8418_BSP);
+ queueEvent(BSP);
}
if (held_interval > _TCA8418_LONG_PRESS_THRESHOLD) {
queueEvent(TCA8418LongPressMap[last_key]);
@@ -366,195 +127,11 @@ void TCA8418Keyboard::released()
}
}
-uint8_t TCA8418Keyboard::flush()
-{
- // Flush key events
- uint8_t count = 0;
- while (readRegister(_TCA8418_REG_KEY_EVENT_A) != 0)
- count++;
- // Flush gpio events
- readRegister(_TCA8418_REG_GPIO_INT_STAT_1);
- readRegister(_TCA8418_REG_GPIO_INT_STAT_2);
- readRegister(_TCA8418_REG_GPIO_INT_STAT_3);
- // Clear INT_STAT register
- writeRegister(_TCA8418_REG_INT_STAT, 3);
- return count;
-}
-
-uint8_t TCA8418Keyboard::digitalRead(uint8_t pinnum) const
-{
- if (pinnum > _TCA8418_COL9)
- return 0xFF;
-
- uint8_t reg = _TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8;
- uint8_t mask = (1 << (pinnum % 8));
-
- // Level 0 = low other = high
- uint8_t value = readRegister(reg);
- if (value & mask)
- return HIGH;
- return LOW;
-}
-
-bool TCA8418Keyboard::digitalWrite(uint8_t pinnum, uint8_t level)
-{
- if (pinnum > _TCA8418_COL9)
- return false;
-
- uint8_t reg = _TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8;
- uint8_t mask = (1 << (pinnum % 8));
-
- // Level 0 = low other = high
- uint8_t value = readRegister(reg);
- if (level == LOW)
- value &= ~mask;
- else
- value |= mask;
- writeRegister(reg, value);
- return true;
-}
-
-bool TCA8418Keyboard::pinMode(uint8_t pinnum, uint8_t mode)
-{
- if (pinnum > _TCA8418_COL9)
- return false;
-
- uint8_t idx = pinnum / 8;
- uint8_t reg = _TCA8418_REG_GPIO_DIR_1 + idx;
- uint8_t mask = (1 << (pinnum % 8));
-
- // Mode 0 = input 1 = output
- uint8_t value = readRegister(reg);
- if (mode == OUTPUT)
- value |= mask;
- else
- value &= ~mask;
- writeRegister(reg, value);
-
- // Pullup 0 = enabled 1 = disabled
- reg = _TCA8418_REG_GPIO_PULL_1 + idx;
- value = readRegister(reg);
- if (mode == INPUT_PULLUP)
- value &= ~mask;
- else
- value |= mask;
- writeRegister(reg, value);
-
- return true;
-}
-
-bool TCA8418Keyboard::pinIRQMode(uint8_t pinnum, uint8_t mode)
-{
- if (pinnum > _TCA8418_COL9)
- return false;
- if ((mode != RISING) && (mode != FALLING))
- return false;
-
- // Mode 0 = falling 1 = rising
- uint8_t idx = pinnum / 8;
- uint8_t reg = _TCA8418_REG_GPIO_INT_LVL_1 + idx;
- uint8_t mask = (1 << (pinnum % 8));
-
- uint8_t value = readRegister(reg);
- if (mode == RISING)
- value |= mask;
- else
- value &= ~mask;
- writeRegister(reg, value);
-
- // Enable interrupt
- reg = _TCA8418_REG_GPIO_INT_EN_1 + idx;
- value = readRegister(reg);
- value |= mask;
- writeRegister(reg, value);
-
- return true;
-}
-
-void TCA8418Keyboard::enableInterrupts()
-{
- uint8_t value = readRegister(_TCA8418_REG_CFG);
- value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN);
- writeRegister(_TCA8418_REG_CFG, value);
-};
-
-void TCA8418Keyboard::disableInterrupts()
-{
- uint8_t value = readRegister(_TCA8418_REG_CFG);
- value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN);
- writeRegister(_TCA8418_REG_CFG, value);
-};
-
-void TCA8418Keyboard::enableMatrixOverflow()
-{
- uint8_t value = readRegister(_TCA8418_REG_CFG);
- value |= _TCA8418_REG_CFG_OVR_FLOW_M;
- writeRegister(_TCA8418_REG_CFG, value);
-};
-
-void TCA8418Keyboard::disableMatrixOverflow()
-{
- uint8_t value = readRegister(_TCA8418_REG_CFG);
- value &= ~_TCA8418_REG_CFG_OVR_FLOW_M;
- writeRegister(_TCA8418_REG_CFG, value);
-};
-
-void TCA8418Keyboard::enableDebounce()
-{
- writeRegister(_TCA8418_REG_DEBOUNCE_DIS_1, 0x00);
- writeRegister(_TCA8418_REG_DEBOUNCE_DIS_2, 0x00);
- writeRegister(_TCA8418_REG_DEBOUNCE_DIS_3, 0x00);
-}
-
-void TCA8418Keyboard::disableDebounce()
-{
- writeRegister(_TCA8418_REG_DEBOUNCE_DIS_1, 0xFF);
- writeRegister(_TCA8418_REG_DEBOUNCE_DIS_2, 0xFF);
- writeRegister(_TCA8418_REG_DEBOUNCE_DIS_3, 0xFF);
-}
-
void TCA8418Keyboard::setBacklight(bool on)
{
if (on) {
- digitalWrite(_TCA8418_COL9, HIGH);
+ digitalWrite(TCA8418_COL9, HIGH);
} else {
- digitalWrite(_TCA8418_COL9, LOW);
+ digitalWrite(TCA8418_COL9, LOW);
}
}
-
-uint8_t TCA8418Keyboard::readRegister(uint8_t reg) const
-{
- if (m_wire) {
- m_wire->beginTransmission(m_addr);
- m_wire->write(reg);
- m_wire->endTransmission();
-
- m_wire->requestFrom(m_addr, (uint8_t)1);
- if (m_wire->available() < 1)
- return 0;
-
- return m_wire->read();
- }
- if (readCallback) {
- uint8_t data;
- readCallback(m_addr, reg, &data, 1);
- return data;
- }
- return 0;
-}
-
-void TCA8418Keyboard::writeRegister(uint8_t reg, uint8_t value)
-{
- uint8_t data[2];
- data[0] = reg;
- data[1] = value;
-
- if (m_wire) {
- m_wire->beginTransmission(m_addr);
- m_wire->write(data, sizeof(uint8_t) * 2);
- m_wire->endTransmission();
- }
- if (writeCallback) {
- writeCallback(m_addr, data[0], &(data[1]), 1);
- }
-}
\ No newline at end of file
diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h
index 5c53452a4..b76916643 100644
--- a/src/input/TCA8418Keyboard.h
+++ b/src/input/TCA8418Keyboard.h
@@ -1,82 +1,23 @@
-// Based on the MPR121 Keyboard and Adafruit TCA8418 library
-#include "configuration.h"
-#include
+#include "TCA8418KeyboardBase.h"
-#define _TCA8418_NONE 0x00
-#define _TCA8418_REBOOT 0x90
-#define _TCA8418_LEFT 0xb4
-#define _TCA8418_UP 0xb5
-#define _TCA8418_DOWN 0xb6
-#define _TCA8418_RIGHT 0xb7
-#define _TCA8418_ESC 0x1b
-#define _TCA8418_BSP 0x08
-#define _TCA8418_SELECT 0x0d
-
-class TCA8418Keyboard
+/**
+ * @brief 3x4 keypad with 3 columns and 4 rows
+ */
+class TCA8418Keyboard : public TCA8418KeyboardBase
{
public:
- typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
+ TCA8418Keyboard();
+ void reset(void) override;
+ void setBacklight(bool on) override;
- enum KeyState { Init = 0, Idle, Held, Busy };
+ protected:
+ void pressed(uint8_t key) override;
+ void released(void) override;
- KeyState state;
int8_t last_key;
- bool should_backspace;
+ int8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
- bool backlight_on;
-
- String queue;
-
- TCA8418Keyboard();
-
- void begin(uint8_t addr = XPOWERS_AXP192_AXP2101_ADDRESS, TwoWire *wire = &Wire);
- void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = XPOWERS_AXP192_AXP2101_ADDRESS);
-
- void reset(void);
- // Configure the size of the keypad.
- // All other rows and columns are set as inputs.
- bool matrix(uint8_t rows, uint8_t columns);
-
- // Flush all events in the FIFO buffer + GPIO events.
- uint8_t flush(void);
-
- // Key events available in the internal FIFO buffer.
- uint8_t keyCount(void) const;
-
- void trigger(void);
- void pressed(uint8_t key);
- void released(void);
- bool hasEvent(void);
- char dequeueEvent(void);
- void queueEvent(char);
-
- uint8_t digitalRead(uint8_t pinnum) const;
- bool digitalWrite(uint8_t pinnum, uint8_t level);
- bool pinMode(uint8_t pinnum, uint8_t mode);
- bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING
-
- // enable / disable interrupts for matrix and GPI pins
- void enableInterrupts();
- void disableInterrupts();
-
- // ignore key events when FIFO buffer is full or not.
- void enableMatrixOverflow();
- void disableMatrixOverflow();
-
- // debounce keys.
- void enableDebounce();
- void disableDebounce();
-
- void setBacklight(bool on);
-
- uint8_t readRegister(uint8_t reg) const;
- void writeRegister(uint8_t reg, uint8_t value);
-
- private:
- TwoWire *m_wire;
- uint8_t m_addr;
- i2c_com_fptr_t readCallback;
- i2c_com_fptr_t writeCallback;
+ bool should_backspace;
};
diff --git a/src/input/TCA8418KeyboardBase.cpp b/src/input/TCA8418KeyboardBase.cpp
new file mode 100644
index 000000000..aafc4c36c
--- /dev/null
+++ b/src/input/TCA8418KeyboardBase.cpp
@@ -0,0 +1,372 @@
+// Based on the MPR121 Keyboard and Adafruit TCA8418 library
+
+#include "TCA8418KeyboardBase.h"
+#include "configuration.h"
+
+#include
+
+// FIELDS CONFIG REGISTER 1
+#define _TCA8418_REG_CFG_AI 0x80 // Auto-increment for read/write
+#define _TCA8418_REG_CFG_GPI_E_CGF 0x40 // Event mode config
+#define _TCA8418_REG_CFG_OVR_FLOW_M 0x20 // Overflow mode enable
+#define _TCA8418_REG_CFG_INT_CFG 0x10 // Interrupt config
+#define _TCA8418_REG_CFG_OVR_FLOW_IEN 0x08 // Overflow interrupt enable
+#define _TCA8418_REG_CFG_K_LCK_IEN 0x04 // Keypad lock interrupt enable
+#define _TCA8418_REG_CFG_GPI_IEN 0x02 // GPI interrupt enable
+#define _TCA8418_REG_CFG_KE_IEN 0x01 // Key events interrupt enable
+
+// FIELDS INT_STAT REGISTER 2
+#define _TCA8418_REG_STAT_CAD_INT 0x10 // Ctrl-alt-del seq status
+#define _TCA8418_REG_STAT_OVR_FLOW_INT 0x08 // Overflow interrupt status
+#define _TCA8418_REG_STAT_K_LCK_INT 0x04 // Key lock interrupt status
+#define _TCA8418_REG_STAT_GPI_INT 0x02 // GPI interrupt status
+#define _TCA8418_REG_STAT_K_INT 0x01 // Key events interrupt status
+
+// FIELDS KEY_LCK_EC REGISTER 3
+#define _TCA8418_REG_LCK_EC_K_LCK_EN 0x40 // Key lock enable
+#define _TCA8418_REG_LCK_EC_LCK_2 0x20 // Keypad lock status 2
+#define _TCA8418_REG_LCK_EC_LCK_1 0x10 // Keypad lock status 1
+#define _TCA8418_REG_LCK_EC_KLEC_3 0x08 // Key event count bit 3
+#define _TCA8418_REG_LCK_EC_KLEC_2 0x04 // Key event count bit 2
+#define _TCA8418_REG_LCK_EC_KLEC_1 0x02 // Key event count bit 1
+#define _TCA8418_REG_LCK_EC_KLEC_0 0x01 // Key event count bit 0
+
+TCA8418KeyboardBase::TCA8418KeyboardBase(uint8_t rows, uint8_t columns)
+ : rows(rows), columns(columns), state(Init), queue(""), m_wire(nullptr), m_addr(0), readCallback(nullptr),
+ writeCallback(nullptr)
+{
+}
+
+void TCA8418KeyboardBase::begin(uint8_t addr, TwoWire *wire)
+{
+ m_addr = addr;
+ m_wire = wire;
+ m_wire->begin();
+ reset();
+}
+
+void TCA8418KeyboardBase::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr)
+{
+ m_addr = addr;
+ m_wire = nullptr;
+ writeCallback = w;
+ readCallback = r;
+ reset();
+}
+
+void TCA8418KeyboardBase::reset()
+{
+ LOG_DEBUG("TCA8418 Reset");
+ // GPIO
+ // set default all GIO pins to INPUT
+ writeRegister(TCA8418_REG_GPIO_DIR_1, 0x00);
+ writeRegister(TCA8418_REG_GPIO_DIR_2, 0x00);
+ writeRegister(TCA8418_REG_GPIO_DIR_3, 0x00);
+
+ // add all pins to key events
+ writeRegister(TCA8418_REG_GPI_EM_1, 0xFF);
+ writeRegister(TCA8418_REG_GPI_EM_2, 0xFF);
+ writeRegister(TCA8418_REG_GPI_EM_3, 0xFF);
+
+ // set all pins to FALLING interrupts
+ writeRegister(TCA8418_REG_GPIO_INT_LVL_1, 0x00);
+ writeRegister(TCA8418_REG_GPIO_INT_LVL_2, 0x00);
+ writeRegister(TCA8418_REG_GPIO_INT_LVL_3, 0x00);
+
+ // add all pins to interrupts
+ writeRegister(TCA8418_REG_GPIO_INT_EN_1, 0xFF);
+ writeRegister(TCA8418_REG_GPIO_INT_EN_2, 0xFF);
+ writeRegister(TCA8418_REG_GPIO_INT_EN_3, 0xFF);
+
+ // Set keyboard matrix size
+ matrix(rows, columns);
+ enableDebounce();
+ flush();
+ state = Idle;
+}
+
+bool TCA8418KeyboardBase::matrix(uint8_t rows, uint8_t columns)
+{
+ if (rows < 1 || rows > 8 || columns < 1 || columns > 10)
+ return false;
+
+ // Setup the keypad matrix.
+ uint8_t mask = 0x00;
+ for (int r = 0; r < rows; r++) {
+ mask <<= 1;
+ mask |= 1;
+ }
+ writeRegister(TCA8418_REG_KP_GPIO_1, mask);
+
+ mask = 0x00;
+ for (int c = 0; c < columns && c < 8; c++) {
+ mask <<= 1;
+ mask |= 1;
+ }
+ writeRegister(TCA8418_REG_KP_GPIO_2, mask);
+
+ if (columns > 8) {
+ if (columns == 9)
+ mask = 0x01;
+ else
+ mask = 0x03;
+ writeRegister(TCA8418_REG_KP_GPIO_3, mask);
+ }
+
+ return true;
+}
+
+uint8_t TCA8418KeyboardBase::keyCount() const
+{
+ uint8_t eventCount = readRegister(TCA8418_REG_KEY_LCK_EC);
+ eventCount &= 0x0F; // lower 4 bits only
+ return eventCount;
+}
+
+bool TCA8418KeyboardBase::hasEvent() const
+{
+ return queue.length() > 0;
+}
+
+void TCA8418KeyboardBase::queueEvent(char next)
+{
+ if (next == NONE) {
+ return;
+ }
+ queue.concat(next);
+}
+
+char TCA8418KeyboardBase::dequeueEvent()
+{
+ if (queue.length() < 1) {
+ return NONE;
+ }
+ char next = queue.charAt(0);
+ queue.remove(0, 1);
+ return next;
+}
+
+void TCA8418KeyboardBase::trigger()
+{
+ if (keyCount() == 0) {
+ return;
+ }
+ if (state != Init) {
+ // Read the key register
+ uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A);
+ uint8_t key = k & 0x7F;
+ if (k & 0x80) {
+ if (state == Idle)
+ pressed(key);
+ return;
+ } else {
+ if (state == Held) {
+ released();
+ }
+ state = Idle;
+ return;
+ }
+ } else {
+ reset();
+ }
+}
+
+void TCA8418KeyboardBase::pressed(uint8_t key)
+{
+ // must be defined in derived class
+ LOG_ERROR("pressed() not implemented in derived class");
+}
+
+void TCA8418KeyboardBase::released()
+{
+ // must be defined in derived class
+ LOG_ERROR("released() not implemented in derived class");
+}
+
+uint8_t TCA8418KeyboardBase::flush()
+{
+ // Flush key events
+ uint8_t count = 0;
+ while (readRegister(TCA8418_REG_KEY_EVENT_A) != 0)
+ count++;
+
+ // Flush gpio events
+ readRegister(TCA8418_REG_GPIO_INT_STAT_1);
+ readRegister(TCA8418_REG_GPIO_INT_STAT_2);
+ readRegister(TCA8418_REG_GPIO_INT_STAT_3);
+
+ // Clear INT_STAT register
+ writeRegister(TCA8418_REG_INT_STAT, 3);
+ return count;
+}
+
+uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const
+{
+ if (pinnum > TCA8418_COL9)
+ return 0xFF;
+
+ uint8_t reg = TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8;
+ uint8_t mask = (1 << (pinnum % 8));
+
+ // Level 0 = low other = high
+ uint8_t value = readRegister(reg);
+ if (value & mask)
+ return HIGH;
+ return LOW;
+}
+
+bool TCA8418KeyboardBase::digitalWrite(uint8_t pinnum, uint8_t level)
+{
+ if (pinnum > TCA8418_COL9)
+ return false;
+
+ uint8_t reg = TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8;
+ uint8_t mask = (1 << (pinnum % 8));
+
+ // Level 0 = low other = high
+ uint8_t value = readRegister(reg);
+ if (level == LOW)
+ value &= ~mask;
+ else
+ value |= mask;
+ writeRegister(reg, value);
+ return true;
+}
+
+bool TCA8418KeyboardBase::pinMode(uint8_t pinnum, uint8_t mode)
+{
+ if (pinnum > TCA8418_COL9)
+ return false;
+
+ uint8_t idx = pinnum / 8;
+ uint8_t reg = TCA8418_REG_GPIO_DIR_1 + idx;
+ uint8_t mask = (1 << (pinnum % 8));
+
+ // Mode 0 = input 1 = output
+ uint8_t value = readRegister(reg);
+ if (mode == OUTPUT)
+ value |= mask;
+ else
+ value &= ~mask;
+ writeRegister(reg, value);
+
+ // Pullup 0 = enabled 1 = disabled
+ reg = TCA8418_REG_GPIO_PULL_1 + idx;
+ value = readRegister(reg);
+ if (mode == INPUT_PULLUP)
+ value &= ~mask;
+ else
+ value |= mask;
+ writeRegister(reg, value);
+
+ return true;
+}
+
+bool TCA8418KeyboardBase::pinIRQMode(uint8_t pinnum, uint8_t mode)
+{
+ if (pinnum > TCA8418_COL9)
+ return false;
+ if ((mode != RISING) && (mode != FALLING))
+ return false;
+
+ // Mode 0 = falling 1 = rising
+ uint8_t idx = pinnum / 8;
+ uint8_t reg = TCA8418_REG_GPIO_INT_LVL_1 + idx;
+ uint8_t mask = (1 << (pinnum % 8));
+
+ uint8_t value = readRegister(reg);
+ if (mode == RISING)
+ value |= mask;
+ else
+ value &= ~mask;
+ writeRegister(reg, value);
+
+ // Enable interrupt
+ reg = TCA8418_REG_GPIO_INT_EN_1 + idx;
+ value = readRegister(reg);
+ value |= mask;
+ writeRegister(reg, value);
+
+ return true;
+}
+
+void TCA8418KeyboardBase::enableInterrupts()
+{
+ uint8_t value = readRegister(TCA8418_REG_CFG);
+ value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN);
+ writeRegister(TCA8418_REG_CFG, value);
+};
+
+void TCA8418KeyboardBase::disableInterrupts()
+{
+ uint8_t value = readRegister(TCA8418_REG_CFG);
+ value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN);
+ writeRegister(TCA8418_REG_CFG, value);
+};
+
+void TCA8418KeyboardBase::enableMatrixOverflow()
+{
+ uint8_t value = readRegister(TCA8418_REG_CFG);
+ value |= _TCA8418_REG_CFG_OVR_FLOW_M;
+ writeRegister(TCA8418_REG_CFG, value);
+};
+
+void TCA8418KeyboardBase::disableMatrixOverflow()
+{
+ uint8_t value = readRegister(TCA8418_REG_CFG);
+ value &= ~_TCA8418_REG_CFG_OVR_FLOW_M;
+ writeRegister(TCA8418_REG_CFG, value);
+};
+
+void TCA8418KeyboardBase::enableDebounce()
+{
+ writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0x00);
+ writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0x00);
+ writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0x00);
+}
+
+void TCA8418KeyboardBase::disableDebounce()
+{
+ writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0xFF);
+ writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0xFF);
+ writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0xFF);
+}
+
+void TCA8418KeyboardBase::setBacklight(bool on) {}
+
+uint8_t TCA8418KeyboardBase::readRegister(uint8_t reg) const
+{
+ if (m_wire) {
+ m_wire->beginTransmission(m_addr);
+ m_wire->write(reg);
+ m_wire->endTransmission();
+
+ m_wire->requestFrom(m_addr, (uint8_t)1);
+ if (m_wire->available() < 1)
+ return 0;
+
+ return m_wire->read();
+ }
+ if (readCallback) {
+ uint8_t data;
+ readCallback(m_addr, reg, &data, 1);
+ return data;
+ }
+ return 0;
+}
+
+void TCA8418KeyboardBase::writeRegister(uint8_t reg, uint8_t value)
+{
+ uint8_t data[2];
+ data[0] = reg;
+ data[1] = value;
+
+ if (m_wire) {
+ m_wire->beginTransmission(m_addr);
+ m_wire->write(data, sizeof(uint8_t) * 2);
+ m_wire->endTransmission();
+ }
+ if (writeCallback) {
+ writeCallback(m_addr, data[0], &(data[1]), 1);
+ }
+}
\ No newline at end of file
diff --git a/src/input/TCA8418KeyboardBase.h b/src/input/TCA8418KeyboardBase.h
new file mode 100644
index 000000000..5d6c4f7e9
--- /dev/null
+++ b/src/input/TCA8418KeyboardBase.h
@@ -0,0 +1,170 @@
+// Based on the MPR121 Keyboard and Adafruit TCA8418 library
+#include "configuration.h"
+#include
+
+/**
+ * @brief TCA8418KeyboardBase is the base class for TCA8418 keyboard handling.
+ * It provides basic functionality for reading key events, managing the keyboard matrix,
+ * and handling key states. It is designed to be extended for specific keyboard implementations.
+ * It supports both I2C communication and function pointers for custom I2C operations.
+ */
+class TCA8418KeyboardBase
+{
+ public:
+ enum TCA8418Key : uint8_t {
+ NONE = 0x00,
+ BSP = 0x08,
+ TAB = 0x09,
+ SELECT = 0x0d,
+ ESC = 0x1b,
+ REBOOT = 0x90,
+ LEFT = 0xb4,
+ UP = 0xb5,
+ DOWN = 0xb6,
+ RIGHT = 0xb7,
+ BT_TOGGLE = 0xAA,
+ GPS_TOGGLE = 0x9E,
+ MUTE_TOGGLE = 0xAC,
+ SEND_PING = 0xAF,
+ BL_TOGGLE = 0xAB
+ };
+
+ typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
+
+ TCA8418KeyboardBase(uint8_t rows, uint8_t columns);
+
+ virtual void begin(uint8_t addr = TCA8418_KB_ADDR, TwoWire *wire = &Wire);
+ virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR);
+
+ virtual void reset(void);
+ virtual void trigger(void);
+
+ virtual void setBacklight(bool on);
+
+ // Key events available
+ virtual bool hasEvent(void) const;
+ virtual char dequeueEvent(void);
+
+ protected:
+ enum KeyState { Init, Idle, Held, Busy };
+
+ enum TCA8418Register : uint8_t {
+ TCA8418_REG_RESERVED = 0x00,
+ TCA8418_REG_CFG = 0x01,
+ TCA8418_REG_INT_STAT = 0x02,
+ TCA8418_REG_KEY_LCK_EC = 0x03,
+ TCA8418_REG_KEY_EVENT_A = 0x04,
+ TCA8418_REG_KEY_EVENT_B = 0x05,
+ TCA8418_REG_KEY_EVENT_C = 0x06,
+ TCA8418_REG_KEY_EVENT_D = 0x07,
+ TCA8418_REG_KEY_EVENT_E = 0x08,
+ TCA8418_REG_KEY_EVENT_F = 0x09,
+ TCA8418_REG_KEY_EVENT_G = 0x0A,
+ TCA8418_REG_KEY_EVENT_H = 0x0B,
+ TCA8418_REG_KEY_EVENT_I = 0x0C,
+ TCA8418_REG_KEY_EVENT_J = 0x0D,
+ TCA8418_REG_KP_LCK_TIMER = 0x0E,
+ TCA8418_REG_UNLOCK_1 = 0x0F,
+ TCA8418_REG_UNLOCK_2 = 0x10,
+ TCA8418_REG_GPIO_INT_STAT_1 = 0x11,
+ TCA8418_REG_GPIO_INT_STAT_2 = 0x12,
+ TCA8418_REG_GPIO_INT_STAT_3 = 0x13,
+ TCA8418_REG_GPIO_DAT_STAT_1 = 0x14,
+ TCA8418_REG_GPIO_DAT_STAT_2 = 0x15,
+ TCA8418_REG_GPIO_DAT_STAT_3 = 0x16,
+ TCA8418_REG_GPIO_DAT_OUT_1 = 0x17,
+ TCA8418_REG_GPIO_DAT_OUT_2 = 0x18,
+ TCA8418_REG_GPIO_DAT_OUT_3 = 0x19,
+ TCA8418_REG_GPIO_INT_EN_1 = 0x1A,
+ TCA8418_REG_GPIO_INT_EN_2 = 0x1B,
+ TCA8418_REG_GPIO_INT_EN_3 = 0x1C,
+ TCA8418_REG_KP_GPIO_1 = 0x1D,
+ TCA8418_REG_KP_GPIO_2 = 0x1E,
+ TCA8418_REG_KP_GPIO_3 = 0x1F,
+ TCA8418_REG_GPI_EM_1 = 0x20,
+ TCA8418_REG_GPI_EM_2 = 0x21,
+ TCA8418_REG_GPI_EM_3 = 0x22,
+ TCA8418_REG_GPIO_DIR_1 = 0x23,
+ TCA8418_REG_GPIO_DIR_2 = 0x24,
+ TCA8418_REG_GPIO_DIR_3 = 0x25,
+ TCA8418_REG_GPIO_INT_LVL_1 = 0x26,
+ TCA8418_REG_GPIO_INT_LVL_2 = 0x27,
+ TCA8418_REG_GPIO_INT_LVL_3 = 0x28,
+ TCA8418_REG_DEBOUNCE_DIS_1 = 0x29,
+ TCA8418_REG_DEBOUNCE_DIS_2 = 0x2A,
+ TCA8418_REG_DEBOUNCE_DIS_3 = 0x2B,
+ TCA8418_REG_GPIO_PULL_1 = 0x2C,
+ TCA8418_REG_GPIO_PULL_2 = 0x2D,
+ TCA8418_REG_GPIO_PULL_3 = 0x2E
+ };
+
+ // Pin IDs for matrix rows/columns
+ enum TCA8418PinId : uint8_t {
+ TCA8418_ROW0, // Pin ID for row 0
+ TCA8418_ROW1, // Pin ID for row 1
+ TCA8418_ROW2, // Pin ID for row 2
+ TCA8418_ROW3, // Pin ID for row 3
+ TCA8418_ROW4, // Pin ID for row 4
+ TCA8418_ROW5, // Pin ID for row 5
+ TCA8418_ROW6, // Pin ID for row 6
+ TCA8418_ROW7, // Pin ID for row 7
+ TCA8418_COL0, // Pin ID for column 0
+ TCA8418_COL1, // Pin ID for column 1
+ TCA8418_COL2, // Pin ID for column 2
+ TCA8418_COL3, // Pin ID for column 3
+ TCA8418_COL4, // Pin ID for column 4
+ TCA8418_COL5, // Pin ID for column 5
+ TCA8418_COL6, // Pin ID for column 6
+ TCA8418_COL7, // Pin ID for column 7
+ TCA8418_COL8, // Pin ID for column 8
+ TCA8418_COL9 // Pin ID for column 9
+ };
+
+ virtual void pressed(uint8_t key);
+ virtual void released(void);
+
+ virtual void queueEvent(char);
+
+ virtual ~TCA8418KeyboardBase() {}
+
+ protected:
+ // Set the size of the keypad matrix
+ // All other rows and columns are set as inputs.
+ bool matrix(uint8_t rows, uint8_t columns);
+
+ uint8_t keyCount(void) const;
+
+ // Flush all events in the FIFO buffer + GPIO events.
+ uint8_t flush(void);
+
+ // debounce keys.
+ void enableDebounce();
+ void disableDebounce();
+
+ // enable / disable interrupts for matrix and GPI pins
+ void enableInterrupts();
+ void disableInterrupts();
+
+ // ignore key events when FIFO buffer is full or not.
+ void enableMatrixOverflow();
+ void disableMatrixOverflow();
+
+ uint8_t digitalRead(uint8_t pinnum) const;
+ bool digitalWrite(uint8_t pinnum, uint8_t level);
+ bool pinMode(uint8_t pinnum, uint8_t mode);
+ bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING
+ uint8_t readRegister(uint8_t reg) const;
+ void writeRegister(uint8_t reg, uint8_t value);
+
+ protected:
+ uint8_t rows;
+ uint8_t columns;
+ KeyState state;
+ String queue;
+
+ private:
+ TwoWire *m_wire;
+ uint8_t m_addr;
+ i2c_com_fptr_t readCallback;
+ i2c_com_fptr_t writeCallback;
+};
diff --git a/src/input/TDeckProKeyboard.cpp b/src/input/TDeckProKeyboard.cpp
new file mode 100644
index 000000000..098e0804a
--- /dev/null
+++ b/src/input/TDeckProKeyboard.cpp
@@ -0,0 +1,196 @@
+#if defined(T_DECK_PRO)
+
+#include "TDeckProKeyboard.h"
+
+#define _TCA8418_COLS 10
+#define _TCA8418_ROWS 4
+#define _TCA8418_NUM_KEYS 35
+
+#define _TCA8418_MULTI_TAP_THRESHOLD 1500
+
+using Key = TCA8418KeyboardBase::TCA8418Key;
+
+constexpr uint8_t modifierRightShiftKey = 31 - 1; // keynum -1
+constexpr uint8_t modifierRightShift = 0b0001;
+constexpr uint8_t modifierLeftShiftKey = 35 - 1;
+constexpr uint8_t modifierLeftShift = 0b0001;
+constexpr uint8_t modifierSymKey = 32 - 1;
+constexpr uint8_t modifierSym = 0b0010;
+constexpr uint8_t modifierAltKey = 30 - 1;
+constexpr uint8_t modifierAlt = 0b0100;
+
+// Num chars per key, Modulus for rotating through characters
+static uint8_t TDeckProTapMod[_TCA8418_NUM_KEYS] = {5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5};
+
+static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
+ {'p', 'P', '@', 0x00, Key::SEND_PING},
+ {'o', 'O', '+'},
+ {'i', 'I', '-'},
+ {'u', 'U', '_'},
+ {'y', 'Y', ')'},
+ {'t', 'T', '(', 0x00, Key::TAB},
+ {'r', 'R', '3'},
+ {'e', 'E', '2', 0x00, Key::UP},
+ {'w', 'W', '1'},
+ {'q', 'Q', '#', 0x00, Key::ESC}, // p, o, i, u, y, t, r, e, w, q
+ {Key::BSP, 0x00, 0x00},
+ {'l', 'L', '"'},
+ {'k', 'K', '\''},
+ {'j', 'J', ';'},
+ {'h', 'H', ':'},
+ {'g', 'G', '/', 0x00, Key::GPS_TOGGLE},
+ {'f', 'F', '6', 0x00, Key::RIGHT},
+ {'d', 'D', '5'},
+ {'s', 'S', '4', 0x00, Key::LEFT},
+ {'a', 'A', '*'}, // bsp, l, k, j, h, g, f, d, s, a
+ {0x0d, 0x00, 0x00},
+ {'$', 0x00, 0x00},
+ {'m', 'M', '.', 0x00, Key::MUTE_TOGGLE},
+ {'n', 'N', ','},
+ {'b', 'B', '!', 0x00, Key::BL_TOGGLE},
+ {'v', 'V', '?'},
+ {'c', 'C', '9'},
+ {'x', 'X', '8', 0x00, Key::DOWN},
+ {'z', 'Z', '7'},
+ {0x00, 0x00, 0x00}, // Ent, $, m, n, b, v, c, x, z, alt
+ {0x00, 0x00, 0x00},
+ {0x00, 0x00, 0x00},
+ {0x20, 0x00, 0x00},
+ {0x00, 0x00, 0x00},
+ {0x00, 0x00, 0x00} // R_Shift, sym, space, mic, L_Shift
+};
+
+TDeckProKeyboard::TDeckProKeyboard()
+ : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
+ last_tap(0L), char_idx(0), tap_interval(0)
+{
+}
+
+void TDeckProKeyboard::reset()
+{
+ TCA8418KeyboardBase::reset();
+ pinMode(KB_BL_PIN, OUTPUT);
+ setBacklight(false);
+}
+
+// handle multi-key presses (shift and alt)
+void TDeckProKeyboard::trigger()
+{
+ uint8_t count = keyCount();
+ if (count == 0)
+ return;
+ for (uint8_t i = 0; i < count; ++i) {
+ uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
+ uint8_t key = k & 0x7F;
+ if (k & 0x80) {
+ pressed(key);
+ } else {
+ released();
+ state = Idle;
+ }
+ }
+}
+
+void TDeckProKeyboard::pressed(uint8_t key)
+{
+ if (state == Init || state == Busy) {
+ return;
+ }
+ if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
+ modifierFlag = 0;
+ }
+
+ uint8_t next_key = 0;
+ int row = (key - 1) / 10;
+ int col = (key - 1) % 10;
+
+ if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
+ return; // Invalid key
+ }
+
+ next_key = row * _TCA8418_COLS + col;
+ state = Held;
+
+ uint32_t now = millis();
+ tap_interval = now - last_tap;
+
+ updateModifierFlag(next_key);
+ if (isModifierKey(next_key)) {
+ last_modifier_time = now;
+ }
+
+ if (tap_interval < 0) {
+ last_tap = 0;
+ state = Busy;
+ return;
+ }
+
+ if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
+ char_idx = 0;
+ } else {
+ char_idx += 1;
+ }
+
+ last_key = next_key;
+ last_tap = now;
+}
+
+void TDeckProKeyboard::released()
+{
+ if (state != Held) {
+ return;
+ }
+
+ if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
+ last_key = -1;
+ state = Idle;
+ return;
+ }
+
+ uint32_t now = millis();
+ last_tap = now;
+
+ if (TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]] == Key::BL_TOGGLE) {
+ toggleBacklight();
+ return;
+ }
+
+ queueEvent(TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]]);
+ if (isModifierKey(last_key) == false)
+ modifierFlag = 0;
+}
+
+void TDeckProKeyboard::setBacklight(bool on)
+{
+ if (on) {
+ digitalWrite(KB_BL_PIN, HIGH);
+ } else {
+ digitalWrite(KB_BL_PIN, LOW);
+ }
+}
+
+void TDeckProKeyboard::toggleBacklight(void)
+{
+ digitalWrite(KB_BL_PIN, !digitalRead(KB_BL_PIN));
+}
+
+void TDeckProKeyboard::updateModifierFlag(uint8_t key)
+{
+ if (key == modifierRightShiftKey) {
+ modifierFlag ^= modifierRightShift;
+ } else if (key == modifierLeftShiftKey) {
+ modifierFlag ^= modifierLeftShift;
+ } else if (key == modifierSymKey) {
+ modifierFlag ^= modifierSym;
+ } else if (key == modifierAltKey) {
+ modifierFlag ^= modifierAlt;
+ }
+}
+
+bool TDeckProKeyboard::isModifierKey(uint8_t key)
+{
+ return (key == modifierRightShiftKey || key == modifierLeftShiftKey || key == modifierAltKey || key == modifierSymKey);
+}
+
+#endif // T_DECK_PRO
\ No newline at end of file
diff --git a/src/input/TDeckProKeyboard.h b/src/input/TDeckProKeyboard.h
new file mode 100644
index 000000000..617f3f20b
--- /dev/null
+++ b/src/input/TDeckProKeyboard.h
@@ -0,0 +1,27 @@
+#include "TCA8418KeyboardBase.h"
+
+class TDeckProKeyboard : public TCA8418KeyboardBase
+{
+ public:
+ TDeckProKeyboard();
+ void reset(void) override;
+ void trigger(void) override;
+ void setBacklight(bool on) override;
+
+ protected:
+ void pressed(uint8_t key) override;
+ void released(void) override;
+
+ void updateModifierFlag(uint8_t key);
+ bool isModifierKey(uint8_t key);
+ void toggleBacklight(void);
+
+ private:
+ uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
+ uint32_t last_modifier_time; // Timestamp of the last modifier key press
+ int8_t last_key;
+ int8_t next_key;
+ uint32_t last_tap;
+ uint8_t char_idx;
+ int32_t tap_interval;
+};
diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp
new file mode 100644
index 000000000..b3f62a36c
--- /dev/null
+++ b/src/input/TLoraPagerKeyboard.cpp
@@ -0,0 +1,230 @@
+#if defined(T_LORA_PAGER)
+
+#include "TLoraPagerKeyboard.h"
+#include "main.h"
+
+#ifndef LEDC_BACKLIGHT_CHANNEL
+#define LEDC_BACKLIGHT_CHANNEL 4
+#endif
+
+#ifndef LEDC_BACKLIGHT_BIT_WIDTH
+#define LEDC_BACKLIGHT_BIT_WIDTH 8
+#endif
+
+#ifndef LEDC_BACKLIGHT_FREQ
+#define LEDC_BACKLIGHT_FREQ 1000 // Hz
+#endif
+
+#define _TCA8418_COLS 10
+#define _TCA8418_ROWS 4
+#define _TCA8418_NUM_KEYS 31
+
+#define _TCA8418_MULTI_TAP_THRESHOLD 1500
+
+using Key = TCA8418KeyboardBase::TCA8418Key;
+
+constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1
+constexpr uint8_t modifierRightShift = 0b0001;
+constexpr uint8_t modifierSymKey = 21 - 1;
+constexpr uint8_t modifierSym = 0b0010;
+
+// Num chars per key, Modulus for rotating through characters
+static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3};
+
+static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
+ {'w', 'W', '2'},
+ {'e', 'E', '3'},
+ {'r', 'R', '4'},
+ {'t', 'T', '5'},
+ {'y', 'Y', '6'},
+ {'u', 'U', '7'},
+ {'i', 'I', '8'},
+ {'o', 'O', '9'},
+ {'p', 'P', '0'},
+ {'a', 'A', '*'},
+ {'s', 'S', '/'},
+ {'d', 'D', '+'},
+ {'f', 'F', '-'},
+ {'g', 'G', '='},
+ {'h', 'H', ':'},
+ {'j', 'J', '\''},
+ {'k', 'K', '"'},
+ {'l', 'L', '@'},
+ {Key::SELECT, 0x00, Key::TAB},
+ {0x00, 0x00, 0x00},
+ {'z', 'Z', '_'},
+ {'x', 'X', '$'},
+ {'c', 'C', ';'},
+ {'v', 'V', '?'},
+ {'b', 'B', '!'},
+ {'n', 'N', ','},
+ {'m', 'M', '.'},
+ {0x00, 0x00, 0x00},
+ {Key::BSP, 0x00, Key::ESC},
+ {' ', 0x00, Key::BL_TOGGLE}};
+
+TLoraPagerKeyboard::TLoraPagerKeyboard()
+ : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
+ last_tap(0L), char_idx(0), tap_interval(0)
+{
+#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
+ ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
+#else
+ ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
+ ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL);
+#endif
+ reset();
+}
+
+void TLoraPagerKeyboard::reset(void)
+{
+ TCA8418KeyboardBase::reset();
+ pinMode(KB_BL_PIN, OUTPUT);
+ digitalWrite(KB_BL_PIN, LOW);
+ setBacklight(false);
+}
+
+// handle multi-key presses (shift and alt)
+void TLoraPagerKeyboard::trigger()
+{
+ uint8_t count = keyCount();
+ if (count == 0)
+ return;
+ for (uint8_t i = 0; i < count; ++i) {
+ uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
+ uint8_t key = k & 0x7F;
+ if (k & 0x80) {
+ pressed(key);
+ } else {
+ released();
+ state = Idle;
+ }
+ }
+}
+
+void TLoraPagerKeyboard::setBacklight(bool on)
+{
+ toggleBacklight(!on);
+}
+
+void TLoraPagerKeyboard::pressed(uint8_t key)
+{
+ if (state == Init || state == Busy) {
+ return;
+ }
+ if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED ||
+ config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) {
+ hapticFeedback();
+ }
+
+ if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
+ modifierFlag = 0;
+ }
+
+ uint8_t next_key = 0;
+ int row = (key - 1) / 10;
+ int col = (key - 1) % 10;
+
+ if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
+ return; // Invalid key
+ }
+
+ next_key = row * _TCA8418_COLS + col;
+ state = Held;
+
+ uint32_t now = millis();
+ tap_interval = now - last_tap;
+
+ updateModifierFlag(next_key);
+ if (isModifierKey(next_key)) {
+ last_modifier_time = now;
+ }
+
+ if (tap_interval < 0) {
+ last_tap = 0;
+ state = Busy;
+ return;
+ }
+
+ if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
+ char_idx = 0;
+ } else {
+ char_idx += 1;
+ }
+
+ last_key = next_key;
+ last_tap = now;
+}
+
+void TLoraPagerKeyboard::released()
+{
+ if (state != Held) {
+ return;
+ }
+
+ if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
+ last_key = -1;
+ state = Idle;
+ return;
+ }
+
+ uint32_t now = millis();
+ last_tap = now;
+
+ if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) {
+ toggleBacklight();
+ return;
+ }
+
+ queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]);
+ if (isModifierKey(last_key) == false)
+ modifierFlag = 0;
+}
+
+void TLoraPagerKeyboard::hapticFeedback()
+{
+ drv.setWaveform(0, 14); // strong buzz 100%
+ drv.setWaveform(1, 0); // end waveform
+ drv.go();
+}
+
+// toggle brightness of the backlight in three steps
+void TLoraPagerKeyboard::toggleBacklight(bool off)
+{
+ static uint32_t brightness = 0;
+ if (off) {
+ brightness = 0;
+ } else {
+ if (brightness == 0) {
+ brightness = 40;
+ } else if (brightness == 40) {
+ brightness = 127;
+ } else if (brightness >= 127) {
+ brightness = 0;
+ }
+ }
+ LOG_DEBUG("Toggle backlight: %d", brightness);
+
+#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
+ ledcWrite(KB_BL_PIN, brightness);
+#else
+ ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness);
+#endif
+}
+
+void TLoraPagerKeyboard::updateModifierFlag(uint8_t key)
+{
+ if (key == modifierRightShiftKey) {
+ modifierFlag ^= modifierRightShift;
+ } else if (key == modifierSymKey) {
+ modifierFlag ^= modifierSym;
+ }
+}
+
+bool TLoraPagerKeyboard::isModifierKey(uint8_t key)
+{
+ return (key == modifierRightShiftKey || key == modifierSymKey);
+}
+
+#endif
\ No newline at end of file
diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h
new file mode 100644
index 000000000..4dabbac64
--- /dev/null
+++ b/src/input/TLoraPagerKeyboard.h
@@ -0,0 +1,29 @@
+#include "TCA8418KeyboardBase.h"
+
+class TLoraPagerKeyboard : public TCA8418KeyboardBase
+{
+ public:
+ TLoraPagerKeyboard();
+ void reset(void);
+ void trigger(void) override;
+ void setBacklight(bool on) override;
+ virtual ~TLoraPagerKeyboard() {}
+
+ protected:
+ void pressed(uint8_t key) override;
+ void released(void) override;
+ void hapticFeedback(void);
+
+ void updateModifierFlag(uint8_t key);
+ bool isModifierKey(uint8_t key);
+ void toggleBacklight(bool off = false);
+
+ private:
+ uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
+ uint32_t last_modifier_time; // Timestamp of the last modifier key press
+ int8_t last_key;
+ int8_t next_key;
+ uint32_t last_tap;
+ uint8_t char_idx;
+ int32_t tap_interval;
+};
diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp
index c66eb13d0..26b281aaf 100644
--- a/src/input/UpDownInterruptBase.cpp
+++ b/src/input/UpDownInterruptBase.cpp
@@ -15,14 +15,23 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress,
this->_eventDown = eventDown;
this->_eventUp = eventUp;
this->_eventPressed = eventPressed;
+ bool isRAK = false;
+#ifdef RAK_4631
+ isRAK = true;
+#endif
- pinMode(pinPress, INPUT_PULLUP);
- pinMode(this->_pinDown, INPUT_PULLUP);
- pinMode(this->_pinUp, INPUT_PULLUP);
-
- attachInterrupt(pinPress, onIntPress, RISING);
- attachInterrupt(this->_pinDown, onIntDown, RISING);
- attachInterrupt(this->_pinUp, onIntUp, RISING);
+ if (!isRAK || pinPress != 0) {
+ pinMode(pinPress, INPUT_PULLUP);
+ attachInterrupt(pinPress, onIntPress, RISING);
+ }
+ if (!isRAK || this->_pinDown != 0) {
+ pinMode(this->_pinDown, INPUT_PULLUP);
+ attachInterrupt(this->_pinDown, onIntDown, RISING);
+ }
+ if (!isRAK || this->_pinUp != 0) {
+ pinMode(this->_pinUp, INPUT_PULLUP);
+ attachInterrupt(this->_pinUp, onIntUp, RISING);
+ }
LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress);
diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp
index 21ecf381a..9b0926a1d 100644
--- a/src/input/cardKbI2cImpl.cpp
+++ b/src/input/cardKbI2cImpl.cpp
@@ -12,8 +12,8 @@ void CardKbI2cImpl::init()
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN)
if (cardkb_found.address == 0x00) {
LOG_DEBUG("Rescan for I2C keyboard");
- uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, XPOWERS_AXP192_AXP2101_ADDRESS};
- uint8_t i2caddr_asize = 5;
+ uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR};
+ uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]);
auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire());
#if WIRE_INTERFACES_COUNT == 2
@@ -67,4 +67,5 @@ void CardKbI2cImpl::init()
}
#endif
inputBroker->registerSource(this);
+ kb_found = true;
}
\ No newline at end of file
diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp
index 5cc069816..5db1e39a9 100644
--- a/src/input/kbI2cBase.cpp
+++ b/src/input/kbI2cBase.cpp
@@ -3,10 +3,26 @@
#include "detect/ScanI2C.h"
#include "detect/ScanI2CTwoWire.h"
+#if defined(T_DECK_PRO)
+#include "TDeckProKeyboard.h"
+#elif defined(T_LORA_PAGER)
+#include "TLoraPagerKeyboard.h"
+#else
+#include "TCA8418Keyboard.h"
+#endif
+
extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model;
-KbI2cBase::KbI2cBase(const char *name) : concurrency::OSThread(name)
+KbI2cBase::KbI2cBase(const char *name)
+ : concurrency::OSThread(name),
+#if defined(T_DECK_PRO)
+ TCAKeyboard(*(new TDeckProKeyboard()))
+#elif defined(T_LORA_PAGER)
+ TCAKeyboard(*(new TLoraPagerKeyboard()))
+#else
+ TCAKeyboard(*(new TCA8418Keyboard()))
+#endif
{
this->_originName = name;
}
@@ -43,8 +59,8 @@ int32_t KbI2cBase::runOnce()
if (cardkb_found.address == MPR121_KB_ADDR) {
MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1);
}
- if (cardkb_found.address == XPOWERS_AXP192_AXP2101_ADDRESS) {
- TCAKeyboard.begin(XPOWERS_AXP192_AXP2101_ADDRESS, &Wire1);
+ if (cardkb_found.address == TCA8418_KB_ADDR) {
+ TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1);
}
break;
#endif
@@ -58,8 +74,8 @@ int32_t KbI2cBase::runOnce()
if (cardkb_found.address == MPR121_KB_ADDR) {
MPRkeyboard.begin(MPR121_KB_ADDR, &Wire);
}
- if (cardkb_found.address == XPOWERS_AXP192_AXP2101_ADDRESS) {
- TCAKeyboard.begin(XPOWERS_AXP192_AXP2101_ADDRESS, &Wire);
+ if (cardkb_found.address == TCA8418_KB_ADDR) {
+ TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire);
}
break;
case ScanI2C::NO_I2C:
@@ -241,41 +257,65 @@ int32_t KbI2cBase::runOnce()
e.kbchar = 0x00;
e.source = this->_originName;
switch (nextEvent) {
- case _TCA8418_NONE:
+ case TCA8418KeyboardBase::NONE:
e.inputEvent = INPUT_BROKER_NONE;
e.kbchar = 0x00;
break;
- case _TCA8418_REBOOT:
+ case TCA8418KeyboardBase::REBOOT:
e.inputEvent = INPUT_BROKER_ANYKEY;
e.kbchar = INPUT_BROKER_MSG_REBOOT;
break;
- case _TCA8418_LEFT:
+ case TCA8418KeyboardBase::LEFT:
e.inputEvent = INPUT_BROKER_LEFT;
e.kbchar = 0x00;
break;
- case _TCA8418_UP:
+ case TCA8418KeyboardBase::UP:
e.inputEvent = INPUT_BROKER_UP;
e.kbchar = 0x00;
break;
- case _TCA8418_DOWN:
+ case TCA8418KeyboardBase::DOWN:
e.inputEvent = INPUT_BROKER_DOWN;
e.kbchar = 0x00;
break;
- case _TCA8418_RIGHT:
+ case TCA8418KeyboardBase::RIGHT:
e.inputEvent = INPUT_BROKER_RIGHT;
e.kbchar = 0x00;
break;
- case _TCA8418_BSP:
+ case TCA8418KeyboardBase::BSP:
e.inputEvent = INPUT_BROKER_BACK;
e.kbchar = 0x08;
break;
- case _TCA8418_SELECT:
+ case TCA8418KeyboardBase::SELECT:
e.inputEvent = INPUT_BROKER_SELECT;
e.kbchar = 0x00;
break;
- case _TCA8418_ESC:
+ case TCA8418KeyboardBase::ESC:
e.inputEvent = INPUT_BROKER_CANCEL;
- e.kbchar = 0;
+ e.kbchar = 0x00;
+ break;
+ case TCA8418KeyboardBase::GPS_TOGGLE:
+ e.inputEvent = INPUT_BROKER_ANYKEY;
+ e.kbchar = INPUT_BROKER_GPS_TOGGLE;
+ break;
+ case TCA8418KeyboardBase::SEND_PING:
+ e.inputEvent = INPUT_BROKER_ANYKEY;
+ e.kbchar = INPUT_BROKER_SEND_PING;
+ break;
+ case TCA8418KeyboardBase::MUTE_TOGGLE:
+ e.inputEvent = INPUT_BROKER_ANYKEY;
+ e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE;
+ break;
+ case TCA8418KeyboardBase::BT_TOGGLE:
+ e.inputEvent = INPUT_BROKER_ANYKEY;
+ e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE;
+ break;
+ case TCA8418KeyboardBase::BL_TOGGLE:
+ e.inputEvent = INPUT_BROKER_ANYKEY;
+ e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE;
+ break;
+ case TCA8418KeyboardBase::TAB:
+ e.inputEvent = INPUT_BROKER_ANYKEY;
+ e.kbchar = INPUT_BROKER_MSG_TAB;
break;
default:
if (nextEvent > 127) {
@@ -291,6 +331,7 @@ int32_t KbI2cBase::runOnce()
LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar);
this->notifyObservers(&e);
}
+ TCAKeyboard.trigger();
}
break;
}
diff --git a/src/input/kbI2cBase.h b/src/input/kbI2cBase.h
index 8193433fe..af7602979 100644
--- a/src/input/kbI2cBase.h
+++ b/src/input/kbI2cBase.h
@@ -3,10 +3,11 @@
#include "BBQ10Keyboard.h"
#include "InputBroker.h"
#include "MPR121Keyboard.h"
-#include "TCA8418Keyboard.h"
#include "Wire.h"
#include "concurrency/OSThread.h"
+class TCA8418KeyboardBase;
+
class KbI2cBase : public Observable, public concurrency::OSThread
{
public:
@@ -22,6 +23,6 @@ class KbI2cBase : public Observable, public concurrency::OST
BBQ10Keyboard Q10keyboard;
MPR121Keyboard MPRkeyboard;
- TCA8418Keyboard TCAKeyboard;
+ TCA8418KeyboardBase &TCAKeyboard;
bool is_sym = false;
};
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 229bb0c49..a3b40e091 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -33,12 +33,15 @@
#include "mesh/generated/meshtastic/config.pb.h"
#include "meshUtils.h"
#include "modules/Modules.h"
-#include "shutdown.h"
#include "sleep.h"
#include "target_specific.h"
#include
#include
+#ifdef ELECROW_ThinkNode_M5
+PCA9557 io(0x18, &Wire);
+#endif
+
#ifdef ARCH_ESP32
#include "freertosinc.h"
#if !MESHTASTIC_EXCLUDE_WEBSERVER
@@ -132,8 +135,9 @@ AccelerometerThread *accelerometerThread = nullptr;
AudioThread *audioThread = nullptr;
#endif
-#ifdef USE_PCA9557
-PCA9557 IOEXP;
+#ifdef USE_XL9555
+#include "ExtensionIOXL9555.hpp"
+ExtensionIOXL9555 io;
#endif
#if HAS_TFT
@@ -198,7 +202,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE,
/// The I2C address of our Air Quality Indicator (if found)
ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE;
-#ifdef T_WATCH_S3
+#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
Adafruit_DRV2605 drv;
#endif
@@ -286,7 +290,7 @@ void lateInitVariant() {}
*/
void printInfo()
{
- LOG_INFO("S:B:%d,%s", HW_VENDOR, optstr(APP_VERSION));
+ LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
}
#ifndef PIO_UNIT_TESTING
void setup()
@@ -297,6 +301,14 @@ void setup()
digitalWrite(PIN_POWER_EN, HIGH);
#endif
+#if defined(ELECROW_ThinkNode_M5)
+ Wire.begin(48, 47);
+ io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
+ io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
+ io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
+ // io.pinMode(C2_PIN, OUTPUT);
+#endif
+
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
@@ -314,8 +326,12 @@ void setup()
#ifdef BLE_LED
pinMode(BLE_LED, OUTPUT);
+#ifdef BLE_LED_INVERTED
+ digitalWrite(BLE_LED, HIGH);
+#else
digitalWrite(BLE_LED, LOW);
#endif
+#endif
#if defined(T_DECK)
// GPIO10 manages all peripheral power supplies
@@ -335,6 +351,39 @@ void setup()
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
delay(100);
+#elif defined(T_DECK_PRO)
+ pinMode(LORA_EN, OUTPUT);
+ digitalWrite(LORA_EN, HIGH);
+ pinMode(LORA_CS, OUTPUT);
+ digitalWrite(LORA_CS, HIGH);
+ pinMode(SDCARD_CS, OUTPUT);
+ digitalWrite(SDCARD_CS, HIGH);
+ pinMode(PIN_EINK_CS, OUTPUT);
+ digitalWrite(PIN_EINK_CS, HIGH);
+#elif defined(T_LORA_PAGER)
+ pinMode(LORA_CS, OUTPUT);
+ digitalWrite(LORA_CS, HIGH);
+ pinMode(SDCARD_CS, OUTPUT);
+ digitalWrite(SDCARD_CS, HIGH);
+ pinMode(TFT_CS, OUTPUT);
+ digitalWrite(TFT_CS, HIGH);
+ // io expander
+ io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
+ io.pinMode(EXPANDS_DRV_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_DRV_EN, HIGH);
+ io.pinMode(EXPANDS_AMP_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_AMP_EN, HIGH);
+ io.pinMode(EXPANDS_LORA_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_LORA_EN, HIGH);
+ io.pinMode(EXPANDS_GPS_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_GPS_EN, HIGH);
+ io.pinMode(EXPANDS_KB_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_KB_EN, HIGH);
+ io.pinMode(EXPANDS_SD_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_SD_EN, HIGH);
+ io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
+ io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
+ io.pinMode(EXPANDS_SD_PULLEN, INPUT);
#endif
concurrency::hasBeenSetup = true;
@@ -380,6 +429,16 @@ void setup()
initDeepSleep();
+#if defined(MODEM_POWER_EN)
+ pinMode(MODEM_POWER_EN, OUTPUT);
+ digitalWrite(MODEM_POWER_EN, LOW);
+#endif
+
+#if defined(MODEM_PWRKEY)
+ pinMode(MODEM_PWRKEY, OUTPUT);
+ digitalWrite(MODEM_PWRKEY, LOW);
+#endif
+
#if defined(LORA_TCXO_GPIO)
pinMode(LORA_TCXO_GPIO, OUTPUT);
digitalWrite(LORA_TCXO_GPIO, HIGH);
@@ -515,25 +574,11 @@ void setup()
LOG_INFO("Scan for i2c devices");
#endif
-#if defined(I2C_SDA1) && defined(ARCH_RP2040)
- Wire1.setSDA(I2C_SDA1);
- Wire1.setSCL(I2C_SCL1);
- Wire1.begin();
- i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
-#elif defined(I2C_SDA1) && !defined(ARCH_RP2040)
- Wire1.begin(I2C_SDA1, I2C_SCL1);
- i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
-#elif defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)
+#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
#endif
-#if defined(I2C_SDA) && defined(ARCH_RP2040)
- Wire.setSDA(I2C_SDA);
- Wire.setSCL(I2C_SCL);
- Wire.begin();
- i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
-#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
- Wire.begin(I2C_SDA, I2C_SCL);
+#if defined(I2C_SDA)
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
#elif defined(ARCH_PORTDUINO)
if (settingsStrings[i2cdev] != "") {
@@ -785,7 +830,7 @@ void setup()
#endif
#endif
-#ifdef T_WATCH_S3
+#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
drv.begin();
drv.selectLibrary(1);
// I2C trigger by sending 'go' command
@@ -835,7 +880,7 @@ 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(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
#elif defined(ARCH_PORTDUINO)
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
@@ -912,14 +957,20 @@ void setup()
service = new MeshService();
service->init();
- if (nodeDB->keyIsLowEntropy) {
- service->reloadConfig(SEGMENT_CONFIG);
- rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
- }
-
// Now that the mesh service is created, create any modules
setupModules();
+ // warn the user about a low entropy key
+ if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) {
+ LOG_WARN(LOW_ENTROPY_WARNING);
+ meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
+ cn->level = meshtastic_LogRecord_Level_WARNING;
+ cn->time = getValidTime(RTCQualityFromNet);
+ sprintf(cn->message, LOW_ENTROPY_WARNING);
+ service->sendClientNotification(cn);
+ nodeDB->hasWarned = true;
+ }
+
// buttons are now inputBroker, so have to come after setupModules
#if HAS_BUTTON
int pullup_sense = 0;
@@ -1060,8 +1111,9 @@ void setup()
mainDelay.interruptFromISR(&higherWake);
};
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
- userConfigNoScreen.longPress = INPUT_BROKER_SHUTDOWN;
- userConfigNoScreen.longPressTime = 5000;
+ userConfigNoScreen.longPress = INPUT_BROKER_NONE;
+ userConfigNoScreen.longPressTime = 500;
+ userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
UserButtonThread->initButton(userConfigNoScreen);
@@ -1091,7 +1143,7 @@ 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(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
if (screen)
screen->setup();
#elif defined(ARCH_PORTDUINO)
@@ -1542,7 +1594,7 @@ void loop()
#ifdef ARCH_NRF52
nrf52Loop();
#endif
- powerCommandsCheck();
+ power->powerCommandsCheck();
#ifdef DEBUG_STACK
static uint32_t lastPrint = 0;
@@ -1553,7 +1605,13 @@ void loop()
#endif
service->loop();
-
+#if defined(LGFX_SDL)
+ if (screen) {
+ auto dispdev = screen->getDisplayDevice();
+ if (dispdev)
+ static_cast(dispdev)->sdlLoop();
+ }
+#endif
long delayMsec = mainController.runOrDelay();
// We want to sleep as long as possible here - because it saves power
diff --git a/src/main.h b/src/main.h
index 7105bd62b..ef1f241ef 100644
--- a/src/main.h
+++ b/src/main.h
@@ -41,7 +41,7 @@ extern bool eink_found;
extern bool pmu_found;
extern bool isUSBPowered;
-#ifdef T_WATCH_S3
+#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
#include
extern Adafruit_DRV2605 drv;
#endif
@@ -51,6 +51,11 @@ extern Adafruit_DRV2605 drv;
extern AudioThread *audioThread;
#endif
+#ifdef ELECROW_ThinkNode_M5
+#include
+extern PCA9557 io;
+#endif
+
#ifdef HAS_UDP_MULTICAST
#include "mesh/udp/UdpMulticastHandler.h"
extern UdpMulticastHandler *udpHandler;
diff --git a/src/memGet.cpp b/src/memGet.cpp
index ef1102f1e..e8cd177dd 100644
--- a/src/memGet.cpp
+++ b/src/memGet.cpp
@@ -10,6 +10,10 @@
#include "memGet.h"
#include "configuration.h"
+#ifdef ARCH_STM32WL
+#include
+#endif
+
MemGet memGet;
/**
@@ -24,6 +28,9 @@ uint32_t MemGet::getFreeHeap()
return dbgHeapFree();
#elif defined(ARCH_RP2040)
return rp2040.getFreeHeap();
+#elif defined(ARCH_STM32WL)
+ struct mallinfo m = mallinfo();
+ return m.fordblks; // Total free space (bytes)
#else
// this platform does not have heap management function implemented
return UINT32_MAX;
@@ -42,6 +49,9 @@ uint32_t MemGet::getHeapSize()
return dbgHeapTotal();
#elif defined(ARCH_RP2040)
return rp2040.getTotalHeap();
+#elif defined(ARCH_STM32WL)
+ struct mallinfo m = mallinfo();
+ return m.arena; // Non-mmapped space allocated (bytes)
#else
// this platform does not have heap management function implemented
return UINT32_MAX;
diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp
index 142ada806..dbd458b61 100644
--- a/src/mesh/FloodingRouter.cpp
+++ b/src/mesh/FloodingRouter.cpp
@@ -47,8 +47,10 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
{
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
- config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
+ config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE &&
+ p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater!
+ // But only LoRa packets should be able to trigger this.
if (Router::cancelSending(p->from, p->id))
txRelayCanceled++;
}
diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp
index a20db808e..a0d992c42 100644
--- a/src/mesh/LR11x0Interface.cpp
+++ b/src/mesh/LR11x0Interface.cpp
@@ -6,6 +6,10 @@
#include "mesh/NodeDB.h"
#ifdef LR11X0_DIO_AS_RF_SWITCH
#include "rfswitch.h"
+#elif ARCH_PORTDUINO
+#include "PortduinoGlue.h"
+#define rfswitch_dio_pins portduino_config.rfswitch_dio_pins
+#define rfswitch_table portduino_config.rfswitch_table
#else
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
@@ -14,10 +18,6 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
};
#endif
-#ifdef ARCH_PORTDUINO
-#include "PortduinoGlue.h"
-#endif
-
// 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
@@ -117,17 +117,14 @@ template bool LR11x0Interface::init()
#ifdef LR11X0_DIO_AS_RF_SWITCH
bool dioAsRfSwitch = true;
#elif defined(ARCH_PORTDUINO)
- bool dioAsRfSwitch = false;
- if (settingsMap[dio2_as_rf_switch]) {
- dioAsRfSwitch = true;
- }
+ bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
#else
bool dioAsRfSwitch = false;
#endif
if (dioAsRfSwitch) {
lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table);
- LOG_DEBUG("Set DIO RF switch", res);
+ LOG_DEBUG("Set DIO RF switch");
}
if (res == RADIOLIB_ERR_NONE) {
diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp
index c5748a560..22fcec663 100644
--- a/src/mesh/MeshModule.cpp
+++ b/src/mesh/MeshModule.cpp
@@ -100,6 +100,7 @@ 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;
diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp
index f8af81321..a64678a7f 100644
--- a/src/mesh/MeshPacketQueue.cpp
+++ b/src/mesh/MeshPacketQueue.cpp
@@ -121,7 +121,7 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t
bool MeshPacketQueue::find(const NodeNum from, const PacketId id)
{
for (auto it = queue.begin(); it != queue.end(); it++) {
- const auto p = (*it);
+ const auto *p = *it;
if (getFrom(p) == from && p->id == id) {
return true;
}
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 297c7b2ed..2cc4197c1 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -16,6 +16,7 @@
#include "meshUtils.h"
#include "modules/NodeInfoModule.h"
#include "modules/PositionModule.h"
+#include "modules/RoutingModule.h"
#include "power.h"
#include
#include
@@ -333,6 +334,21 @@ void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage
fromNum++;
}
+void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp)
+{
+ if (!mp) {
+ LOG_WARN("Cannot send routing error response: null packet");
+ return;
+ }
+
+ // Use the routing module to send the error response
+ if (routingModule) {
+ routingModule->sendAckNak(error, mp->from, mp->id, mp->channel);
+ } else {
+ LOG_ERROR("Cannot send routing error response: no routing module");
+ }
+}
+
void MeshService::sendClientNotification(meshtastic_ClientNotification *n)
{
LOG_DEBUG("Send client notification to phone");
diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h
index e2e430c03..f7d79366e 100644
--- a/src/mesh/MeshService.h
+++ b/src/mesh/MeshService.h
@@ -146,7 +146,10 @@ class MeshService
virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m);
/// Send a ClientNotification to the phone
- void sendClientNotification(meshtastic_ClientNotification *cn);
+ virtual void sendClientNotification(meshtastic_ClientNotification *cn);
+
+ /// Send an error response to the phone
+ void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp);
bool isToPhoneQueueEmpty();
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index 185ea0744..c8eba1b2e 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -225,7 +225,11 @@ NodeDB::NodeDB()
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
myNodeInfo.device_id.size = 16;
// Uncomment below to print the device id
-
+#elif ARCH_PORTDUINO
+ if (portduino_config.has_device_id) {
+ memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
+ myNodeInfo.device_id.size = 16;
+ }
#else
// FIXME - implement for other platforms
#endif
@@ -264,12 +268,12 @@ NodeDB::NodeDB()
if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
bool keygenSuccess = false;
- if (config.security.private_key.size == 32) {
+ keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key);
+ if (config.security.private_key.size == 32 && !keyIsLowEntropy) {
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true;
}
} else {
- LOG_INFO("Generate new PKI keys");
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
keygenSuccess = true;
}
@@ -288,16 +292,6 @@ NodeDB::NodeDB()
crypto->setDHPrivateKey(config.security.private_key.bytes);
}
#endif
- keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key);
- if (keyIsLowEntropy) {
- LOG_WARN("Erasing low entropy keys");
- config.security.private_key.size = 0;
- memfll(config.security.private_key.bytes, '\0', sizeof(config.security.private_key.bytes));
- config.security.public_key.size = 0;
- memfll(config.security.public_key.bytes, '\0', sizeof(config.security.public_key.bytes));
- owner.public_key.size = 0;
- memfll(owner.public_key.bytes, '\0', sizeof(owner.public_key.bytes));
- }
// Include our owner in the node db under our nodenum
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
info->user = TypeConversions::ConvertToUserLite(owner);
@@ -406,6 +400,9 @@ NodeDB::NodeDB()
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
config.position.gps_enabled = 0;
}
+#ifdef USERPREFS_FIRMWARE_EDITION
+ myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION;
+#endif
#ifdef USERPREFS_FIXED_GPS
if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset.
meshtastic_Position fixedGPS = meshtastic_Position_init_default;
@@ -628,11 +625,6 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#ifdef PIN_GPS_EN
config.position.gps_en_gpio = PIN_GPS_EN;
#endif
-#ifdef GPS_POWER_TOGGLE
- config.device.disable_triple_click = false;
-#else
- config.device.disable_triple_click = true;
-#endif
#if defined(USERPREFS_CONFIG_GPS_MODE)
config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE;
#elif !HAS_GPS || GPS_DEFAULT_NOT_PRESENT
@@ -671,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(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
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);
@@ -735,7 +727,6 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.display.screen_on_secs = 30;
config.display.wake_on_tap_or_motion = true;
#endif
-
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
if (WiFiOTA::isUpdated()) {
WiFiOTA::recoverConfig(&config.network);
@@ -796,6 +787,13 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.alert_message_buzzer = true;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
+#if defined(PIN_VIBRATION)
+ moduleConfig.external_notification.enabled = true;
+ moduleConfig.external_notification.output_vibra = PIN_VIBRATION;
+ moduleConfig.external_notification.alert_message_vibra = true;
+ moduleConfig.external_notification.output_ms = 500;
+ moduleConfig.external_notification.nag_timeout = 2;
+#endif
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312)
// Default to RAK led pin 2 (blue)
moduleConfig.external_notification.enabled = true;
@@ -832,6 +830,15 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = 60;
+#endif
+#ifdef T_LORA_PAGER
+ moduleConfig.canned_message.updown1_enabled = true;
+ moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
+ moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B;
+ moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS;
+ moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28);
+ moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29);
+ moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
#endif
moduleConfig.has_canned_message = true;
#if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT
@@ -1637,25 +1644,33 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
printBytes("Incoming Pubkey: ", p.public_key.bytes, 32);
// Alert the user if a remote node is advertising public key that matches our own
- if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0 && !duplicateWarned) {
- duplicateWarned = true;
- char warning[] = "Remote device %s has advertised your public key. This may indicate a compromised key. You may need "
- "to regenerate your public keys.";
- LOG_WARN(warning, p.long_name);
- meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
- cn->which_payload_variant = meshtastic_ClientNotification_duplicated_public_key_tag;
- cn->level = meshtastic_LogRecord_Level_WARNING;
- cn->time = getValidTime(RTCQualityFromNet);
- sprintf(cn->message, warning, p.long_name);
- service->sendClientNotification(cn);
+ if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) {
+ if (!duplicateWarned) {
+ duplicateWarned = true;
+ char warning[] =
+ "Remote device %s has advertised your public key. This may indicate a compromised key. You may need "
+ "to regenerate your public keys.";
+ LOG_WARN(warning, p.long_name);
+ meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
+ cn->level = meshtastic_LogRecord_Level_WARNING;
+ cn->time = getValidTime(RTCQualityFromNet);
+ sprintf(cn->message, warning, p.long_name);
+ service->sendClientNotification(cn);
+ }
+ return false;
}
}
- if (info->user.public_key.size > 0) { // if we have a key for this user already, don't overwrite with a new one
+ if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
+ // if the key doesn't match, don't update nodeDB at all.
+ if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) {
+ LOG_WARN("Public Key mismatch, dropping NodeInfo");
+ return false;
+ }
LOG_INFO("Public Key set for node, not updating!");
// we copy the key into the incoming packet, to prevent overwrite
p.public_key.size = 32;
memcpy(p.public_key.bytes, info->user.public_key.bytes, 32);
- } else if (p.public_key.size > 0) {
+ } else if (p.public_key.size == 32) {
LOG_INFO("Update Node Pubkey!");
}
#endif
@@ -1696,10 +1711,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
/// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw
void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
{
- // if (mp.from == getNodeNum()) {
- // LOG_DEBUG("Ignore update from self");
- // return;
- // }
+ if (mp.from == getNodeNum()) {
+ LOG_DEBUG("Ignore update from self");
+ return;
+ }
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time);
@@ -1868,34 +1883,16 @@ UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
}
-bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t keyToTest)
+bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest)
{
if (keyToTest.size == 32) {
uint8_t keyHash[32] = {0};
memcpy(keyHash, keyToTest.bytes, keyToTest.size);
crypto->hash(keyHash, 32);
- if (memcmp(keyHash, LOW_ENTROPY_HASH1, sizeof(LOW_ENTROPY_HASH1)) ==
- 0 || // should become an array that gets looped through rather than this abomination
- memcmp(keyHash, LOW_ENTROPY_HASH2, sizeof(LOW_ENTROPY_HASH2)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH3, sizeof(LOW_ENTROPY_HASH3)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH4, sizeof(LOW_ENTROPY_HASH4)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH5, sizeof(LOW_ENTROPY_HASH5)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH6, sizeof(LOW_ENTROPY_HASH6)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH7, sizeof(LOW_ENTROPY_HASH7)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH8, sizeof(LOW_ENTROPY_HASH8)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH9, sizeof(LOW_ENTROPY_HASH9)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH10, sizeof(LOW_ENTROPY_HASH10)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH11, sizeof(LOW_ENTROPY_HASH11)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH12, sizeof(LOW_ENTROPY_HASH12)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH13, sizeof(LOW_ENTROPY_HASH13)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH14, sizeof(LOW_ENTROPY_HASH14)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH15, sizeof(LOW_ENTROPY_HASH15)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH16, sizeof(LOW_ENTROPY_HASH16)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH17, sizeof(LOW_ENTROPY_HASH17)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH18, sizeof(LOW_ENTROPY_HASH18)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH19, sizeof(LOW_ENTROPY_HASH19)) == 0 ||
- memcmp(keyHash, LOW_ENTROPY_HASH20, sizeof(LOW_ENTROPY_HASH20)) == 0) {
- return true;
+ for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) {
+ if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) {
+ return true;
+ }
}
}
return false;
diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h
index 845f42c76..167dc1337 100644
--- a/src/mesh/NodeDB.h
+++ b/src/mesh/NodeDB.h
@@ -18,68 +18,57 @@
#endif
#if !defined(MESHTASTIC_EXCLUDE_PKI)
-
-static const uint8_t LOW_ENTROPY_HASH1[] = {0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9,
- 0x08, 0x4f, 0x39, 0x63, 0xea, 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05,
- 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b};
-static const uint8_t LOW_ENTROPY_HASH2[] = {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00,
- 0x64, 0xa3, 0xa6, 0xfe, 0x71, 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d,
- 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9};
-static const uint8_t LOW_ENTROPY_HASH3[] = {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c,
- 0x45, 0x5e, 0x2e, 0xbd, 0x16, 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8,
- 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f};
-static const uint8_t LOW_ENTROPY_HASH4[] = {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef,
- 0x91, 0x18, 0x5a, 0x62, 0xe1, 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60,
- 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d};
-static const uint8_t LOW_ENTROPY_HASH5[] = {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a,
- 0x25, 0x64, 0x54, 0x6a, 0x6b, 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a,
- 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58};
-static const uint8_t LOW_ENTROPY_HASH6[] = {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7,
- 0xd9, 0xd1, 0x5d, 0xc4, 0x68, 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58,
- 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f};
-static const uint8_t LOW_ENTROPY_HASH7[] = {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47,
- 0x8e, 0x98, 0x9d, 0xc2, 0x3e, 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0,
- 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54};
-static const uint8_t LOW_ENTROPY_HASH8[] = {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5,
- 0x2b, 0x3a, 0xe2, 0x43, 0x42, 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca,
- 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49};
-static const uint8_t LOW_ENTROPY_HASH9[] = {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0,
- 0xf9, 0x55, 0xcf, 0x30, 0xa0, 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4,
- 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70};
-static const uint8_t LOW_ENTROPY_HASH10[] = {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd,
- 0x6c, 0x0a, 0x9b, 0x75, 0xa4, 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff,
- 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b};
-static const uint8_t LOW_ENTROPY_HASH11[] = {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9,
- 0x64, 0x82, 0xf0, 0x4d, 0x1b, 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e,
- 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49};
-static const uint8_t LOW_ENTROPY_HASH12[] = {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30,
- 0xa3, 0xc3, 0xe1, 0xcf, 0x97, 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf,
- 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e};
-static const uint8_t LOW_ENTROPY_HASH13[] = {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf,
- 0xff, 0x58, 0x17, 0x91, 0xaa, 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88,
- 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98};
-static const uint8_t LOW_ENTROPY_HASH14[] = {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72,
- 0xb4, 0x13, 0xd2, 0x01, 0x2f, 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0,
- 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0};
-static const uint8_t LOW_ENTROPY_HASH15[] = {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d,
- 0x03, 0xe5, 0x80, 0x59, 0xd3, 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6,
- 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64};
-static const uint8_t LOW_ENTROPY_HASH16[] = {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95,
- 0x2D, 0x3C, 0xDF, 0xB1, 0x22, 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0,
- 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF};
-static const uint8_t LOW_ENTROPY_HASH17[] = {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80,
- 0x26, 0x60, 0xF4, 0x6D, 0xCE, 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A,
- 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B};
-static const uint8_t LOW_ENTROPY_HASH18[] = {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE,
- 0x89, 0x36, 0x40, 0xC9, 0x99, 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24,
- 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5};
-static const uint8_t LOW_ENTROPY_HASH19[] = {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3,
- 0xA4, 0xC2, 0x19, 0xA3, 0x83, 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2,
- 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60};
-static const uint8_t LOW_ENTROPY_HASH20[] = {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35,
- 0x89, 0x9D, 0x0E, 0x24, 0xA1, 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6,
- 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4};
-static const char LOW_ENTROPY_WARNING[] = "Compromised keys detected, please regenerate.";
+// E3B0C442 is the blank hash
+static const uint8_t LOW_ENTROPY_HASHES[][32] = {
+ {0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, 0x08, 0x4f, 0x39, 0x63, 0xea,
+ 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b},
+ {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, 0x64, 0xa3, 0xa6, 0xfe, 0x71,
+ 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9},
+ {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, 0x45, 0x5e, 0x2e, 0xbd, 0x16,
+ 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f},
+ {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, 0x91, 0x18, 0x5a, 0x62, 0xe1,
+ 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d},
+ {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, 0x25, 0x64, 0x54, 0x6a, 0x6b,
+ 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58},
+ {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, 0xd9, 0xd1, 0x5d, 0xc4, 0x68,
+ 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f},
+ {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, 0x8e, 0x98, 0x9d, 0xc2, 0x3e,
+ 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54},
+ {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, 0x2b, 0x3a, 0xe2, 0x43, 0x42,
+ 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49},
+ {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, 0xf9, 0x55, 0xcf, 0x30, 0xa0,
+ 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70},
+ {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, 0x6c, 0x0a, 0x9b, 0x75, 0xa4,
+ 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b},
+ {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, 0x64, 0x82, 0xf0, 0x4d, 0x1b,
+ 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49},
+ {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, 0xa3, 0xc3, 0xe1, 0xcf, 0x97,
+ 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e},
+ {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, 0xff, 0x58, 0x17, 0x91, 0xaa,
+ 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98},
+ {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, 0xb4, 0x13, 0xd2, 0x01, 0x2f,
+ 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0},
+ {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, 0x03, 0xe5, 0x80, 0x59, 0xd3,
+ 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64},
+ {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, 0x2D, 0x3C, 0xDF, 0xB1, 0x22,
+ 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF},
+ {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, 0x26, 0x60, 0xF4, 0x6D, 0xCE,
+ 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B},
+ {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, 0x89, 0x36, 0x40, 0xC9, 0x99,
+ 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5},
+ {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, 0xA4, 0xC2, 0x19, 0xA3, 0x83,
+ 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60},
+ {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, 0x89, 0x9D, 0x0E, 0x24, 0xA1,
+ 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4},
+ {0x91, 0x70, 0xb4, 0x7c, 0xfb, 0xff, 0xa0, 0x59, 0x6a, 0x25, 0x1c, 0xa9, 0x9e, 0xe9, 0x43, 0x81,
+ 0x5d, 0x74, 0xb1, 0xb1, 0x09, 0x28, 0x00, 0x4a, 0xaf, 0xe3, 0xfc, 0xa9, 0x4e, 0x27, 0x76, 0x4c},
+ {0x85, 0xfe, 0x7c, 0xec, 0xb6, 0x78, 0x74, 0xc3, 0xec, 0xe1, 0x32, 0x7f, 0xb0, 0xb7, 0x02, 0x74,
+ 0xf9, 0x23, 0xd8, 0xe7, 0xfa, 0x14, 0xe6, 0xee, 0x66, 0x44, 0xb1, 0x8c, 0xa5, 0x2f, 0x7e, 0xd2},
+ {0x8e, 0x66, 0x65, 0x7b, 0x3b, 0x6f, 0x7e, 0xcc, 0x57, 0xb4, 0x57, 0xea, 0xcc, 0x83, 0xf5, 0xaa,
+ 0xf7, 0x65, 0xa3, 0xce, 0x93, 0x72, 0x13, 0xc1, 0xb6, 0x46, 0x7b, 0x29, 0x45, 0xb5, 0xc8, 0x93},
+ {0xcc, 0x11, 0xfb, 0x1a, 0xab, 0xa1, 0x31, 0x87, 0x6a, 0xc6, 0xde, 0x88, 0x87, 0xa9, 0xb9, 0x59,
+ 0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}};
+static const char LOW_ENTROPY_WARNING[] = "Compromised keys were detected and regenerated.";
#endif
/*
DeviceState versions used to be defined in the .proto file but really only this function cares. So changed to a
@@ -279,7 +268,7 @@ class NodeDB
bool hasValidPosition(const meshtastic_NodeInfoLite *n);
- bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t keyToTest);
+ bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest);
bool backupPreferences(meshtastic_AdminMessage_BackupLocation location);
bool restorePreferences(meshtastic_AdminMessage_BackupLocation location,
diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp
index 8cac31a3e..3902c1057 100644
--- a/src/mesh/PacketHistory.cpp
+++ b/src/mesh/PacketHistory.cpp
@@ -181,7 +181,7 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
}
/** Insert/Replace oldest PacketRecord in recentPackets. */
-void PacketHistory::insert(PacketRecord &r)
+void PacketHistory::insert(const PacketRecord &r)
{
uint32_t now_millis = millis(); // Should not jump with time changes
uint32_t OldtrxTimeMsec = 0;
@@ -308,7 +308,7 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N
return false;
}
- PacketRecord *found = find(sender, id);
+ const PacketRecord *found = find(sender, id);
if (found == NULL) {
#if VERBOSE_PACKET_HISTORY
@@ -327,7 +327,7 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N
/* Check if a certain node was a relayer of a packet in the history given iterator
* @return true if node was indeed a relayer, false if not */
-bool PacketHistory::wasRelayer(const uint8_t relayer, PacketRecord &r)
+bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r)
{
for (uint8_t i = 0; i < NUM_RELAYERS; i++) {
if (r.relayed_by[i] == relayer) {
diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h
index d06c9bd2f..9f14a4cf0 100644
--- a/src/mesh/PacketHistory.h
+++ b/src/mesh/PacketHistory.h
@@ -31,11 +31,11 @@ class PacketHistory
/** Insert/Replace oldest PacketRecord in mx_recentPackets.
* @param r PacketRecord to insert or replace */
- void insert(PacketRecord &r); // Insert or replace a packet record in the history
+ void insert(const PacketRecord &r); // Insert or replace a packet record in the history
/* Check if a certain node was a relayer of a packet in the history given iterator
* @return true if node was indeed a relayer, false if not */
- bool wasRelayer(const uint8_t relayer, PacketRecord &r);
+ bool wasRelayer(const uint8_t relayer, const PacketRecord &r);
PacketHistory(const PacketHistory &); // non construction-copyable
PacketHistory &operator=(const PacketHistory &); // non copyable
diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp
index 287de38fa..a3a8a2087 100644
--- a/src/mesh/PhoneAPI.cpp
+++ b/src/mesh/PhoneAPI.cpp
@@ -31,6 +31,9 @@
#include "Throttle.h"
#include
+// Flag to indicate a heartbeat was received and we should send queue status
+bool heartbeatReceived = false;
+
PhoneAPI::PhoneAPI()
{
lastContactMsec = millis();
@@ -155,6 +158,7 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
#endif
case meshtastic_ToRadio_heartbeat_tag:
LOG_DEBUG("Got client heartbeat");
+ heartbeatReceived = true;
break;
default:
// Ignore nop messages
@@ -188,6 +192,17 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
size_t PhoneAPI::getFromRadio(uint8_t *buf)
{
+ // Respond to heartbeat by sending queue status
+ if (heartbeatReceived) {
+ memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
+ fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag;
+ fromRadioScratch.queueStatus = router->getQueueStatus();
+ heartbeatReceived = false;
+ size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch);
+ LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes);
+ return numbytes;
+ }
+
if (!available()) {
return 0;
}
@@ -205,6 +220,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
// app not to send locations on our behalf.
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag;
strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env));
+ myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes());
fromRadioScratch.my_info = myNodeInfo;
state = STATE_SEND_UIDATA;
@@ -686,7 +702,8 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
LOG_WARN("Rate limit portnum %d", p.decoded.portnum);
meshtastic_QueueStatus qs = router->getQueueStatus();
service->sendQueueStatusToPhone(qs, 0, p.id);
- sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds");
+ service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p);
+ // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds");
return false;
}
lastPortNumToRadio[p.decoded.portnum] = millis();
diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp
index faa67a1c2..c210d5d48 100644
--- a/src/mesh/RadioInterface.cpp
+++ b/src/mesh/RadioInterface.cpp
@@ -67,6 +67,7 @@ const RegionInfo regions[] = {
/*
https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
+ Also used in Brazil.
*/
RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false),
@@ -169,6 +170,20 @@ const RegionInfo regions[] = {
*/
RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, true),
+ /*
+ Nepal
+ 865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
+ specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf
+ */
+ RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false),
+
+ /*
+ Brazil
+ 902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
+ https://github.com/meshtastic/firmware/issues/3741
+ */
+ RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false),
+
/*
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
*/
@@ -320,8 +335,9 @@ uint32_t RadioInterface::getTxDelayMsecWeighted(float snr)
void printPacket(const char *prefix, const meshtastic_MeshPacket *p)
{
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
- std::string out = DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id,
- p->from, p->to, p->want_ack, p->hop_limit, p->channel);
+ std::string out =
+ DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id,
+ p->from, p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel);
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
auto &s = p->decoded;
@@ -650,8 +666,10 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p)
{
- if (router)
+ if (router) {
+ p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
router->enqueueReceivedMessage(p);
+ }
}
/***
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 02968513c..c7e32c4a1 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -224,9 +224,10 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) {
float hourlyTxPercent = airTime->utilizationTXPercent();
if (hourlyTxPercent > myRegion->dutyCycle) {
-#ifdef DEBUG_PORT
uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle);
+
LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes);
+
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->has_reply_id = true;
cn->reply_id = p->id;
@@ -234,7 +235,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
cn->time = getValidTime(RTCQualityFromNet);
sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes);
service->sendClientNotification(cn);
-#endif
+
meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT;
if (isFromUs(p)) { // only send NAK to API, not to the mesh
abortSendAndNak(err, p);
@@ -522,12 +523,15 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// is not in the local nodedb
// First, only PKC encrypt packets we are originating
if (isFromUs(p) &&
- // Don't use PKC with simulator
- radioType != SIM_RADIO &&
+#if ARCH_PORTDUINO
+ // Sim radio via the cli flag skips PKC
+ !portduino_config.force_simradio &&
+#endif
// Don't use PKC with Ham mode
!owner.is_licensed &&
- // Don't use PKC if it's not explicitly requested and a non-primary channel is requested
- !(p->pki_encrypted != true && p->channel > 0) &&
+ // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested
+ !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 ||
+ strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) &&
// Check for valid keys and single node destination
config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr &&
// Check for a known public key for the destination
@@ -548,20 +552,6 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
numbytes += MESHTASTIC_PKC_OVERHEAD;
p->channel = 0;
p->pki_encrypted = true;
-
- // warn the user about a low entropy key
- if (nodeDB->keyIsLowEntropy) {
- LOG_WARN(LOW_ENTROPY_WARNING);
- if (!nodeDB->hasWarned) {
- meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
- cn->which_payload_variant = meshtastic_ClientNotification_low_entropy_key_tag;
- cn->level = meshtastic_LogRecord_Level_WARNING;
- cn->time = getValidTime(RTCQualityFromNet);
- sprintf(cn->message, LOW_ENTROPY_WARNING);
- service->sendClientNotification(cn);
- nodeDB->hasWarned = true;
- }
- }
} else {
if (p->pki_encrypted == true) {
// Client specifically requested PKI encryption
@@ -572,7 +562,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// Now that we are encrypting the packet channel should be the hash (no longer the index)
p->channel = hash;
if (hash < 0) {
- // No suitable channel could be found for sending
+ // No suitable channel could be found for
return meshtastic_Routing_Error_NO_CHANNEL;
}
crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes);
@@ -588,7 +578,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// Now that we are encrypting the packet channel should be the hash (no longer the index)
p->channel = hash;
if (hash < 0) {
- // No suitable channel could be found for sending
+ // No suitable channel could be found for
return meshtastic_Routing_Error_NO_CHANNEL;
}
crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes);
@@ -651,11 +641,12 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
shouldIgnoreNonstandardPorts = true;
#endif
if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
- IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_ATAK_FORWARDER, meshtastic_PortNum_ATAK_PLUGIN,
- meshtastic_PortNum_PAXCOUNTER_APP, meshtastic_PortNum_IP_TUNNEL_APP, meshtastic_PortNum_AUDIO_APP,
- meshtastic_PortNum_PRIVATE_APP, meshtastic_PortNum_DETECTION_SENSOR_APP, meshtastic_PortNum_RANGE_TEST_APP,
- meshtastic_PortNum_REMOTE_HARDWARE_APP)) {
- LOG_DEBUG("Ignore packet on blacklisted portnum for CORE_PORTNUMS_ONLY");
+ !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP,
+ meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP,
+ meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP,
+ meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP,
+ meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP)) {
+ LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY");
cancelSending(p->from, p->id);
skipHandle = true;
}
@@ -664,7 +655,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
}
// call modules here
- if (!skipHandle) {
+ // If this could be a spoofed packet, don't let the modules see it.
+ if (!skipHandle && p->from != nodeDB->getNodeNum()) {
MeshModule::callModules(*p, src);
#if !MESHTASTIC_EXCLUDE_MQTT
@@ -678,6 +670,8 @@ 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
diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp
index 4a42e5197..20026767e 100644
--- a/src/mesh/StreamAPI.cpp
+++ b/src/mesh/StreamAPI.cpp
@@ -16,6 +16,95 @@ int32_t StreamAPI::runOncePart()
return result;
}
+int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
+{
+ auto result = readStream(buf, bufLen);
+ writeStream();
+ checkConnectionTimeout();
+ return result;
+}
+
+/**
+ * Read any rx chars from the link and call handleRecStream
+ */
+int32_t StreamAPI::readStream(char *buf, uint16_t bufLen)
+{
+ if (bufLen < 1) {
+ // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time
+ bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000);
+ return recentRx ? 5 : 250;
+ } else {
+ handleRecStream(buf, bufLen);
+ // we had bytes available this time, so assume we might have them next time also
+ lastRxMsec = millis();
+ return 0;
+ }
+}
+
+/**
+ * call getFromRadio() and deliver encapsulated packets to the Stream
+ */
+void StreamAPI::writeStream()
+{
+ if (canWrite) {
+ uint32_t len;
+ do {
+ // Send every packet we can
+ len = getFromRadio(txBuf + HEADER_LEN);
+ emitTxBuffer(len);
+ } while (len);
+ }
+}
+
+int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen)
+{
+ uint16_t index = 0;
+ while (bufLen > index) { // Currently we never want to block
+ int cInt = buf[index++];
+ if (cInt < 0)
+ break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit
+ // arduino
+
+ uint8_t c = (uint8_t)cInt;
+
+ // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload
+ size_t ptr = rxPtr;
+
+ rxPtr++; // assume we will probably advance the rxPtr
+ rxBuf[ptr] = c; // store all bytes (including framing)
+
+ // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c);
+
+ if (ptr == 0) { // looking for START1
+ if (c != START1)
+ rxPtr = 0; // failed to find framing
+ } else if (ptr == 1) { // looking for START2
+ if (c != START2)
+ rxPtr = 0; // failed to find framing
+ } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing
+ uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing
+
+ // console->printf("len %d\n", len);
+
+ if (ptr == HEADER_LEN - 1) {
+ // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid
+ // protobuf also)
+ if (len > MAX_TO_FROM_RADIO_SIZE)
+ rxPtr = 0; // length is bogus, restart search for framing
+ }
+
+ if (rxPtr != 0) // Is packet still considered 'good'?
+ if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload?
+ rxPtr = 0; // start over again on the next packet
+
+ // If we didn't just fail the packet and we now have the right # of bytes, parse it
+ handleToRadio(rxBuf + HEADER_LEN, len);
+ }
+ }
+ }
+ return 0;
+}
+
/**
* Read any rx chars from the link and call handleToRadio
*/
@@ -76,21 +165,6 @@ int32_t StreamAPI::readStream()
}
}
-/**
- * call getFromRadio() and deliver encapsulated packets to the Stream
- */
-void StreamAPI::writeStream()
-{
- if (canWrite) {
- uint32_t len;
- do {
- // Send every packet we can
- len = getFromRadio(txBuf + HEADER_LEN);
- emitTxBuffer(len);
- } while (len);
- }
-}
-
/**
* Send the current txBuffer over our stream
*/
diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h
index 6e0364bc1..547dd0175 100644
--- a/src/mesh/StreamAPI.h
+++ b/src/mesh/StreamAPI.h
@@ -50,12 +50,15 @@ class StreamAPI : public PhoneAPI
* phone.
*/
virtual int32_t runOncePart();
+ virtual int32_t runOncePart(char *buf,uint16_t bufLen);
private:
/**
* Read any rx chars from the link and call handleToRadio
*/
int32_t readStream();
+ int32_t readStream(char *buf,uint16_t bufLen);
+ int32_t handleRecStream(char *buf,uint16_t bufLen);
/**
* call getFromRadio() and deliver encapsulated packets to the Stream
diff --git a/src/mesh/api/PacketAPI.cpp b/src/mesh/api/PacketAPI.cpp
index 4f0fbaf97..ab380d696 100644
--- a/src/mesh/api/PacketAPI.cpp
+++ b/src/mesh/api/PacketAPI.cpp
@@ -59,6 +59,7 @@ bool PacketAPI::receivePacket(void)
switch (mr->which_payload_variant) {
case meshtastic_ToRadio_packet_tag: {
meshtastic_MeshPacket *mp = &mr->packet;
+ mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API;
printPacket("PACKET FROM QUEUE", mp);
service->handleToRadio(*mp);
break;
@@ -70,7 +71,7 @@ bool PacketAPI::receivePacket(void)
break;
}
case meshtastic_ToRadio_heartbeat_tag:
- if (mr->heartbeat.dummy_field == 1) {
+ if (mr->heartbeat.nonce == 1) {
if (nodeInfoModule) {
LOG_INFO("Broadcasting nodeinfo ping");
nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true);
diff --git a/src/mesh/api/WiFiServerAPI.cpp b/src/mesh/api/WiFiServerAPI.cpp
index 5b63bc165..b19194f78 100644
--- a/src/mesh/api/WiFiServerAPI.cpp
+++ b/src/mesh/api/WiFiServerAPI.cpp
@@ -17,7 +17,10 @@ void initApiServer(int port)
}
void deInitApiServer()
{
- delete apiPort;
+ if (apiPort) {
+ delete apiPort;
+ apiPort = nullptr;
+ }
}
WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client)
diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp
index 9c92a6c27..2b4f63512 100644
--- a/src/mesh/eth/ethClient.cpp
+++ b/src/mesh/eth/ethClient.cpp
@@ -68,6 +68,11 @@ static int32_t reconnectETH()
initApiServer();
}
#endif
+#if HAS_UDP_MULTICAST
+ if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) {
+ udpHandler->start();
+ }
+#endif
ethStartupComplete = true;
}
diff --git a/src/mesh/eth/ethClient.h b/src/mesh/eth/ethClient.h
index 9e1745b9f..3adf481d2 100644
--- a/src/mesh/eth/ethClient.h
+++ b/src/mesh/eth/ethClient.h
@@ -2,7 +2,6 @@
#include "configuration.h"
#include
-#include
bool initEthernet();
bool isEthernetAvailable();
diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h
index 071640b0d..bc0b780b9 100644
--- a/src/mesh/generated/meshtastic/admin.pb.h
+++ b/src/mesh/generated/meshtastic/admin.pb.h
@@ -7,9 +7,9 @@
#include "meshtastic/channel.pb.h"
#include "meshtastic/config.pb.h"
#include "meshtastic/connection_status.pb.h"
+#include "meshtastic/device_ui.pb.h"
#include "meshtastic/mesh.pb.h"
#include "meshtastic/module_config.pb.h"
-#include "meshtastic/device_ui.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index f28daadbd..67d461611 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -102,7 +102,11 @@ typedef enum _meshtastic_Config_DeviceConfig_BuzzerMode {
meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY = 2,
/* Non-notification system buzzer tones only.
Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. */
- meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY = 3
+ meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY = 3,
+ /* Direct Message notifications only.
+ Buzzer is enabled only for direct messages and alerts, but not for button presses.
+ External notification config determines the specifics of the notification behavior. */
+ meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY = 4
} meshtastic_Config_DeviceConfig_BuzzerMode;
/* Bit field of boolean configuration options, indicating which optional
@@ -203,10 +207,10 @@ typedef enum _meshtastic_Config_DisplayConfig_OledType {
meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306 = 1,
/* Default / Autodetect */
meshtastic_Config_DisplayConfig_OledType_OLED_SH1106 = 2,
- /* Can not be auto detected but set by proto. Used for 128x128 screens */
- meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3,
/* Can not be auto detected but set by proto. Used for 128x64 screens */
- meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64 = 4
+ meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3,
+ /* Can not be auto detected but set by proto. Used for 128x128 screens */
+ meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4
} meshtastic_Config_DisplayConfig_OledType;
typedef enum _meshtastic_Config_DisplayConfig_DisplayMode {
@@ -289,7 +293,11 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
/* Kazakhstan 433MHz */
meshtastic_Config_LoRaConfig_RegionCode_KZ_433 = 23,
/* Kazakhstan 863MHz */
- meshtastic_Config_LoRaConfig_RegionCode_KZ_863 = 24
+ meshtastic_Config_LoRaConfig_RegionCode_KZ_863 = 24,
+ /* Nepal 865MHz */
+ meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
+ /* Brazil 902MHz */
+ meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26
} meshtastic_Config_LoRaConfig_RegionCode;
/* Standard predefined channel settings
@@ -476,7 +484,8 @@ typedef struct _meshtastic_Config_DisplayConfig {
/* Number of seconds the screen stays on after pressing the user button or receiving a message
0 for default of one minute MAXUINT for always on */
uint32_t screen_on_secs;
- /* How the GPS coordinates are formatted on the OLED screen. */
+ /* Deprecated in 2.7.4: Unused
+ How the GPS coordinates are formatted on the OLED screen. */
meshtastic_Config_DisplayConfig_GpsCoordinateFormat gps_format;
/* Automatically toggles to the next page on the screen like a carousel, based the specified interval in seconds.
Potentially useful for devices without user buttons. */
@@ -645,8 +654,8 @@ extern "C" {
#define _meshtastic_Config_DeviceConfig_RebroadcastMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_RebroadcastMode)(meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY+1))
#define _meshtastic_Config_DeviceConfig_BuzzerMode_MIN meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED
-#define _meshtastic_Config_DeviceConfig_BuzzerMode_MAX meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY
-#define _meshtastic_Config_DeviceConfig_BuzzerMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_BuzzerMode)(meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY+1))
+#define _meshtastic_Config_DeviceConfig_BuzzerMode_MAX meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY
+#define _meshtastic_Config_DeviceConfig_BuzzerMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_BuzzerMode)(meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY+1))
#define _meshtastic_Config_PositionConfig_PositionFlags_MIN meshtastic_Config_PositionConfig_PositionFlags_UNSET
#define _meshtastic_Config_PositionConfig_PositionFlags_MAX meshtastic_Config_PositionConfig_PositionFlags_SPEED
@@ -673,8 +682,8 @@ extern "C" {
#define _meshtastic_Config_DisplayConfig_DisplayUnits_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayUnits)(meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL+1))
#define _meshtastic_Config_DisplayConfig_OledType_MIN meshtastic_Config_DisplayConfig_OledType_OLED_AUTO
-#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64
-#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64+1))
+#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128
+#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128+1))
#define _meshtastic_Config_DisplayConfig_DisplayMode_MIN meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT
#define _meshtastic_Config_DisplayConfig_DisplayMode_MAX meshtastic_Config_DisplayConfig_DisplayMode_COLOR
@@ -685,8 +694,8 @@ extern "C" {
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
-#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_KZ_863
-#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_KZ_863+1))
+#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902
+#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO
diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h
index f78689cb2..f47091384 100644
--- a/src/mesh/generated/meshtastic/deviceonly.pb.h
+++ b/src/mesh/generated/meshtastic/deviceonly.pb.h
@@ -6,10 +6,10 @@
#include
#include
#include "meshtastic/channel.pb.h"
-#include "meshtastic/mesh.pb.h"
-#include "meshtastic/telemetry.pb.h"
#include "meshtastic/config.pb.h"
#include "meshtastic/localonly.pb.h"
+#include "meshtastic/mesh.pb.h"
+#include "meshtastic/telemetry.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
@@ -362,7 +362,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2271
#define meshtastic_ChannelFile_size 718
-#define meshtastic_DeviceState_size 1722
+#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196
#define meshtastic_PositionLite_size 28
#define meshtastic_UserLite_size 98
diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp
index 361d01b9a..9966e52f8 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.cpp
+++ b/src/mesh/generated/meshtastic/mesh.pb.cpp
@@ -115,6 +115,10 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
+
+
+
+
diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h
index 9e0415198..ce3722aa7 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.h
+++ b/src/mesh/generated/meshtastic/mesh.pb.h
@@ -6,11 +6,11 @@
#include
#include "meshtastic/channel.pb.h"
#include "meshtastic/config.pb.h"
+#include "meshtastic/device_ui.pb.h"
#include "meshtastic/module_config.pb.h"
#include "meshtastic/portnums.pb.h"
#include "meshtastic/telemetry.pb.h"
#include "meshtastic/xmodem.pb.h"
-#include "meshtastic/device_ui.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
@@ -247,32 +247,31 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO = 96,
/* Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin */
meshtastic_HardwareModel_CROWPANEL = 97,
- /* *
- Lilygo LINK32 board with sensors */
+ /* Lilygo LINK32 board with sensors */
meshtastic_HardwareModel_LINK_32 = 98,
- /* *
- Seeed Tracker L1 */
+ /* Seeed Tracker L1 */
meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 = 99,
- /* *
- Seeed Tracker L1 EINK driver */
+ /* Seeed Tracker L1 EINK driver */
meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK = 100,
/* Reserved ID for future and past use */
meshtastic_HardwareModel_QWANTZ_TINY_ARMS = 101,
- /* *
- Lilygo T-Deck Pro */
+ /* Lilygo T-Deck Pro */
meshtastic_HardwareModel_T_DECK_PRO = 102,
- /* *
- Lilygo TLora Pager */
+ /* Lilygo TLora Pager */
meshtastic_HardwareModel_T_LORA_PAGER = 103,
- /* *
- GAT562 Mesh Trial Tracker */
+ /* GAT562 Mesh Trial Tracker */
meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104,
- /* *
- RAKwireless WisMesh Tag */
+ /* RAKwireless WisMesh Tag */
meshtastic_HardwareModel_WISMESH_TAG = 105,
- /* *
- RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */
+ /* RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */
meshtastic_HardwareModel_RAK3312 = 106,
+ /* Elecrow ThinkNode M5 https://www.elecrow.com/wiki/ThinkNode_M5_Meshtastic_LoRa_Signal_Transceiver_ESP32-S3.html */
+ meshtastic_HardwareModel_THINKNODE_M5 = 107,
+ /* MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices.
+ https://heltec.org/project/meshsolar/ */
+ meshtastic_HardwareModel_HELTEC_MESH_SOLAR = 108,
+ /* Lilygo T-Echo Lite */
+ meshtastic_HardwareModel_T_ECHO_LITE = 109,
/* ------------------------------------------------------------------------------------------------------------------------------------------
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.
------------------------------------------------------------------------------------------------------------------------------------------ */
@@ -331,6 +330,25 @@ typedef enum _meshtastic_CriticalErrorCode {
meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE = 13
} meshtastic_CriticalErrorCode;
+/* Enum to indicate to clients whether this firmware is a special firmware build, like an event.
+ The first 16 values are reserved for non-event special firmwares, like the Smart Citizen use case. */
+typedef enum _meshtastic_FirmwareEdition {
+ /* Vanilla firmware */
+ meshtastic_FirmwareEdition_VANILLA = 0,
+ /* Firmware for use in the Smart Citizen environmental monitoring network */
+ meshtastic_FirmwareEdition_SMART_CITIZEN = 1,
+ /* Open Sauce, the maker conference held yearly in CA */
+ meshtastic_FirmwareEdition_OPEN_SAUCE = 16,
+ /* DEFCON, the yearly hacker conference */
+ meshtastic_FirmwareEdition_DEFCON = 17,
+ /* Burning Man, the yearly hippie gathering in the desert */
+ meshtastic_FirmwareEdition_BURNING_MAN = 18,
+ /* Hamvention, the Dayton amateur radio convention */
+ meshtastic_FirmwareEdition_HAMVENTION = 19,
+ /* Placeholder for DIY and unofficial events */
+ meshtastic_FirmwareEdition_DIY_EDITION = 127
+} meshtastic_FirmwareEdition;
+
/* Enum for modules excluded from a device's configuration.
Each value represents a ModuleConfigType that can be toggled as excluded
by setting its corresponding bit in the `excluded_modules` bitmask field. */
@@ -432,7 +450,10 @@ typedef enum _meshtastic_Routing_Error {
/* Admin packet otherwise checks out, but uses a bogus or expired session key */
meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY = 36,
/* Admin packet sent using PKC, but not from a public key on the admin key list */
- meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37
+ meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37,
+ /* Airtime fairness rate limit exceeded for a packet
+ This typically enforced per portnum and is used to prevent a single node from monopolizing airtime */
+ meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38
} meshtastic_Routing_Error;
/* The priority of this message for sending.
@@ -490,6 +511,26 @@ typedef enum _meshtastic_MeshPacket_Delayed {
meshtastic_MeshPacket_Delayed_DELAYED_DIRECT = 2
} meshtastic_MeshPacket_Delayed;
+/* Enum to identify which transport mechanism this packet arrived over */
+typedef enum _meshtastic_MeshPacket_TransportMechanism {
+ /* The default case is that the node generated a packet itself */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL = 0,
+ /* Arrived via the primary LoRa radio */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA = 1,
+ /* Arrived via a secondary LoRa radio */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA_ALT1 = 2,
+ /* Arrived via a tertiary LoRa radio */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA_ALT2 = 3,
+ /* Arrived via a quaternary LoRa radio */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA_ALT3 = 4,
+ /* Arrived via an MQTT connection */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT = 5,
+ /* Arrived via Multicast UDP */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP = 6,
+ /* Arrived via API connection */
+ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API = 7
+} meshtastic_MeshPacket_TransportMechanism;
+
/* Log levels, chosen to match python logging conventions. */
typedef enum _meshtastic_LogRecord_Level {
/* Log levels, chosen to match python logging conventions. */
@@ -844,6 +885,8 @@ typedef struct _meshtastic_MeshPacket {
Timestamp after which this packet may be sent.
Set by the firmware internally, clients are not supposed to set this. */
uint32_t tx_after;
+ /* Indicates which transport mechanism this packet arrived over */
+ meshtastic_MeshPacket_TransportMechanism transport_mechanism;
} meshtastic_MeshPacket;
/* The bluetooth to device link:
@@ -917,6 +960,11 @@ typedef struct _meshtastic_MyNodeInfo {
meshtastic_MyNodeInfo_device_id_t device_id;
/* The PlatformIO environment used to build this firmware */
char pio_env[40];
+ /* The indicator for whether this device is running event firmware and which */
+ meshtastic_FirmwareEdition firmware_edition;
+ /* The number of nodes in the nodedb.
+ This is used by the phone to know how many NodeInfo packets to expect on want_config */
+ uint16_t nodedb_count;
} meshtastic_MyNodeInfo;
/* Debug output from the device.
@@ -1125,7 +1173,8 @@ typedef struct _meshtastic_FromRadio {
/* A heartbeat message is sent to the node from the client to keep the connection alive.
This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. */
typedef struct _meshtastic_Heartbeat {
- char dummy_field;
+ /* The nonce of the heartbeat message */
+ uint32_t nonce;
} meshtastic_Heartbeat;
/* Packets/commands to the radio will be written (reliably) to the toRadio characteristic.
@@ -1215,6 +1264,10 @@ extern "C" {
#define _meshtastic_CriticalErrorCode_MAX meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE
#define _meshtastic_CriticalErrorCode_ARRAYSIZE ((meshtastic_CriticalErrorCode)(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE+1))
+#define _meshtastic_FirmwareEdition_MIN meshtastic_FirmwareEdition_VANILLA
+#define _meshtastic_FirmwareEdition_MAX meshtastic_FirmwareEdition_DIY_EDITION
+#define _meshtastic_FirmwareEdition_ARRAYSIZE ((meshtastic_FirmwareEdition)(meshtastic_FirmwareEdition_DIY_EDITION+1))
+
#define _meshtastic_ExcludedModules_MIN meshtastic_ExcludedModules_EXCLUDED_NONE
#define _meshtastic_ExcludedModules_MAX meshtastic_ExcludedModules_NETWORK_CONFIG
#define _meshtastic_ExcludedModules_ARRAYSIZE ((meshtastic_ExcludedModules)(meshtastic_ExcludedModules_NETWORK_CONFIG+1))
@@ -1228,8 +1281,8 @@ extern "C" {
#define _meshtastic_Position_AltSource_ARRAYSIZE ((meshtastic_Position_AltSource)(meshtastic_Position_AltSource_ALT_BAROMETRIC+1))
#define _meshtastic_Routing_Error_MIN meshtastic_Routing_Error_NONE
-#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED
-#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED+1))
+#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED
+#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1))
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
@@ -1239,6 +1292,10 @@ extern "C" {
#define _meshtastic_MeshPacket_Delayed_MAX meshtastic_MeshPacket_Delayed_DELAYED_DIRECT
#define _meshtastic_MeshPacket_Delayed_ARRAYSIZE ((meshtastic_MeshPacket_Delayed)(meshtastic_MeshPacket_Delayed_DELAYED_DIRECT+1))
+#define _meshtastic_MeshPacket_TransportMechanism_MIN meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL
+#define _meshtastic_MeshPacket_TransportMechanism_MAX meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API
+#define _meshtastic_MeshPacket_TransportMechanism_ARRAYSIZE ((meshtastic_MeshPacket_TransportMechanism)(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API+1))
+
#define _meshtastic_LogRecord_Level_MIN meshtastic_LogRecord_Level_UNSET
#define _meshtastic_LogRecord_Level_MAX meshtastic_LogRecord_Level_CRITICAL
#define _meshtastic_LogRecord_Level_ARRAYSIZE ((meshtastic_LogRecord_Level)(meshtastic_LogRecord_Level_CRITICAL+1))
@@ -1259,8 +1316,10 @@ extern "C" {
#define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority
#define meshtastic_MeshPacket_delayed_ENUMTYPE meshtastic_MeshPacket_Delayed
+#define meshtastic_MeshPacket_transport_mechanism_ENUMTYPE meshtastic_MeshPacket_TransportMechanism
+#define meshtastic_MyNodeInfo_firmware_edition_ENUMTYPE meshtastic_FirmwareEdition
#define meshtastic_LogRecord_level_ENUMTYPE meshtastic_LogRecord_Level
@@ -1297,9 +1356,9 @@ extern "C" {
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
-#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0}
+#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0}
-#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, ""}
+#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
#define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN}
#define meshtastic_QueueStatus_init_default {0, 0, 0, 0}
#define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}}
@@ -1328,9 +1387,9 @@ extern "C" {
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
-#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0}
+#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0}
-#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, ""}
+#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
#define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN}
#define meshtastic_QueueStatus_init_zero {0, 0, 0, 0}
#define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}}
@@ -1436,6 +1495,7 @@ extern "C" {
#define meshtastic_MeshPacket_next_hop_tag 18
#define meshtastic_MeshPacket_relay_node_tag 19
#define meshtastic_MeshPacket_tx_after_tag 20
+#define meshtastic_MeshPacket_transport_mechanism_tag 21
#define meshtastic_NodeInfo_num_tag 1
#define meshtastic_NodeInfo_user_tag 2
#define meshtastic_NodeInfo_position_tag 3
@@ -1453,6 +1513,8 @@ extern "C" {
#define meshtastic_MyNodeInfo_min_app_version_tag 11
#define meshtastic_MyNodeInfo_device_id_tag 12
#define meshtastic_MyNodeInfo_pio_env_tag 13
+#define meshtastic_MyNodeInfo_firmware_edition_tag 14
+#define meshtastic_MyNodeInfo_nodedb_count_tag 15
#define meshtastic_LogRecord_message_tag 1
#define meshtastic_LogRecord_time_tag 2
#define meshtastic_LogRecord_source_tag 3
@@ -1520,6 +1582,7 @@ extern "C" {
#define meshtastic_FromRadio_fileInfo_tag 15
#define meshtastic_FromRadio_clientNotification_tag 16
#define meshtastic_FromRadio_deviceuiConfig_tag 17
+#define meshtastic_Heartbeat_nonce_tag 1
#define meshtastic_ToRadio_packet_tag 1
#define meshtastic_ToRadio_want_config_id_tag 3
#define meshtastic_ToRadio_disconnect_tag 4
@@ -1656,7 +1719,8 @@ X(a, STATIC, SINGULAR, BYTES, public_key, 16) \
X(a, STATIC, SINGULAR, BOOL, pki_encrypted, 17) \
X(a, STATIC, SINGULAR, UINT32, next_hop, 18) \
X(a, STATIC, SINGULAR, UINT32, relay_node, 19) \
-X(a, STATIC, SINGULAR, UINT32, tx_after, 20)
+X(a, STATIC, SINGULAR, UINT32, tx_after, 20) \
+X(a, STATIC, SINGULAR, UENUM, transport_mechanism, 21)
#define meshtastic_MeshPacket_CALLBACK NULL
#define meshtastic_MeshPacket_DEFAULT NULL
#define meshtastic_MeshPacket_payload_variant_decoded_MSGTYPE meshtastic_Data
@@ -1685,7 +1749,9 @@ X(a, STATIC, SINGULAR, UINT32, my_node_num, 1) \
X(a, STATIC, SINGULAR, UINT32, reboot_count, 8) \
X(a, STATIC, SINGULAR, UINT32, min_app_version, 11) \
X(a, STATIC, SINGULAR, BYTES, device_id, 12) \
-X(a, STATIC, SINGULAR, STRING, pio_env, 13)
+X(a, STATIC, SINGULAR, STRING, pio_env, 13) \
+X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) \
+X(a, STATIC, SINGULAR, UINT32, nodedb_count, 15)
#define meshtastic_MyNodeInfo_CALLBACK NULL
#define meshtastic_MyNodeInfo_DEFAULT NULL
@@ -1849,7 +1915,7 @@ X(a, STATIC, SINGULAR, UINT32, excluded_modules, 12)
#define meshtastic_DeviceMetadata_DEFAULT NULL
#define meshtastic_Heartbeat_FIELDLIST(X, a) \
-
+X(a, STATIC, SINGULAR, UINT32, nonce, 1)
#define meshtastic_Heartbeat_CALLBACK NULL
#define meshtastic_Heartbeat_DEFAULT NULL
@@ -1959,16 +2025,16 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_DuplicatedPublicKey_size 0
#define meshtastic_FileInfo_size 236
#define meshtastic_FromRadio_size 510
-#define meshtastic_Heartbeat_size 0
+#define meshtastic_Heartbeat_size 6
#define meshtastic_KeyVerificationFinal_size 65
#define meshtastic_KeyVerificationNumberInform_size 58
#define meshtastic_KeyVerificationNumberRequest_size 52
#define meshtastic_KeyVerification_size 79
#define meshtastic_LogRecord_size 426
#define meshtastic_LowEntropyKey_size 0
-#define meshtastic_MeshPacket_size 378
+#define meshtastic_MeshPacket_size 381
#define meshtastic_MqttClientProxyMessage_size 501
-#define meshtastic_MyNodeInfo_size 77
+#define meshtastic_MyNodeInfo_size 83
#define meshtastic_NeighborInfo_size 258
#define meshtastic_Neighbor_size 22
#define meshtastic_NodeInfo_size 323
diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h
index e8ae48072..b27f5f515 100644
--- a/src/mesh/generated/meshtastic/module_config.pb.h
+++ b/src/mesh/generated/meshtastic/module_config.pb.h
@@ -82,7 +82,10 @@ typedef enum _meshtastic_ModuleConfig_SerialConfig_Serial_Mode {
meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85 = 6,
/* VE.Direct is a serial protocol used by Victron Energy products
https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable */
- meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT = 7
+ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT = 7,
+ /* Used to configure and view some parameters of MeshSolar.
+https://heltec.org/project/meshsolar/ */
+ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG = 8
} meshtastic_ModuleConfig_SerialConfig_Serial_Mode;
/* TODO: REPLACE */
@@ -472,8 +475,8 @@ extern "C" {
#define _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Baud)(meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600+1))
#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT
-#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT
-#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT+1))
+#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG
+#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG+1))
#define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE
#define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MAX meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK
diff --git a/src/mesh/generated/meshtastic/powermon.pb.h b/src/mesh/generated/meshtastic/powermon.pb.h
index 9d4d94193..3072b8ac5 100644
--- a/src/mesh/generated/meshtastic/powermon.pb.h
+++ b/src/mesh/generated/meshtastic/powermon.pb.h
@@ -11,7 +11,7 @@
/* Enum definitions */
/* Any significant power changing event in meshtastic should be tagged with a powermon state transition.
-If you are making new meshtastic features feel free to add new entries at the end of this definition. */
+ If you are making new meshtastic features feel free to add new entries at the end of this definition. */
typedef enum _meshtastic_PowerMon_State {
meshtastic_PowerMon_State_None = 0,
meshtastic_PowerMon_State_CPU_DeepSleep = 1,
@@ -34,13 +34,13 @@ something like "S:PM:C,0x00001234,REASON" where the hex number is the bitmask of
meshtastic_PowerMon_State_Screen_Drawing = 512,
meshtastic_PowerMon_State_Wifi_On = 1024,
/* GPS is actively trying to find our location
-See GPSPowerState for more details */
+ See GPSPowerState for more details */
meshtastic_PowerMon_State_GPS_Active = 2048
} meshtastic_PowerMon_State;
/* What operation would we like the UUT to perform.
-note: senders should probably set want_response in their request packets, so that they can know when the state
-machine has started processing their request */
+ note: senders should probably set want_response in their request packets, so that they can know when the state
+ machine has started processing their request */
typedef enum _meshtastic_PowerStressMessage_Opcode {
/* Unset/unused */
meshtastic_PowerStressMessage_Opcode_UNSET = 0,
@@ -67,7 +67,7 @@ typedef enum _meshtastic_PowerStressMessage_Opcode {
/* Struct definitions */
/* Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs).
-But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) */
+ But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) */
typedef struct _meshtastic_PowerMon {
char dummy_field;
} meshtastic_PowerMon;
diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h
index 072a99a24..9af095e78 100644
--- a/src/mesh/generated/meshtastic/telemetry.pb.h
+++ b/src/mesh/generated/meshtastic/telemetry.pb.h
@@ -93,7 +93,15 @@ typedef enum _meshtastic_TelemetrySensorType {
/* PCT2075 Temperature Sensor */
meshtastic_TelemetrySensorType_PCT2075 = 39,
/* ADS1X15 ADC */
- meshtastic_TelemetrySensorType_ADS1X15 = 40
+ meshtastic_TelemetrySensorType_ADS1X15 = 40,
+ /* ADS1X15 ADC_ALT */
+ meshtastic_TelemetrySensorType_ADS1X15_ALT = 41,
+ /* Sensirion SFA30 Formaldehyde sensor */
+ meshtastic_TelemetrySensorType_SFA30 = 42,
+ /* SEN5X PM SENSORS */
+ meshtastic_TelemetrySensorType_SEN5X = 43,
+ /* TSL2561 light sensor */
+ meshtastic_TelemetrySensorType_TSL2561 = 44
} meshtastic_TelemetrySensorType;
/* Struct definitions */
@@ -242,40 +250,40 @@ typedef struct _meshtastic_PowerMetrics {
/* Air quality metrics */
typedef struct _meshtastic_AirQualityMetrics {
- /* Concentration Units Standard PM1.0 */
+ /* Concentration Units Standard PM1.0 in ug/m3 */
bool has_pm10_standard;
uint32_t pm10_standard;
- /* Concentration Units Standard PM2.5 */
+ /* Concentration Units Standard PM2.5 in ug/m3 */
bool has_pm25_standard;
uint32_t pm25_standard;
- /* Concentration Units Standard PM10.0 */
+ /* Concentration Units Standard PM10.0 in ug/m3 */
bool has_pm100_standard;
uint32_t pm100_standard;
- /* Concentration Units Environmental PM1.0 */
+ /* Concentration Units Environmental PM1.0 in ug/m3 */
bool has_pm10_environmental;
uint32_t pm10_environmental;
- /* Concentration Units Environmental PM2.5 */
+ /* Concentration Units Environmental PM2.5 in ug/m3 */
bool has_pm25_environmental;
uint32_t pm25_environmental;
- /* Concentration Units Environmental PM10.0 */
+ /* Concentration Units Environmental PM10.0 in ug/m3 */
bool has_pm100_environmental;
uint32_t pm100_environmental;
- /* 0.3um Particle Count */
+ /* 0.3um Particle Count in #/0.1l */
bool has_particles_03um;
uint32_t particles_03um;
- /* 0.5um Particle Count */
+ /* 0.5um Particle Count in #/0.1l */
bool has_particles_05um;
uint32_t particles_05um;
- /* 1.0um Particle Count */
+ /* 1.0um Particle Count in #/0.1l */
bool has_particles_10um;
uint32_t particles_10um;
- /* 2.5um Particle Count */
+ /* 2.5um Particle Count in #/0.1l */
bool has_particles_25um;
uint32_t particles_25um;
- /* 5.0um Particle Count */
+ /* 5.0um Particle Count in #/0.1l */
bool has_particles_50um;
uint32_t particles_50um;
- /* 10.0um Particle Count */
+ /* 10.0um Particle Count in #/0.1l */
bool has_particles_100um;
uint32_t particles_100um;
/* CO2 concentration in ppm */
@@ -287,6 +295,36 @@ typedef struct _meshtastic_AirQualityMetrics {
/* CO2 sensor relative humidity in % */
bool has_co2_humidity;
float co2_humidity;
+ /* Formaldehyde sensor formaldehyde concentration in ppb */
+ bool has_form_formaldehyde;
+ float form_formaldehyde;
+ /* Formaldehyde sensor relative humidity in %RH */
+ bool has_form_humidity;
+ float form_humidity;
+ /* Formaldehyde sensor temperature in degrees Celsius */
+ bool has_form_temperature;
+ float form_temperature;
+ /* Concentration Units Standard PM4.0 in ug/m3 */
+ bool has_pm40_standard;
+ uint32_t pm40_standard;
+ /* 4.0um Particle Count in #/0.1l */
+ bool has_particles_40um;
+ uint32_t particles_40um;
+ /* PM Sensor Temperature */
+ bool has_pm_temperature;
+ float pm_temperature;
+ /* PM Sensor humidity */
+ bool has_pm_humidity;
+ float pm_humidity;
+ /* PM Sensor VOC Index */
+ bool has_pm_voc_idx;
+ float pm_voc_idx;
+ /* PM Sensor NOx Index */
+ bool has_pm_nox_idx;
+ float pm_nox_idx;
+ /* Typical Particle Size in um */
+ bool has_particles_tps;
+ float particles_tps;
} meshtastic_AirQualityMetrics;
/* Local device mesh statistics */
@@ -398,8 +436,8 @@ extern "C" {
/* Helper constants for enums */
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
-#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_ADS1X15
-#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_ADS1X15+1))
+#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_TSL2561
+#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_TSL2561+1))
@@ -415,7 +453,7 @@ extern "C" {
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
-#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
+#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0}
#define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""}
@@ -424,7 +462,7 @@ extern "C" {
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
-#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
+#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0}
#define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""}
@@ -490,6 +528,16 @@ extern "C" {
#define meshtastic_AirQualityMetrics_co2_tag 13
#define meshtastic_AirQualityMetrics_co2_temperature_tag 14
#define meshtastic_AirQualityMetrics_co2_humidity_tag 15
+#define meshtastic_AirQualityMetrics_form_formaldehyde_tag 16
+#define meshtastic_AirQualityMetrics_form_humidity_tag 17
+#define meshtastic_AirQualityMetrics_form_temperature_tag 18
+#define meshtastic_AirQualityMetrics_pm40_standard_tag 19
+#define meshtastic_AirQualityMetrics_particles_40um_tag 20
+#define meshtastic_AirQualityMetrics_pm_temperature_tag 21
+#define meshtastic_AirQualityMetrics_pm_humidity_tag 22
+#define meshtastic_AirQualityMetrics_pm_voc_idx_tag 23
+#define meshtastic_AirQualityMetrics_pm_nox_idx_tag 24
+#define meshtastic_AirQualityMetrics_particles_tps_tag 25
#define meshtastic_LocalStats_uptime_seconds_tag 1
#define meshtastic_LocalStats_channel_utilization_tag 2
#define meshtastic_LocalStats_air_util_tx_tag 3
@@ -597,7 +645,17 @@ X(a, STATIC, OPTIONAL, UINT32, particles_50um, 11) \
X(a, STATIC, OPTIONAL, UINT32, particles_100um, 12) \
X(a, STATIC, OPTIONAL, UINT32, co2, 13) \
X(a, STATIC, OPTIONAL, FLOAT, co2_temperature, 14) \
-X(a, STATIC, OPTIONAL, FLOAT, co2_humidity, 15)
+X(a, STATIC, OPTIONAL, FLOAT, co2_humidity, 15) \
+X(a, STATIC, OPTIONAL, FLOAT, form_formaldehyde, 16) \
+X(a, STATIC, OPTIONAL, FLOAT, form_humidity, 17) \
+X(a, STATIC, OPTIONAL, FLOAT, form_temperature, 18) \
+X(a, STATIC, OPTIONAL, UINT32, pm40_standard, 19) \
+X(a, STATIC, OPTIONAL, UINT32, particles_40um, 20) \
+X(a, STATIC, OPTIONAL, FLOAT, pm_temperature, 21) \
+X(a, STATIC, OPTIONAL, FLOAT, pm_humidity, 22) \
+X(a, STATIC, OPTIONAL, FLOAT, pm_voc_idx, 23) \
+X(a, STATIC, OPTIONAL, FLOAT, pm_nox_idx, 24) \
+X(a, STATIC, OPTIONAL, FLOAT, particles_tps, 25)
#define meshtastic_AirQualityMetrics_CALLBACK NULL
#define meshtastic_AirQualityMetrics_DEFAULT NULL
@@ -686,7 +744,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size
-#define meshtastic_AirQualityMetrics_size 88
+#define meshtastic_AirQualityMetrics_size 150
#define meshtastic_DeviceMetrics_size 27
#define meshtastic_EnvironmentMetrics_size 113
#define meshtastic_HealthMetrics_size 11
diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h
index d1cc1065c..9650668a8 100644
--- a/src/mesh/udp/UdpMulticastHandler.h
+++ b/src/mesh/udp/UdpMulticastHandler.h
@@ -4,8 +4,13 @@
#include "main.h"
#include "mesh/Router.h"
-#include
+#if HAS_ETHERNET && defined(ARCH_NRF52)
+#include "mesh/eth/ethClient.h"
+#else
#include
+#endif
+
+#include
#if HAS_ETHERNET && defined(USE_WS5500)
#include
@@ -22,11 +27,11 @@ class UdpMulticastHandler final
void start()
{
if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) {
-#ifndef ARCH_PORTDUINO
- // FIXME(PORTDUINO): arduino lacks IPAddress::toString()
- LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str());
+#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
+ LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3],
+ UDP_MULTICAST_DEFAUL_PORT);
#else
- LOG_DEBUG("UDP Listening");
+ LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str());
#endif
udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); });
} else {
@@ -37,11 +42,15 @@ class UdpMulticastHandler final
void onReceive(AsyncUDPPacket packet)
{
size_t packetLength = packet.length();
-#ifndef ARCH_PORTDUINO
+#if defined(ARCH_NRF52)
+ IPAddress ip = packet.remoteIP();
+ LOG_DEBUG("UDP broadcast from: %u.%u.%u.%u, len=%u", ip[0], ip[1], ip[2], ip[3], packetLength);
+#elif !defined(ARCH_PORTDUINO)
// FIXME(PORTDUINO): arduino lacks IPAddress::toString()
LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength);
#endif
meshtastic_MeshPacket mp;
+ mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength);
bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp);
if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
@@ -61,11 +70,18 @@ class UdpMulticastHandler final
if (!mp || !udp) {
return false;
}
-#ifndef ARCH_PORTDUINO
+#if defined(ARCH_NRF52)
+ if (!isEthernetAvailable()) {
+ return false;
+ }
+#elif !defined(ARCH_PORTDUINO)
if (WiFi.status() != WL_CONNECTED) {
return false;
}
#endif
+ if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) {
+ LOG_ERROR("Attempt to send UDP sourced packet over UDP");
+ }
LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id);
uint8_t buffer[meshtastic_MeshPacket_size];
size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp);
diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp
index 7a56c258b..1133ad424 100644
--- a/src/mesh/wifi/WiFiAPClient.cpp
+++ b/src/mesh/wifi/WiFiAPClient.cpp
@@ -235,6 +235,11 @@ bool isWifiAvailable()
#ifdef USE_WS5500
} else if (config.network.eth_enabled) {
return true;
+#endif
+#ifndef ARCH_PORTDUINO
+ } else if (WiFi.status() == WL_CONNECTED) {
+ // it's likely we have wifi now, but user intends to turn it off in config!
+ return true;
#endif
} else {
return false;
diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index 8d3e710df..407003f7e 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -43,6 +43,10 @@
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
#include "motion/AccelerometerThread.h"
#endif
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
+ !defined(CONFIG_IDF_TARGET_ESP32C3)
+#include "SerialModule.h"
+#endif
AdminModule *adminModule;
bool hasOpenEditTransaction;
@@ -501,7 +505,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
if (mp.decoded.want_response && !myReply) {
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
}
-
+ if (mp.pki_encrypted && myReply) {
+ myReply->pki_encrypted = true;
+ }
return handled;
}
@@ -596,7 +602,6 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
config.device.role == c.payload_variant.device.role &&
- config.device.disable_triple_click == c.payload_variant.device.disable_triple_click &&
config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) {
requiresReboot = false;
}
@@ -639,7 +644,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
case meshtastic_Config_position_tag:
LOG_INFO("Set config: Position");
config.has_position = true;
+ // If we have turned off the GPS (disabled or not present) and we're not using fixed position,
+ // clear the stored position since it may not get updated
+ if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
+ c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
+ config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) {
+ nodeDB->clearLocalPosition();
+ saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
+ }
config.position = c.payload_variant.position;
+
// Save nodedb as well in case we got a fixed position packet
break;
case meshtastic_Config_power_tag:
@@ -706,6 +720,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
requiresReboot = false;
}
+#if defined(ARCH_PORTDUINO)
+ // If running on portduino and using SimRadio, do not require reboot
+ if (SimRadio::instance) {
+ requiresReboot = false;
+ }
+#endif
+
#ifdef RF95_FAN_EN
// Turn PA off if disabled by config
if (c.payload_variant.lora.pa_fan_disabled) {
@@ -715,7 +736,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
}
#endif
config.lora = c.payload_variant.lora;
- // If we're setting region for the first time, init the region
+ // If we're setting region for the first time, init the region and regenerate the keys
if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
if (!owner.is_licensed) {
bool keygenSuccess = false;
@@ -760,8 +781,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
if (config.security.private_key.size != 32) {
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
- } else if (config.security.public_key.size != 32) {
- // We check for a potentially valid private key, and a blank public key, and regen the public key if needed.
+ } else {
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
config.security.public_key.size = 32;
}
@@ -799,8 +819,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
{
- if (!hasOpenEditTransaction)
+ // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
+ // Otherwise, disable Bluetooth to prevent the phone from interfering with the config
+ if (!hasOpenEditTransaction &&
+ !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
disableBluetooth();
+ }
+
switch (c.which_payload_variant) {
case meshtastic_ModuleConfig_mqtt_tag:
#if MESHTASTIC_EXCLUDE_MQTT
@@ -811,12 +836,22 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
if (!MQTT::isValidConfig(c.payload_variant.mqtt)) {
return false;
}
+ // Disable Bluetooth to prevent interference during MQTT configuration
+ disableBluetooth();
moduleConfig.has_mqtt = true;
moduleConfig.mqtt = c.payload_variant.mqtt;
#endif
break;
case meshtastic_ModuleConfig_serial_tag:
LOG_INFO("Set module config: Serial");
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
+ !defined(CONFIG_IDF_TARGET_ESP32C3)
+ if (!SerialModule::isValidConfig(c.payload_variant.serial)) {
+ LOG_ERROR("Invalid serial config");
+ return false;
+ }
+ disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration
+#endif
moduleConfig.has_serial = true;
moduleConfig.serial = c.payload_variant.serial;
break;
@@ -908,6 +943,9 @@ void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req)
res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag;
setPassKey(&res);
myReply = allocDataProtobuf(res);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
}
@@ -972,12 +1010,16 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
// using to the app (so that even old phone apps work with new device loads).
// r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
- // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private
- // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password);
- // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag;
+ // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally
+ // private and useful for users to know current provisioning)
+ // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant =
+ // Config_ModuleConfig_telemetry_tag;
res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag;
setPassKey(&res);
myReply = allocDataProtobuf(res);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
}
@@ -1058,12 +1100,16 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
// using to the app (so that even old phone apps work with new device loads).
// r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
- // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private
- // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password);
- // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag;
+ // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally
+ // private and useful for users to know current provisioning)
+ // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant =
+ // Config_ModuleConfig_telemetry_tag;
res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag;
setPassKey(&res);
myReply = allocDataProtobuf(res);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
}
@@ -1088,6 +1134,9 @@ void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &r
}
setPassKey(&r);
myReply = allocDataProtobuf(r);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
@@ -1097,6 +1146,9 @@ void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag;
setPassKey(&r);
myReply = allocDataProtobuf(r);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req)
@@ -1165,6 +1217,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag;
setPassKey(&r);
myReply = allocDataProtobuf(r);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex)
@@ -1176,6 +1231,9 @@ void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t ch
r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag;
setPassKey(&r);
myReply = allocDataProtobuf(r);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
}
@@ -1185,6 +1243,9 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req)
r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag;
r.get_ui_config_response = uiconfig;
myReply = allocDataProtobuf(r);
+ if (req.pki_encrypted) {
+ myReply->pki_encrypted = true;
+ }
}
void AdminModule::reboot(int32_t seconds)
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index a1b89e0f8..e9165e57c 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -40,6 +40,9 @@ extern ScanI2C::DeviceAddress cardkb_found;
extern bool graphics::isMuted;
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
+static NodeNum lastDest = NODENUM_BROADCAST;
+static uint8_t lastChannel = 0;
+static bool lastDestSet = false;
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
@@ -56,24 +59,34 @@ CannedMessageModule::CannedMessageModule()
disable();
} else {
LOG_INFO("CannedMessageModule is enabled");
+ moduleConfig.canned_message.enabled = true;
this->inputObserver.observe(inputBroker);
}
}
void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel)
{
+ // Use the requested destination, unless it's "broadcast" and we have a previous node/channel
+ if (newDest == NODENUM_BROADCAST && lastDestSet) {
+ newDest = lastDest;
+ newChannel = lastChannel;
+ }
dest = newDest;
channel = newChannel;
- // Always select the first real canned message on activation
- int firstRealMsgIdx = 0;
+ lastDest = dest;
+ lastChannel = channel;
+ lastDestSet = true;
+
+ // Rest of function unchanged...
+ // Upon activation, highlight "[Select Destination]"
+ int selectDestination = 0;
for (int i = 0; i < messagesCount; ++i) {
- if (strcmp(messages[i], "[Select Destination]") != 0 && strcmp(messages[i], "[Exit]") != 0 &&
- strcmp(messages[i], "[---- Free Text ----]") != 0) {
- firstRealMsgIdx = i;
+ if (strcmp(messages[i], "[Select Destination]") == 0) {
+ selectDestination = i;
break;
}
}
- currentMessageIndex = firstRealMsgIdx;
+ currentMessageIndex = selectDestination;
// This triggers the canned message list
runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
@@ -83,10 +96,28 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan
notifyObservers(&e);
}
+void CannedMessageModule::LaunchRepeatDestination()
+{
+ if (!lastDestSet) {
+ LaunchWithDestination(NODENUM_BROADCAST, 0);
+ } else {
+ LaunchWithDestination(lastDest, lastChannel);
+ }
+}
+
void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel)
{
+ // Use the requested destination, unless it's "broadcast" and we have a previous node/channel
+ if (newDest == NODENUM_BROADCAST && lastDestSet) {
+ newDest = lastDest;
+ newChannel = lastChannel;
+ }
dest = newDest;
channel = newChannel;
+ lastDest = dest;
+ lastChannel = channel;
+ lastDestSet = true;
+
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
requestFocus();
UIFrameEvent e;
@@ -478,6 +509,9 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
if (destIndex < static_cast(activeChannelIndices.size())) {
dest = NODENUM_BROADCAST;
channel = activeChannelIndices[destIndex];
+ lastDest = dest;
+ lastChannel = channel;
+ lastDestSet = true;
} else {
int nodeIndex = destIndex - static_cast(activeChannelIndices.size());
if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) {
@@ -485,6 +519,10 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
if (selectedNode) {
dest = selectedNode->num;
channel = selectedNode->channel;
+ // Already saves here, but for clarity, also:
+ lastDest = dest;
+ lastChannel = channel;
+ lastDestSet = true;
}
}
}
@@ -594,8 +632,27 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// Normal canned message selection
if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
} else {
+#if CANNED_MESSAGE_ADD_CONFIRMATION
+ // Show confirmation dialog before sending canned message
+ NodeNum destNode = dest;
+ ChannelIndex chan = channel;
+ graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() {
+ this->sendText(destNode, chan, current, false);
+ payload = runState;
+ runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
+ currentMessageIndex = -1;
+
+ // Notify UI to regenerate frame set and redraw
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ screen->forceDisplay();
+ });
+#else
payload = runState;
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
+#endif
+ // Do not immediately set runState; wait for confirmation
handled = true;
}
}
@@ -826,6 +883,9 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event)
void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies)
{
+ lastDest = dest;
+ lastChannel = channel;
+ lastDestSet = true;
// === Prepare packet ===
meshtastic_MeshPacket *p = allocDataPacket();
p->to = dest;
@@ -850,7 +910,13 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
this->waitingForAck = true;
// Log outgoing message
- LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
+ LOG_INFO("Send message id=%u, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
+
+ if (p->to != 0xffffffff) {
+ LOG_INFO("Proactively adding %x as favorite node", p->to);
+ nodeDB->set_favorite(true, p->to);
+ screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ }
// Send to mesh and phone (even if no phone connected, to track ACKs)
service->sendToMesh(p, RX_SRC_LOCAL, true);
@@ -925,24 +991,22 @@ int32_t CannedMessageModule::runOnce()
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
}
}
- e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->notifyObservers(&e);
return 2000;
}
- // Always highlight the first real canned message when entering the message list
+ // Highlight [Select Destination] initially when entering the message list
else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) {
- int firstRealMsgIdx = 0;
+ int selectDestination = 0;
for (int i = 0; i < this->messagesCount; ++i) {
- if (strcmp(this->messages[i], "[Select Destination]") != 0 && strcmp(this->messages[i], "[Exit]") != 0 &&
- strcmp(this->messages[i], "[---- Free Text ----]") != 0) {
- firstRealMsgIdx = i;
+ if (strcmp(this->messages[i], "[Select Destination]") == 0) {
+ selectDestination = i;
break;
}
}
- this->currentMessageIndex = firstRealMsgIdx;
+ this->currentMessageIndex = selectDestination;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) {
@@ -1441,7 +1505,7 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
int headerY = y;
int listTop = headerY + headerFontHeight + headerMargin;
- int visibleRows = (display->getHeight() - listTop - 2) / rowHeight;
+ int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight;
int numEmotes = graphics::numEmotes;
// Clamp highlight index
@@ -1451,11 +1515,11 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
emotePickerIndex = numEmotes - 1;
// Determine which emote is at the top
- int topIndex = emotePickerIndex - visibleRows / 2;
+ int topIndex = emotePickerIndex - _visibleRows / 2;
if (topIndex < 0)
topIndex = 0;
- if (topIndex > numEmotes - visibleRows)
- topIndex = std::max(0, numEmotes - visibleRows);
+ if (topIndex > numEmotes - _visibleRows)
+ topIndex = std::max(0, numEmotes - _visibleRows);
// Draw header/title
display->setFont(FONT_SMALL);
@@ -1703,8 +1767,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
} else {
// Text: split by words and wrap inside word if needed
String text = token.second;
- uint16_t pos = 0;
- while (pos < text.length()) {
+ pos = 0;
+ while (pos < static_cast(text.length())) {
// Find next space (or end)
int spacePos = text.indexOf(' ', pos);
int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space
@@ -1747,7 +1811,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int yLine = inputY;
for (auto &line : lines) {
int nextX = x;
- for (auto &token : line) {
+ for (const auto &token : line) {
if (token.first) {
const graphics::Emote *emote = nullptr;
for (int j = 0; j < graphics::numEmotes; j++) {
@@ -1783,19 +1847,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int topMsg;
std::vector rowHeights;
- int visibleRows;
+ int _visibleRows;
// Draw header (To: ...)
drawHeader(display, x, y, buffer);
// Shift message list upward by 3 pixels to reduce spacing between header and first message
const int listYOffset = y + FONT_HEIGHT_SMALL - 3;
- visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing;
+ _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing;
// Figure out which messages are visible and their needed heights
- topMsg =
- (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0;
- int countRows = std::min(messagesCount, visibleRows);
+ topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1)
+ ? currentMessageIndex - _visibleRows + 2
+ : 0;
+ int countRows = std::min(messagesCount, _visibleRows);
// --- Build per-row max height based on all emotes in line ---
for (int i = 0; i < countRows; i++) {
@@ -1822,7 +1887,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int lineY = yCursor;
const char *msg = getMessageByIndex(msgIdx);
int rowHeight = rowHeights[vis];
- bool highlight = (msgIdx == currentMessageIndex);
+ bool _highlight = (msgIdx == currentMessageIndex);
// --- Multi-emote tokenization ---
std::vector> tokens; // (isEmote, token)
@@ -1875,20 +1940,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2;
#ifdef USE_EINK
- int nextX = x + (highlight ? 12 : 0);
- if (highlight)
+ int nextX = x + (_highlight ? 12 : 0);
+ if (_highlight)
display->drawString(x + 0, lineY + textYOffset, ">");
#else
int scrollPadding = 8;
- if (highlight) {
+ if (_highlight) {
display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight);
display->setColor(BLACK);
}
- int nextX = x + (highlight ? 2 : 0);
+ int nextX = x + (_highlight ? 2 : 0);
#endif
// Draw all tokens left to right
- for (auto &token : tokens) {
+ for (const auto &token : tokens) {
if (token.first) {
// Emote
const graphics::Emote *emote = nullptr;
@@ -1910,7 +1975,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
}
}
#ifndef USE_EINK
- if (highlight)
+ if (_highlight)
display->setColor(WHITE);
#endif
@@ -1918,11 +1983,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
}
// Scrollbar
- if (messagesCount > visibleRows) {
+ if (messagesCount > _visibleRows) {
int scrollHeight = display->getHeight() - listYOffset;
int scrollTrackX = display->getWidth() - 6;
display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight);
- int barHeight = (scrollHeight * visibleRows) / messagesCount;
+ int barHeight = (scrollHeight * _visibleRows) / messagesCount;
int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount;
display->fillRect(scrollTrackX, scrollPos, 4, barHeight);
}
@@ -2068,6 +2133,9 @@ void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_
if (changed) {
this->saveProtoForModule();
+ if (splitConfiguredMessages()) {
+ moduleConfig.canned_message.enabled = true;
+ }
}
}
diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h
index 55a0a1185..5b0481ac7 100644
--- a/src/modules/CannedMessageModule.h
+++ b/src/modules/CannedMessageModule.h
@@ -59,6 +59,7 @@ class CannedMessageModule : public SinglePortModule, public ObservablebeginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
-#else
- rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
+ if (moduleConfig.external_notification.use_i2s_as_buzzer) {
+ audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
+ } else
#endif
+ if (moduleConfig.external_notification.use_pwm) {
+ rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
+ }
}
if (moduleConfig.external_notification.nag_timeout) {
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
@@ -526,10 +537,11 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
#ifdef HAS_I2S
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
- }
-#else
- rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
+ } else
#endif
+ if (moduleConfig.external_notification.use_pwm) {
+ rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
+ }
}
if (moduleConfig.external_notification.nag_timeout) {
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 3528f57f5..0d405fa81 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -3,6 +3,7 @@
#include "buzz/BuzzerFeedbackThread.h"
#include "input/ExpressLRSFiveWay.h"
#include "input/InputBroker.h"
+#include "input/RotaryEncoderImpl.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/SerialKeyboardImpl.h"
#include "input/TrackballInterruptImpl1.h"
@@ -170,11 +171,20 @@ void setupModules()
delete rotaryEncoderInterruptImpl1;
rotaryEncoderInterruptImpl1 = nullptr;
}
+#ifdef T_LORA_PAGER
+ // use a special FSM based rotary encoder version for T-LoRa Pager
+ rotaryEncoderImpl = new RotaryEncoderImpl();
+ if (!rotaryEncoderImpl->init()) {
+ delete rotaryEncoderImpl;
+ rotaryEncoderImpl = nullptr;
+ }
+#else
upDownInterruptImpl1 = new UpDownInterruptImpl1();
if (!upDownInterruptImpl1->init()) {
delete upDownInterruptImpl1;
upDownInterruptImpl1 = nullptr;
}
+#endif
cardKbI2cImpl = new CardKbI2cImpl();
cardKbI2cImpl->init();
#ifdef INPUTBROKER_MATRIX_TYPE
diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp
index eebf428a4..97dc17001 100644
--- a/src/modules/NeighborInfoModule.cpp
+++ b/src/modules/NeighborInfoModule.cpp
@@ -105,14 +105,15 @@ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies)
{
meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
collectNeighborInfo(&neighborInfo);
- meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
- // send regardless of whether or not we have neighbors in our DB,
- // because we want to get neighbors for the next cycle
- p->to = dest;
- p->decoded.want_response = wantReplies;
- p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
- printNeighborInfo("SENDING", &neighborInfo);
- service->sendToMesh(p, RX_SRC_LOCAL, true);
+ // only send neighbours if we have some to send
+ if (neighborInfo.neighbors_count > 0) {
+ meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
+ p->to = dest;
+ p->decoded.want_response = wantReplies;
+ p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
+ printNeighborInfo("SENDING", &neighborInfo);
+ service->sendToMesh(p, RX_SRC_LOCAL, true);
+ }
}
/*
@@ -214,4 +215,4 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen
neighbors.push_back(new_nbr);
}
return &neighbors.back();
-}
\ No newline at end of file
+}
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index cf9940e25..0060e99fa 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -14,6 +14,15 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
{
auto p = *pptr;
+ if (mp.from == nodeDB->getNodeNum()) {
+ LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
+ return false;
+ }
+ if (p.is_licensed != owner.is_licensed) {
+ LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
+ return true;
+ }
+
// Coerce user.id to be derived from the node number
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp));
diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp
index 93c65ecc1..8b6a9f19c 100644
--- a/src/modules/PositionModule.cpp
+++ b/src/modules/PositionModule.cpp
@@ -266,9 +266,11 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i);
+#ifndef MESHTASTIC_EXCLUDE_ATAK
// TAK Tracker devices should send their position in a TAK packet over the ATAK port
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)
return allocAtakPli();
+#endif
return allocDataProtobuf(p);
}
diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h
index b9fd527c9..4a2415058 100644
--- a/src/modules/PositionModule.h
+++ b/src/modules/PositionModule.h
@@ -65,8 +65,15 @@ class PositionModule : public ProtobufModule, private concu
bool hasGPS();
uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only)
+#if USERPREFS_EVENT_MODE
+ // In event mode we want to prevent excessive position broadcasts
+ // we set the minimum interval to 5m
+ const uint32_t minimumTimeThreshold =
+ max(300000, Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30));
+#else
const uint32_t minimumTimeThreshold =
Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30);
+#endif
};
struct SmartPosition {
diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp
index 8892aaa97..434441d49 100644
--- a/src/modules/ReplyModule.cpp
+++ b/src/modules/ReplyModule.cpp
@@ -8,7 +8,7 @@
meshtastic_MeshPacket *ReplyModule::allocReply()
{
assert(currentRequest); // should always be !NULL
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
auto req = *currentRequest;
auto &p = req.decoded;
// The incoming message is in p.payload
diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp
index f3921ef19..880768839 100644
--- a/src/modules/SerialModule.cpp
+++ b/src/modules/SerialModule.cpp
@@ -45,6 +45,9 @@
*/
+#ifdef HELTEC_MESH_SOLAR
+#include "meshSolarApp.h"
+#endif
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
@@ -60,7 +63,8 @@
SerialModule *serialModule;
SerialModuleRadio *serialModuleRadio;
-#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1)
+#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \
+ defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE)
SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {}
static Print *serialPrint = &Serial;
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
@@ -74,6 +78,27 @@ static Print *serialPrint = &Serial2;
char serialBytes[512];
size_t serialPayloadSize;
+bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config)
+{
+ if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA,
+ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO,
+ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
+ const char *warning =
+ "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes.";
+ LOG_ERROR(warning);
+#if !IS_RUNNING_TESTS
+ meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
+ cn->level = meshtastic_LogRecord_Level_ERROR;
+ cn->time = getValidTime(RTCQualityFromNet);
+ snprintf(cn->message, sizeof(cn->message), "%s", warning);
+ service->sendClientNotification(cn);
+#endif
+ return false;
+ }
+
+ return true;
+}
+
SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio")
{
switch (moduleConfig.serial.mode) {
@@ -158,7 +183,8 @@ int32_t SerialModule::runOnce()
Serial.begin(baud);
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
}
-#elif !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1)
+#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
+ !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
#ifdef ARCH_RP2040
Serial2.setFIFOSize(RX_BUFFER);
@@ -214,11 +240,22 @@ int32_t SerialModule::runOnce()
}
}
-#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1)
+#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
+ !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
processWXSerial();
- } else {
+ }
+#if defined(HELTEC_MESH_SOLAR)
+ else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
+ serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1);
+ // If the parsing fails, the following parsing will be performed.
+ if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) {
+ return runOncePart(serialBytes, serialPayloadSize);
+ }
+ }
+#endif
+ else {
#if defined(CONFIG_IDF_TARGET_ESP32C6)
while (Serial1.available()) {
serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
@@ -473,8 +510,8 @@ ParsedLine parseLine(const char *line)
*/
void SerialModule::processWXSerial()
{
-#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && \
- !defined(ELECROW_ThinkNode_M1)
+#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \
+ !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
static unsigned int lastAveraged = 0;
static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
static double dir_sum_sin = 0;
diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h
index fa86db28f..1c74c927c 100644
--- a/src/modules/SerialModule.h
+++ b/src/modules/SerialModule.h
@@ -20,6 +20,8 @@ class SerialModule : public StreamAPI, private concurrency::OSThread
public:
SerialModule();
+ static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config);
+
protected:
virtual int32_t runOnce() override;
diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp
index 0a6e1b4c4..72ac99118 100644
--- a/src/modules/StoreForwardModule.cpp
+++ b/src/modules/StoreForwardModule.cpp
@@ -202,6 +202,8 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id;
this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji;
this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size;
+ this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi;
+ this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr;
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
this->packetHistoryTotalCount++;
@@ -252,6 +254,8 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t
p->decoded.reply_id = this->packetHistory[i].reply_id;
p->rx_time = this->packetHistory[i].time;
p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji;
+ p->rx_rssi = this->packetHistory[i].rx_rssi;
+ p->rx_snr = this->packetHistory[i].rx_snr;
// Let's assume that if the server received the S&F request that the client is in range.
// TODO: Make this configurable.
@@ -623,4 +627,4 @@ StoreForwardModule::StoreForwardModule()
disable();
}
#endif
-}
\ No newline at end of file
+}
diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h
index 30db1625c..25836eded 100644
--- a/src/modules/StoreForwardModule.h
+++ b/src/modules/StoreForwardModule.h
@@ -19,6 +19,8 @@ struct PacketHistoryStruct {
bool emoji;
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
pb_size_t payload_size;
+ int32_t rx_rssi;
+ float rx_snr;
};
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule
@@ -108,4 +110,4 @@ class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p);
};
-extern StoreForwardModule *storeForwardModule;
\ No newline at end of file
+extern StoreForwardModule *storeForwardModule;
diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp
index ab9439b39..74b9678f4 100644
--- a/src/modules/SystemCommandsModule.cpp
+++ b/src/modules/SystemCommandsModule.cpp
@@ -89,6 +89,11 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
#if !MESHTASTIC_EXCLUDE_GPS
if (gps) {
LOG_WARN("GPS Toggle2");
+ if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
+ config.position.fixed_position == false) {
+ nodeDB->clearLocalPosition();
+ nodeDB->saveToDisk();
+ }
gps->toggleGpsMode();
const char *msg =
(config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled";
@@ -107,11 +112,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
return true;
// Power control
case INPUT_BROKER_SHUTDOWN:
- LOG_ERROR("Shutting Down");
- IF_SCREEN(screen->showSimpleBanner("Shutting Down..."));
- nodeDB->saveToDisk();
- shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
- // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
+ shutdownAtMsec = millis();
return true;
default:
diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp
index 2472b95b1..21a563b9d 100644
--- a/src/modules/Telemetry/AirQualityTelemetry.cpp
+++ b/src/modules/Telemetry/AirQualityTelemetry.cpp
@@ -121,7 +121,7 @@ int32_t AirQualityTelemetryModule::runOnce()
bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp
index 43c2dd84c..08fd09db0 100644
--- a/src/modules/Telemetry/DeviceTelemetry.cpp
+++ b/src/modules/Telemetry/DeviceTelemetry.cpp
@@ -49,7 +49,7 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
return false;
if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) {
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender,
diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp
index d1b10fa82..8926b171c 100644
--- a/src/modules/Telemetry/EnvironmentTelemetry.cpp
+++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp
@@ -502,7 +502,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, "
diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp
index 3a735b1fa..215e49c7a 100644
--- a/src/modules/Telemetry/HealthTelemetry.cpp
+++ b/src/modules/Telemetry/HealthTelemetry.cpp
@@ -149,7 +149,7 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *
bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) {
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature,
diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp
index 6a92b15f8..8f10b9228 100644
--- a/src/modules/Telemetry/HostMetrics.cpp
+++ b/src/modules/Telemetry/HostMetrics.cpp
@@ -27,7 +27,7 @@ bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp,
return false;
if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) {
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
if (t->variant.host_metrics.has_user_string)
t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0';
diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
index a92013d01..35409edef 100644
--- a/src/modules/Telemetry/PowerTelemetry.cpp
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -168,7 +168,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) {
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, "
diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.cpp b/src/modules/Telemetry/Sensor/INA226Sensor.cpp
index 4b313ba81..6fa35598f 100644
--- a/src/modules/Telemetry/Sensor/INA226Sensor.cpp
+++ b/src/modules/Telemetry/Sensor/INA226Sensor.cpp
@@ -32,16 +32,42 @@ void INA226Sensor::begin(TwoWire *wire, uint8_t addr)
_addr = addr;
ina226 = INA226(_addr, _wire);
_wire->begin();
+ ina226.setMaxCurrentShunt(0.8, 0.100);
}
bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+ switch (measurement->which_variant) {
+ case meshtastic_Telemetry_environment_metrics_tag:
+ return getEnvironmentMetrics(measurement);
+
+ case meshtastic_Telemetry_power_metrics_tag:
+ return getPowerMetrics(measurement);
+ }
+
+ // unsupported metric
+ return false;
+}
+
+bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement)
{
measurement->variant.environment_metrics.has_voltage = true;
measurement->variant.environment_metrics.has_current = true;
- // mV conversion to V
measurement->variant.environment_metrics.voltage = ina226.getBusVoltage();
measurement->variant.environment_metrics.current = ina226.getCurrent_mA();
+
+ return true;
+}
+
+bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement)
+{
+ measurement->variant.power_metrics.has_ch1_voltage = true;
+ measurement->variant.power_metrics.has_ch1_current = true;
+
+ measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage();
+ measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA();
+
return true;
}
diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.h b/src/modules/Telemetry/Sensor/INA226Sensor.h
index 2f71c5b86..51435550e 100644
--- a/src/modules/Telemetry/Sensor/INA226Sensor.h
+++ b/src/modules/Telemetry/Sensor/INA226Sensor.h
@@ -15,6 +15,9 @@ class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
TwoWire *_wire = &Wire;
INA226 ina226 = INA226(_addr, _wire);
+ bool getEnvironmentMetrics(meshtastic_Telemetry *measurement);
+ bool getPowerMetrics(meshtastic_Telemetry *measurement);
+
protected:
virtual void setup() override;
void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR);
diff --git a/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp
index ef1756b36..b6b5d89f7 100644
--- a/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp
+++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp
@@ -112,9 +112,8 @@ bool NAU7802Sensor::saveCalibrationData()
} else {
okay = true;
}
- spiLock->lock();
+ // Note: SafeFile::close() already acquires the lock and releases it internally
okay &= file.close();
- spiLock->unlock();
return okay;
}
diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp
index 970f4429c..72df330c5 100644
--- a/src/modules/TextMessageModule.cpp
+++ b/src/modules/TextMessageModule.cpp
@@ -9,7 +9,7 @@ TextMessageModule *textMessageModule;
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
auto &p = mp.decoded;
LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes);
#endif
diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp
index 41cb35649..d7df90bb5 100644
--- a/src/modules/TraceRouteModule.cpp
+++ b/src/modules/TraceRouteModule.cpp
@@ -1,6 +1,13 @@
#include "TraceRouteModule.h"
#include "MeshService.h"
+#include "graphics/Screen.h"
+#include "graphics/ScreenFonts.h"
+#include "graphics/SharedUIDisplay.h"
+#include "mesh/Router.h"
#include "meshUtils.h"
+#include
+
+extern graphics::Screen *screen;
TraceRouteModule *traceRouteModule;
@@ -27,6 +34,123 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti
// Set updated route to the payload of the to be flooded packet
p.decoded.payload.size =
pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r);
+
+ if (tracingNode != 0) {
+ // check isResponseFromTarget
+ bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode);
+ bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0);
+
+ // Check if this is a trace route response containing our target node
+ bool containsTargetNode = false;
+ for (uint8_t i = 0; i < r->route_count; i++) {
+ if (r->route[i] == tracingNode) {
+ containsTargetNode = true;
+ break;
+ }
+ }
+ for (uint8_t i = 0; i < r->route_back_count; i++) {
+ if (r->route_back[i] == tracingNode) {
+ containsTargetNode = true;
+ break;
+ }
+ }
+
+ // Check if this response contains a complete route to our target
+ bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) ||
+ (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0));
+
+ LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode,
+ p.from, p.to, incoming.request_id);
+ LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d",
+ isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute);
+
+ if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) {
+ LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs);
+
+ LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count);
+ for (int i = 0; i < r->snr_towards_count; i++) {
+ LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f);
+ }
+ for (int i = 0; i < r->snr_back_count; i++) {
+ LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f);
+ }
+
+ String result = "";
+
+ // Show request path (from initiator to target)
+ if (r->route_count > 0) {
+ result += getNodeName(nodeDB->getNodeNum());
+ for (uint8_t i = 0; i < r->route_count; i++) {
+ result += " > ";
+ const char *name = getNodeName(r->route[i]);
+ float snr =
+ (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f;
+ result += name;
+ if (snr != 0.0f) {
+ result += "(";
+ result += String(snr, 1);
+ result += "dB)";
+ }
+ }
+ result += " > ";
+ result += getNodeName(tracingNode);
+ if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) {
+ result += "(";
+ result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1);
+ result += "dB)";
+ }
+ result += "\n";
+ } else {
+ // Direct connection (no intermediate hops)
+ result += getNodeName(nodeDB->getNodeNum());
+ result += " > ";
+ result += getNodeName(tracingNode);
+ if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) {
+ result += "(";
+ result += String((float)r->snr_towards[0] / 4.0f, 1);
+ result += "dB)";
+ }
+ result += "\n";
+ }
+
+ // Show response path (from target back to initiator)
+ if (r->route_back_count > 0) {
+ result += getNodeName(tracingNode);
+ for (int8_t i = r->route_back_count - 1; i >= 0; i--) {
+ result += " > ";
+ const char *name = getNodeName(r->route_back[i]);
+ float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f;
+ result += name;
+ if (snr != 0.0f) {
+ result += "(";
+ result += String(snr, 1);
+ result += "dB)";
+ }
+ }
+ // add initiator node
+ result += " > ";
+ result += getNodeName(nodeDB->getNodeNum());
+ if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) {
+ result += "(";
+ result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1);
+ result += "dB)";
+ }
+ } else {
+ // Direct return path (no intermediate hops)
+ result += getNodeName(tracingNode);
+ result += " > ";
+ result += getNodeName(nodeDB->getNodeNum());
+ if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) {
+ result += "(";
+ result += String((float)r->snr_back[0] / 4.0f, 1);
+ result += "dB)";
+ }
+ }
+
+ LOG_INFO("Trace route result: %s", result.c_str());
+ handleTraceRouteResult(result);
+ }
+ }
}
void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination)
@@ -108,7 +232,7 @@ void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, floa
void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination)
{
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
std::string route = "Route traced:\n";
route += vformat("0x%x --> ", origin);
for (uint8_t i = 0; i < r->route_count; i++) {
@@ -173,8 +297,467 @@ meshtastic_MeshPacket *TraceRouteModule::allocReply()
}
TraceRouteModule::TraceRouteModule()
- : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg)
+ : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute")
{
ourPortNum = meshtastic_PortNum_TRACEROUTE_APP;
isPromiscuous = true; // We need to update the route even if it is not destined to us
+}
+
+const char *TraceRouteModule::getNodeName(NodeNum node)
+{
+ meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
+ if (info && info->has_user) {
+ if (strlen(info->user.short_name) > 0) {
+ return info->user.short_name;
+ }
+ if (strlen(info->user.long_name) > 0) {
+ return info->user.long_name;
+ }
+ }
+
+ static char fallback[12];
+ snprintf(fallback, sizeof(fallback), "0x%08x", node);
+ return fallback;
+}
+
+bool TraceRouteModule::startTraceRoute(NodeNum node)
+{
+ LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node);
+ unsigned long now = millis();
+
+ if (node == 0 || node == NODENUM_BROADCAST) {
+ LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Invalid node";
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ return false;
+ }
+
+ if (node == nodeDB->getNodeNum()) {
+ LOG_ERROR("Cannot trace route to self: 0x%08x", node);
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Cannot trace self";
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ return false;
+ }
+
+ if (!initialized) {
+ lastTraceRouteTime = 0;
+ initialized = true;
+ LOG_INFO("TraceRoute initialized for first time");
+ }
+
+ if (runState == TRACEROUTE_STATE_TRACKING) {
+ LOG_INFO("TraceRoute already in progress");
+ return false;
+ }
+
+ if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) {
+ // Cooldown
+ unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
+ bannerText = String("Wait for ") + String(wait) + String("s");
+ runState = TRACEROUTE_STATE_COOLDOWN;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait);
+ return false;
+ }
+
+ tracingNode = node;
+ lastTraceRouteTime = now;
+ runState = TRACEROUTE_STATE_TRACKING;
+ bannerText = String("Tracing ") + getNodeName(node);
+
+ LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node);
+
+ // 请求焦点,然后触发UI更新事件
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+
+ // 设置定时器来处理超时检查
+ setIntervalFromNow(1000); // 每秒检查一次状态
+
+ meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero;
+ LOG_INFO("Creating RouteDiscovery protobuf...");
+
+ // Allocate a packet directly from router like the reference code
+ meshtastic_MeshPacket *p = router->allocForSending();
+ if (p) {
+ // Set destination and port
+ p->to = node;
+ p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP;
+ p->decoded.want_response = true;
+
+ // Manually encode the RouteDiscovery payload
+ p->decoded.payload.size =
+ pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req);
+
+ LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to,
+ p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size);
+ LOG_INFO("About to call service->sendToMesh...");
+
+ if (service) {
+ LOG_INFO("MeshService is available, sending packet...");
+ service->sendToMesh(p, RX_SRC_USER);
+ LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node);
+ } else {
+ LOG_ERROR("MeshService is NULL!");
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Service unavailable";
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e2;
+ e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e2);
+ return false;
+ }
+ } else {
+ LOG_ERROR("Failed to allocate TraceRoute packet from router");
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Failed to send";
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e2;
+ e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e2);
+ return false;
+ }
+ return true;
+}
+
+void TraceRouteModule::launch(NodeNum node)
+{
+ if (node == 0 || node == NODENUM_BROADCAST) {
+ LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Invalid node";
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ return;
+ }
+
+ if (node == nodeDB->getNodeNum()) {
+ LOG_ERROR("Cannot trace route to self: 0x%08x", node);
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Cannot trace self";
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ return;
+ }
+
+ if (!initialized) {
+ lastTraceRouteTime = 0;
+ initialized = true;
+ LOG_INFO("TraceRoute initialized for first time");
+ }
+
+ unsigned long now = millis();
+ if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) {
+ unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
+ bannerText = String("Wait for ") + String(wait) + String("s");
+ runState = TRACEROUTE_STATE_COOLDOWN;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait);
+ return;
+ }
+
+ runState = TRACEROUTE_STATE_TRACKING;
+ tracingNode = node;
+ lastTraceRouteTime = now;
+ bannerText = String("Tracing ") + getNodeName(node);
+
+ requestFocus();
+
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+
+ setIntervalFromNow(1000);
+
+ meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero;
+ LOG_INFO("Creating RouteDiscovery protobuf...");
+
+ meshtastic_MeshPacket *p = router->allocForSending();
+ if (p) {
+ p->to = node;
+ p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP;
+ p->decoded.want_response = true;
+
+ p->decoded.payload.size =
+ pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req);
+
+ LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to,
+ p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size);
+
+ if (service) {
+ service->sendToMesh(p, RX_SRC_USER);
+ LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node);
+ } else {
+ LOG_ERROR("MeshService is NULL!");
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Service unavailable";
+ resultShowTime = millis();
+ tracingNode = 0;
+ }
+ } else {
+ LOG_ERROR("Failed to allocate TraceRoute packet from router");
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "Failed to send";
+ resultShowTime = millis();
+ tracingNode = 0;
+ }
+}
+
+void TraceRouteModule::handleTraceRouteResult(const String &result)
+{
+ resultText = result;
+ runState = TRACEROUTE_STATE_RESULT;
+ resultShowTime = millis();
+ tracingNode = 0;
+
+ LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str());
+
+ setIntervalFromNow(1000);
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+
+ LOG_INFO("=== TraceRoute handleTraceRouteResult END ===");
+}
+
+bool TraceRouteModule::shouldDraw()
+{
+ bool draw = (runState != TRACEROUTE_STATE_IDLE);
+ static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE;
+ if (runState != lastLoggedState) {
+ LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw);
+ lastLoggedState = runState;
+ }
+ return draw;
+}
+#if HAS_SCREEN
+void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
+{
+ LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState);
+
+ display->setTextAlignment(TEXT_ALIGN_CENTER);
+
+ if (runState == TRACEROUTE_STATE_TRACKING) {
+ display->setFont(FONT_MEDIUM);
+ int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2);
+ display->drawString(display->getWidth() / 2 + x, centerY, bannerText);
+
+ } else if (runState == TRACEROUTE_STATE_RESULT) {
+ display->setFont(FONT_MEDIUM);
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+
+ display->drawString(x, y, "Route Result");
+
+ int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+ display->setFont(FONT_SMALL);
+
+ if (resultText.length() > 0) {
+ std::vector lines;
+ String currentLine = "";
+ int maxWidth = display->getWidth() - 4;
+
+ int start = 0;
+ int newlinePos = resultText.indexOf('\n', start);
+
+ while (newlinePos != -1 || start < static_cast(resultText.length())) {
+ String segment;
+ if (newlinePos != -1) {
+ segment = resultText.substring(start, newlinePos);
+ start = newlinePos + 1;
+ newlinePos = resultText.indexOf('\n', start);
+ } else {
+ segment = resultText.substring(start);
+ start = resultText.length();
+ }
+
+ if (display->getStringWidth(segment) <= maxWidth) {
+ lines.push_back(segment);
+ } else {
+ // Try to break at better positions (space, >, <, -)
+ String remaining = segment;
+
+ while (remaining.length() > 0) {
+ String tempLine = "";
+ int lastGoodBreak = -1;
+ bool lineComplete = false;
+
+ for (int i = 0; i < static_cast(remaining.length()); i++) {
+ char ch = remaining.charAt(i);
+ String testLine = tempLine + ch;
+
+ if (display->getStringWidth(testLine) > maxWidth) {
+ if (lastGoodBreak >= 0) {
+ // Break at the last good position
+ lines.push_back(remaining.substring(0, lastGoodBreak + 1));
+ remaining = remaining.substring(lastGoodBreak + 1);
+ lineComplete = true;
+ break;
+ } else if (tempLine.length() > 0) {
+ lines.push_back(tempLine);
+ remaining = remaining.substring(i);
+ lineComplete = true;
+ break;
+ } else {
+ // Single character exceeds width
+ lines.push_back(String(ch));
+ remaining = remaining.substring(i + 1);
+ lineComplete = true;
+ break;
+ }
+ } else {
+ tempLine = testLine;
+ // Mark good break positions
+ if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') {
+ lastGoodBreak = i;
+ }
+ }
+ }
+
+ if (!lineComplete) {
+ // Reached end of remaining text
+ if (tempLine.length() > 0) {
+ lines.push_back(tempLine);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing
+ for (size_t i = 0; i < lines.size(); i++) {
+ int lineY = contentStartY + (i * lineHeight);
+ if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) {
+ display->drawString(x + 2, lineY, lines[i]);
+ }
+ }
+ }
+
+ } else if (runState == TRACEROUTE_STATE_COOLDOWN) {
+ display->setFont(FONT_MEDIUM);
+ int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2);
+ display->drawString(display->getWidth() / 2 + x, centerY, bannerText);
+ }
+}
+#endif // HAS_SCREEN
+int32_t TraceRouteModule::runOnce()
+{
+ unsigned long now = millis();
+
+ if (runState == TRACEROUTE_STATE_IDLE) {
+ return INT32_MAX;
+ }
+
+ // Check for tracking timeout
+ if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) {
+ LOG_INFO("TraceRoute timeout, no response received");
+ runState = TRACEROUTE_STATE_RESULT;
+ resultText = "No response received";
+ resultShowTime = now;
+ tracingNode = 0;
+
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+
+ setIntervalFromNow(resultDisplayMs);
+ return resultDisplayMs;
+ }
+
+ // Update cooldown display every second
+ if (runState == TRACEROUTE_STATE_COOLDOWN) {
+ unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
+ if (wait > 0) {
+ String newBannerText = String("Wait for ") + String(wait) + String("s");
+ bannerText = newBannerText;
+ LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str());
+
+ // Force flash UI
+ requestFocus();
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+
+ if (screen) {
+ screen->forceDisplay();
+ }
+
+ return 1000;
+ } else {
+ // Cooldown finished
+ LOG_INFO("TraceRoute cooldown finished, returning to IDLE");
+ runState = TRACEROUTE_STATE_IDLE;
+ bannerText = "";
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ return INT32_MAX;
+ }
+ }
+
+ if (runState == TRACEROUTE_STATE_RESULT) {
+ if (now - resultShowTime >= resultDisplayMs) {
+ LOG_INFO("TraceRoute result display timeout, returning to IDLE");
+ runState = TRACEROUTE_STATE_IDLE;
+ resultText = "";
+ bannerText = "";
+ tracingNode = 0;
+ UIFrameEvent e;
+ e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
+ notifyObservers(&e);
+ return INT32_MAX;
+ } else {
+ return 1000;
+ }
+ }
+
+ if (runState == TRACEROUTE_STATE_TRACKING) {
+ return 1000;
+ }
+
+ return INT32_MAX;
}
\ No newline at end of file
diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h
index afe2b3871..51d98826e 100644
--- a/src/modules/TraceRouteModule.h
+++ b/src/modules/TraceRouteModule.h
@@ -1,16 +1,40 @@
#pragma once
#include "ProtobufModule.h"
+#include "concurrency/OSThread.h"
+#include "graphics/Screen.h"
+#include "graphics/SharedUIDisplay.h"
+#include "input/InputBroker.h"
+#if HAS_SCREEN
+#include "OLEDDisplayUi.h"
+#endif
#define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0])
/**
* A module that traces the route to a certain destination node
*/
-class TraceRouteModule : public ProtobufModule
+enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN };
+
+class TraceRouteModule : public ProtobufModule,
+ public Observable,
+ private concurrency::OSThread
{
public:
TraceRouteModule();
+ bool startTraceRoute(NodeNum node);
+ void launch(NodeNum node);
+ void handleTraceRouteResult(const String &result);
+ bool shouldDraw();
+#if HAS_SCREEN
+ virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
+#endif
+
+ const char *getNodeName(NodeNum node);
+
+ virtual bool wantUIFrame() override { return shouldDraw(); }
+ virtual Observable *getUIFrameObservable() override { return this; }
+
protected:
bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override;
@@ -20,6 +44,8 @@ class TraceRouteModule : public ProtobufModule
the route array containing the IDs of nodes this packet went through */
void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override;
+ virtual int32_t runOnce() override;
+
private:
// Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit
void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination);
@@ -31,6 +57,17 @@ class TraceRouteModule : public ProtobufModule
Set origin to where the request came from.
Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */
void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination);
+
+ TraceRouteRunState runState = TRACEROUTE_STATE_IDLE;
+ unsigned long lastTraceRouteTime = 0;
+ unsigned long resultShowTime = 0;
+ unsigned long cooldownMs = 30000;
+ unsigned long resultDisplayMs = 10000;
+ unsigned long trackingTimeoutMs = 10000;
+ String bannerText;
+ String resultText;
+ NodeNum tracingNode = 0;
+ bool initialized = false;
};
extern TraceRouteModule *traceRouteModule;
\ No newline at end of file
diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp
index aab3ed6bc..4b05d5fa1 100644
--- a/src/modules/WaypointModule.cpp
+++ b/src/modules/WaypointModule.cpp
@@ -16,7 +16,7 @@ WaypointModule *waypointModule;
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
{
-#ifdef DEBUG_PORT
+#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
auto &p = mp.decoded;
LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes);
#endif
diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp
index 137c92056..7f7a9d511 100644
--- a/src/mqtt/MQTT.cpp
+++ b/src/mqtt/MQTT.cpp
@@ -39,6 +39,7 @@
#include
#define ntohl __ntohl
#endif
+#include
MQTT *mqtt;
@@ -94,6 +95,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
p->hop_start = e.packet->hop_start;
p->want_ack = e.packet->want_ack;
p->via_mqtt = true; // Mark that the packet was received via MQTT
+ p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
p->which_payload_variant = e.packet->which_payload_variant;
memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted)));
@@ -277,6 +279,8 @@ struct PubSubConfig {
// Defaults
static constexpr uint16_t defaultPort = 1883;
+ static constexpr uint16_t defaultPortTls = 8883;
+
uint16_t serverPort = defaultPort;
String serverAddr = default_mqtt_address;
const char *mqttUsername = default_mqtt_username;
@@ -559,10 +563,8 @@ void MQTT::sendSubscriptions()
int32_t MQTT::runOnce()
{
-#if HAS_NETWORKING
if (!moduleConfig.mqtt.enabled || !(moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()))
return disable();
-
bool wantConnection = wantsLink();
perhapsReportToMap();
@@ -572,7 +574,7 @@ int32_t MQTT::runOnce()
publishQueuedMessages();
return 200;
}
-
+#if HAS_NETWORKING
else if (!pubSub.loop()) {
if (!wantConnection)
return 5000; // If we don't want connection now, check again in 5 secs
@@ -596,8 +598,10 @@ int32_t MQTT::runOnce()
powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth)
return 20;
}
-#endif
+#else
+ // No networking available, return default interval
return 30000;
+#endif
}
bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client)
@@ -624,18 +628,32 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection);
}
#else
- LOG_ERROR("Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network");
+ const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network";
+ LOG_ERROR(warning);
+#if !IS_RUNNING_TESTS
+ meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
+ cn->level = meshtastic_LogRecord_Level_ERROR;
+ cn->time = getValidTime(RTCQualityFromNet);
+ strncpy(cn->message, warning, sizeof(cn->message) - 1);
+ cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination
+ service->sendClientNotification(cn);
+#endif
return false;
#endif
}
const bool defaultServer = isDefaultServer(parsed.serverAddr);
- if (defaultServer && config.tls_enabled) {
- LOG_ERROR("Invalid MQTT config: TLS was enabled, but the default server does not support TLS");
- return false;
- }
- if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) {
- LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", parsed.serverPort);
+ if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) {
+ const char *warning = "Invalid MQTT config: default server address must not have a port specified";
+ LOG_ERROR(warning);
+#if !IS_RUNNING_TESTS
+ meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
+ cn->level = meshtastic_LogRecord_Level_ERROR;
+ cn->time = getValidTime(RTCQualityFromNet);
+ strncpy(cn->message, warning, sizeof(cn->message) - 1);
+ cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination
+ service->sendClientNotification(cn);
+#endif
return false;
}
return true;
diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index 834184292..95e191c8e 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -223,9 +223,12 @@ void NimbleBluetooth::deinit()
LOG_INFO("Disable bluetooth until reboot");
#ifdef BLE_LED
+#ifdef BLE_LED_INVERTED
+ digitalWrite(BLE_LED, HIGH);
+#else
digitalWrite(BLE_LED, LOW);
#endif
-
+#endif
NimBLEDevice::deinit();
#endif
}
diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h
index 077b999c4..beaa7f1b7 100644
--- a/src/platform/esp32/architecture.h
+++ b/src/platform/esp32/architecture.h
@@ -146,6 +146,8 @@
#define HW_VENDOR meshtastic_HardwareModel_EBYTE_ESP32_S3
#elif defined(ELECROW_ThinkNode_M2)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M2
+#elif defined(ELECROW_ThinkNode_M5)
+#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M5
#elif defined(ESP32_S3_PICO)
#define HW_VENDOR meshtastic_HardwareModel_ESP32_S3_PICO
#elif defined(SENSELORA_S3)
@@ -190,6 +192,10 @@
#define HW_VENDOR meshtastic_HardwareModel_RAK3312
#elif defined(LINK_32)
#define HW_VENDOR meshtastic_HardwareModel_LINK_32
+#elif defined(T_DECK_PRO)
+#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
+#elif defined(T_LORA_PAGER)
+#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
#endif
// -----------------------------------------------------------------------------
diff --git a/src/platform/extra_variants/t_deck_pro/variant.cpp b/src/platform/extra_variants/t_deck_pro/variant.cpp
new file mode 100644
index 000000000..eae9335ce
--- /dev/null
+++ b/src/platform/extra_variants/t_deck_pro/variant.cpp
@@ -0,0 +1,28 @@
+#include "configuration.h"
+
+#ifdef T_DECK_PRO
+
+#include "input/TouchScreenImpl1.h"
+#include
+#include
+
+CSE_CST328 tsPanel = CSE_CST328(EINK_WIDTH, EINK_HEIGHT, &Wire, CST328_PIN_RST, CST328_PIN_INT);
+
+bool readTouch(int16_t *x, int16_t *y)
+{
+ if (tsPanel.getTouches()) {
+ *x = tsPanel.getPoint(0).x;
+ *y = tsPanel.getPoint(0).y;
+ return true;
+ }
+ return false;
+}
+
+// T-Deck Pro specific init
+void lateInitVariant()
+{
+ tsPanel.begin();
+ touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch);
+ touchScreenImpl1->init();
+}
+#endif
\ No newline at end of file
diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp
new file mode 100644
index 000000000..ea5773d30
--- /dev/null
+++ b/src/platform/extra_variants/t_lora_pager/variant.cpp
@@ -0,0 +1,27 @@
+#include "configuration.h"
+
+#ifdef T_LORA_PAGER
+
+#include "AudioBoard.h"
+
+DriverPins PinsAudioBoardES8311;
+AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
+
+// TLora Pager specific init
+void lateInitVariant()
+{
+ // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug);
+ // I2C: function, scl, sda
+ PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire);
+ // I2S: function, mclk, bck, ws, data_out, data_in
+ PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN);
+
+ // configure codec
+ CodecConfig cfg;
+ cfg.input_device = ADC_INPUT_LINE1;
+ cfg.output_device = DAC_OUTPUT_ALL;
+ cfg.i2s.bits = BIT_LENGTH_16BITS;
+ cfg.i2s.rate = RATE_44K;
+ board.begin(cfg);
+}
+#endif
\ No newline at end of file
diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp
new file mode 100644
index 000000000..836fb1307
--- /dev/null
+++ b/src/platform/nrf52/AsyncUDP.cpp
@@ -0,0 +1,73 @@
+#include "AsyncUDP.h"
+
+#if HAS_ETHERNET
+
+AsyncUDP::AsyncUDP() : OSThread("AsyncUDP"), localPort(0) {}
+
+bool AsyncUDP::listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl)
+{
+ if (!isMulticast(multicastIP))
+ return false;
+ localPort = port;
+ udp.beginMulticast(multicastIP, port);
+ return true;
+}
+
+size_t AsyncUDP::write(uint8_t b)
+{
+ return udp.write(&b, 1);
+}
+
+size_t AsyncUDP::write(const uint8_t *data, size_t len)
+{
+ return udp.write(data, len);
+}
+
+void AsyncUDP::onPacket(const std::function &callback)
+{
+ _onPacket = callback;
+}
+
+bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port)
+{
+ if (!udp.beginPacket(ip, port))
+ return false;
+ udp.write(data, len);
+ return udp.endPacket();
+}
+
+// AsyncUDPPacket
+AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort())
+{
+ if (_udp.available() > 0) {
+ _readLength = _udp.read(_buffer, sizeof(_buffer));
+ } else {
+ _readLength = 0;
+ }
+}
+
+IPAddress AsyncUDPPacket::remoteIP()
+{
+ return _remoteIP;
+}
+
+uint16_t AsyncUDPPacket::length()
+{
+ return _readLength;
+}
+
+const uint8_t *AsyncUDPPacket::data()
+{
+ return _buffer;
+}
+
+int32_t AsyncUDP::runOnce()
+{
+ if (_onPacket && udp.parsePacket() > 0) {
+ AsyncUDPPacket packet(udp);
+ _onPacket(packet);
+ }
+ return 5; // check every 5ms
+}
+
+#endif // HAS_ETHERNET
\ No newline at end of file
diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h
new file mode 100644
index 000000000..e2b406ba9
--- /dev/null
+++ b/src/platform/nrf52/AsyncUDP.h
@@ -0,0 +1,63 @@
+#ifndef ASYNC_UDP_H
+#define ASYNC_UDP_H
+
+#include "configuration.h"
+
+#if HAS_ETHERNET
+
+#include "concurrency/OSThread.h"
+#include
+#include
+#include
+#include
+#include
+
+class AsyncUDPPacket;
+
+class AsyncUDP : public Print, private concurrency::OSThread
+{
+ public:
+ AsyncUDP();
+ explicit operator bool() const { return localPort != 0; }
+
+ bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64);
+ bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port);
+
+ size_t write(uint8_t b) override;
+ size_t write(const uint8_t *data, size_t len) override;
+ void onPacket(const std::function &callback);
+
+ private:
+ EthernetUDP udp;
+ uint16_t localPort;
+ std::function _onPacket;
+ virtual int32_t runOnce() override;
+};
+
+class AsyncUDPPacket
+{
+ public:
+ AsyncUDPPacket(EthernetUDP &source);
+
+ IPAddress remoteIP();
+ uint16_t length();
+ const uint8_t *data();
+
+ private:
+ EthernetUDP &_udp;
+ IPAddress _remoteIP;
+ uint16_t _remotePort;
+ size_t _readLength = 0;
+
+ static constexpr size_t BUF_SIZE = 512;
+ uint8_t _buffer[BUF_SIZE];
+};
+
+inline bool isMulticast(const IPAddress &ip)
+{
+ return (ip[0] & 0xF0) == 0xE0;
+}
+
+#endif // HAS_ETHERNET
+
+#endif // ASYNC_UDP_H
diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h
index 684d20e84..c9938062e 100644
--- a/src/platform/nrf52/architecture.h
+++ b/src/platform/nrf52/architecture.h
@@ -49,12 +49,19 @@
#define HW_VENDOR meshtastic_HardwareModel_RAK2560
#elif defined(WISMESH_TAP)
#define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP
+#elif defined(WISMESH_TAG)
+#define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAG
#elif defined(GAT562_MESH_TRIAL_TRACKER)
#define HW_VENDOR meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER
+#elif defined(NOMADSTAR_METEOR_PRO)
+#define HW_VENDOR meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO
+// MAke sure all custom RAK4630 boards are defined before the generic RAK4630
#elif defined(RAK4630)
#define HW_VENDOR meshtastic_HardwareModel_RAK4631
#elif defined(TTGO_T_ECHO)
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO
+#elif defined(T_ECHO_LITE)
+#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE
#elif defined(ELECROW_ThinkNode_M1)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1
#elif defined(NANO_G2_ULTRA)
@@ -87,10 +94,12 @@
#define HW_VENDOR meshtastic_HardwareModel_SEEED_SOLAR_NODE
#elif defined(HELTEC_MESH_POCKET)
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_POCKET
-#elif defined(NOMADSTAR_METEOR_PRO)
-#define HW_VENDOR meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO
+#elif defined(SEEED_WIO_TRACKER_L1_EINK)
+#define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK
#elif defined(SEEED_WIO_TRACKER_L1)
#define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1
+#elif defined(HELTEC_MESH_SOLAR)
+#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR
#else
#define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN
#endif
diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp
index 1bf9a39fd..8ce74d5f7 100644
--- a/src/platform/nrf52/main-nrf52.cpp
+++ b/src/platform/nrf52/main-nrf52.cpp
@@ -282,10 +282,14 @@ void cpuDeepSleep(uint32_t msecToWake)
#if SPI_INTERFACES_COUNT > 1
SPI1.end();
#endif
- // This may cause crashes as debug messages continue to flow.
- Serial.end();
+ if (Serial) // Another check in case of disabled default serial, does nothing bad
+ Serial.end(); // This may cause crashes as debug messages continue to flow.
+
+ // This causes troubles with waking up on nrf52 (on pro-micro in particular):
+ // we have no Serial1 in use on nrf52, check Serial and GPS modules.
#ifdef PIN_SERIAL1_RX
- Serial1.end();
+ if (Serial1) // A straightforward solution to the wake from deepsleep problem
+ Serial1.end();
#endif
setBluetoothEnable(false);
@@ -319,7 +323,7 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif
#endif
-#ifdef HELTEC_MESH_NODE_T114
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR)
nrf_gpio_cfg_default(PIN_GPS_PPS);
detachInterrupt(PIN_GPS_PPS);
detachInterrupt(PIN_BUTTON1);
@@ -362,6 +366,7 @@ void cpuDeepSleep(uint32_t msecToWake)
// Resume on user button press
// https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738
constexpr uint32_t DFU_MAGIC_SKIP = 0x6d;
+ sd_power_gpregret_clr(0, 0xFF); // Clear the register before setting a new values in it for stability reasons
sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP
// FIXME, use system off mode with ram retention for key state?
@@ -378,6 +383,12 @@ void cpuDeepSleep(uint32_t msecToWake)
nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1);
#endif
+#ifdef PROMICRO_DIY_TCXO
+ nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin
+ nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge
+ nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep
+#endif
+
auto ok = sd_power_system_off();
if (ok != NRF_SUCCESS) {
LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!");
diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp
index 685f0d077..929a45d09 100644
--- a/src/platform/portduino/PortduinoGlue.cpp
+++ b/src/platform/portduino/PortduinoGlue.cpp
@@ -10,6 +10,7 @@
#include "linux/gpio/LinuxGPIOPin.h"
#include "meshUtils.h"
#include "yaml-cpp/yaml.h"
+#include
#include
#include
#include
@@ -29,11 +30,12 @@
std::map settingsMap;
std::map settingsStrings;
+portduino_config_struct portduino_config;
std::ofstream traceFile;
Ch341Hal *ch341Hal = nullptr;
char *configPath = nullptr;
char *optionMac = nullptr;
-bool forceSimulated = false;
+bool verboseEnabled = false;
const char *argp_program_version = optstr(APP_VERSION);
@@ -65,12 +67,14 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
configPath = arg;
break;
case 's':
- forceSimulated = true;
+ portduino_config.force_simradio = true;
break;
case 'h':
optionMac = arg;
break;
-
+ case 'v':
+ verboseEnabled = true;
+ break;
case ARGP_KEY_ARG:
return 0;
default:
@@ -85,6 +89,7 @@ void portduinoCustomInit()
{"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."},
{"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"},
{"sim", 's', 0, 0, "Run in Simulated radio mode"},
+ {"verbose", 'v', 0, 0, "Set log level to full debug"},
{0}};
static void *childArguments;
static char doc[] = "Meshtastic native build.";
@@ -185,7 +190,7 @@ void portduinoSetup()
YAML::Node yamlConfig;
- if (forceSimulated == true) {
+ if (portduino_config.force_simradio == true) {
settingsMap[use_simradio] = true;
} else if (configPath != nullptr) {
if (loadConfig(configPath)) {
@@ -249,16 +254,95 @@ void portduinoSetup()
std::cout << "autoconf: Could not locate CH341 device" << std::endl;
}
// Try Pi HAT+
- std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
- if (access("/proc/device-tree/hat/product", R_OK) == 0) {
- std::ifstream hatProductFile("/proc/device-tree/hat/product");
- if (hatProductFile.is_open()) {
- hatProductFile.read(autoconf_product, 95);
- hatProductFile.close();
+ if (strlen(autoconf_product) < 6) {
+ std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
+ if (access("/proc/device-tree/hat/product", R_OK) == 0) {
+ std::ifstream hatProductFile("/proc/device-tree/hat/product");
+ if (hatProductFile.is_open()) {
+ hatProductFile.read(autoconf_product, 95);
+ hatProductFile.close();
+ }
+ std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
+ } else {
+ std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
+ }
+ }
+ // attempt to load autoconf data from an EEPROM on 0x50
+ // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8
+ // :mac address :<16 random unique bytes in hexidecimal> : crc32
+ // crc32 is calculated on the eeprom string up to but not including the final colon
+ if (strlen(autoconf_product) < 6) {
+ try {
+ char *mac_start = nullptr;
+ char *devID_start = nullptr;
+ char *crc32_start = nullptr;
+ Wire.begin();
+ Wire.beginTransmission(0x50);
+ Wire.write(0x0);
+ Wire.write(0x0);
+ Wire.endTransmission();
+ Wire.requestFrom((uint8_t)0x50, (uint8_t)75);
+ uint8_t i = 0;
+ delay(100);
+ std::string autoconf_raw;
+ while (Wire.available() && i < sizeof(autoconf_product)) {
+ autoconf_product[i] = Wire.read();
+ if (autoconf_product[i] == 0xff) {
+ autoconf_product[i] = 0x0;
+ break;
+ }
+ autoconf_raw += autoconf_product[i];
+ if (autoconf_product[i] == ':') {
+ autoconf_product[i] = 0x0;
+ if (mac_start == nullptr) {
+ mac_start = autoconf_product + i + 1;
+ } else if (devID_start == nullptr) {
+ devID_start = autoconf_product + i + 1;
+ } else if (crc32_start == nullptr) {
+ crc32_start = autoconf_product + i + 1;
+ }
+ }
+ i++;
+ }
+ if (crc32_start != nullptr && strlen(crc32_start) == 8) {
+ std::string crc32_str(crc32_start);
+ uint32_t crc32_value = 0;
+
+ // convert crc32 ascii to raw uint32
+ for (int j = 0; j < 4; j++) {
+ crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8;
+ }
+ std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl;
+
+ // set the autoconf string to blank and short circuit
+ if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) {
+ std::cout << "autoconf: crc32 mismatch, dropping " << std::endl;
+ autoconf_product[0] = 0x0;
+ } else {
+ std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl;
+ if (mac_start != nullptr) {
+ std::cout << "autoconf: Found mac data " << mac_start << std::endl;
+ if (strlen(mac_start) == 12)
+ settingsStrings[mac_address] = std::string(mac_start);
+ }
+ if (devID_start != nullptr) {
+ std::cout << "autoconf: Found deviceid data " << devID_start << std::endl;
+ if (strlen(devID_start) == 32) {
+ std::string devID_str(devID_start);
+ for (int j = 0; j < 16; j++) {
+ portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16);
+ }
+ portduino_config.has_device_id = true;
+ }
+ }
+ }
+ } else {
+ std::cout << "autoconf: crc32 missing " << std::endl;
+ autoconf_product[0] = 0x0;
+ }
+ } catch (...) {
+ std::cout << "autoconf: Could not locate EEPROM" << std::endl;
}
- std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
- } else {
- std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
}
// Load the config file based on the product string
if (strlen(autoconf_product) > 0) {
@@ -417,6 +501,9 @@ void portduinoSetup()
exit(EXIT_FAILURE);
}
}
+ if (verboseEnabled && settingsMap[logoutputlevel] != level_trace) {
+ settingsMap[logoutputlevel] = level_debug;
+ }
return;
}
@@ -546,6 +633,48 @@ bool loadConfig(const char *configPath)
}
}
}
+ if (yamlConfig["Lora"]["rfswitch_table"]) {
+ portduino_config.has_rfswitch_table = true;
+ portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY;
+ portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX;
+ portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX;
+ portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP;
+ portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF;
+ portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS;
+ portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI;
+ portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE;
+
+ for (int i = 0; i < 5; i++) {
+
+ // set up the pin array first
+ if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5")
+ portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5;
+ if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6")
+ portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6;
+ if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7")
+ portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7;
+ if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8")
+ portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8;
+ if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10")
+ portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10;
+
+ // now fill in the table
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[0].values[i] = HIGH;
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[1].values[i] = HIGH;
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[2].values[i] = HIGH;
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[3].values[i] = HIGH;
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[4].values[i] = HIGH;
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[5].values[i] = HIGH;
+ if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH")
+ portduino_config.rfswitch_table[6].values[i] = HIGH;
+ }
+ }
}
if (yamlConfig["GPIO"]) {
settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC);
diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h
index 288870eef..8c36a1180 100644
--- a/src/platform/portduino/PortduinoGlue.h
+++ b/src/platform/portduino/PortduinoGlue.h
@@ -3,16 +3,21 @@
#include