diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..8529eb7fc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +open_collective: meshtastic diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index 67d002eea..f611908ee 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -27,10 +27,10 @@ inputs: description: A newline separated list of paths to store as artifacts required: false default: "" - include-web-ui: - description: Include the web UI in the build - required: false - default: "false" + # include-web-ui: + # description: Include the web UI in the build + # required: false + # default: "false" arch: description: Processor arch name required: true @@ -43,29 +43,29 @@ runs: id: base uses: ./.github/actions/setup-base - - name: Get web ui version - if: inputs.include-web-ui == 'true' - id: webver - shell: bash - run: | - echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT + # - name: Get web ui version + # if: inputs.include-web-ui == 'true' + # id: webver + # shell: bash + # run: | + # echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT - - name: Pull web ui - if: inputs.include-web-ui == 'true' - uses: dsaltares/fetch-gh-release-asset@master - with: - repo: meshtastic/web - file: build.tar - target: build.tar - token: ${{ inputs.github_token }} - version: tags/v${{ steps.webver.outputs.ver }} + # - name: Pull web ui + # if: inputs.include-web-ui == 'true' + # uses: dsaltares/fetch-gh-release-asset@master + # with: + # repo: meshtastic/web + # file: build.tar + # target: build.tar + # token: ${{ inputs.github_token }} + # version: tags/v${{ steps.webver.outputs.ver }} - - name: Unpack web ui - if: inputs.include-web-ui == 'true' - shell: bash - run: | - tar -xf build.tar -C data/static - rm build.tar + # - name: Unpack web ui + # if: inputs.include-web-ui == 'true' + # shell: bash + # run: | + # tar -xf build.tar -C data/static + # rm build.tar - name: Remove debug flags for release shell: bash diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a15b34aae..0142c57a2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,7 @@ ## ๐Ÿ™ Thank you for sending in a pull request, here's some tips to get started! ### โŒ (Please delete all these tips and replace them with your text) โŒ + - Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc... @@ -15,12 +16,12 @@ - If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes. - If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord - ## ๐Ÿค Attestations + - [ ] I have tested that my proposed changes behave as described. - [ ] I have tested that my proposed changes do not cause any obvious regressions on the following devices: - [ ] Heltec (Lora32) V3 - - [ ] LilyGo T-Deck + - [ ] LilyGo T-Deck - [ ] LilyGo T-Beam - [ ] RAK WisBlock 4631 - [ ] Seeed Studio T-1000E tracker card diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml index 4fc31f22c..32cd45000 100644 --- a/.github/workflows/build_esp32.yml +++ b/.github/workflows/build_esp32.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware.bin - ota-firmware-target: release/bleota.bin - artifact-paths: | + 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 - include-web-ui: true - arch: esp32 diff --git a/.github/workflows/build_esp32_c3.yml b/.github/workflows/build_esp32_c3.yml index 546762952..161786f99 100644 --- a/.github/workflows/build_esp32_c3.yml +++ b/.github/workflows/build_esp32_c3.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32-c3: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware-c3.bin - ota-firmware-target: release/bleota-c3.bin - artifact-paths: | + 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 - include-web-ui: true - arch: esp32c3 diff --git a/.github/workflows/build_esp32_c6.yml b/.github/workflows/build_esp32_c6.yml index 56d4d806d..90cdcc78e 100644 --- a/.github/workflows/build_esp32_c6.yml +++ b/.github/workflows/build_esp32_c6.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32-c6: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware-c3.bin - ota-firmware-target: release/bleota-c3.bin - artifact-paths: | + 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 - include-web-ui: true - arch: esp32c6 diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml index a9c067ee1..e5ed48e3e 100644 --- a/.github/workflows/build_esp32_s3.yml +++ b/.github/workflows/build_esp32_s3.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32-s3: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware-s3.bin - ota-firmware-target: release/bleota-s3.bin - artifact-paths: | + 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 - include-web-ui: true - arch: esp32s3 diff --git a/.github/workflows/build_nrf52.yml b/.github/workflows/build_nrf52.yml index 786508f86..312aeb372 100644 --- a/.github/workflows/build_nrf52.yml +++ b/.github/workflows/build_nrf52.yml @@ -11,20 +11,30 @@ permissions: read-all jobs: build-nrf52: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - build-script-path: bin/build-nrf52.sh - artifact-paths: | - release/*.hex + 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/*.zip - arch: nrf52840 + release/*.hex + release/*-ota.zip diff --git a/.github/workflows/build_rpi2040.yml b/.github/workflows/build_rpi2040.yml index 53fee34d2..2abd7a839 100644 --- a/.github/workflows/build_rpi2040.yml +++ b/.github/workflows/build_rpi2040.yml @@ -11,18 +11,28 @@ permissions: read-all jobs: build-rpi2040: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - build-script-path: bin/build-rpi2040.sh - artifact-paths: | + 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 - arch: rp2040 diff --git a/.github/workflows/build_stm32.yml b/.github/workflows/build_stm32.yml index dc469d994..10680f422 100644 --- a/.github/workflows/build_stm32.yml +++ b/.github/workflows/build_stm32.yml @@ -11,19 +11,29 @@ permissions: read-all jobs: build-stm32: - runs-on: ubuntu-latest + 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: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - build-script-path: bin/build-stm32.sh - artifact-paths: | + 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 - arch: stm32 diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 11fe2043a..63d24687b 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -1,7 +1,7 @@ name: Daily Packaging on: schedule: - - cron: 0 9 * * * + - cron: 0 2 * * * workflow_dispatch: push: branches: @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - series: [plucky, oracular, noble, jammy] + series: [plucky, noble, jammy] uses: ./.github/workflows/package_ppa.yml with: ppa_repo: ppa:meshtastic/daily diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 9b9877e04..a676efa1e 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -135,6 +135,7 @@ jobs: board: ${{ matrix.board }} build-debian-src: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/build_debian_src.yml with: series: UNRELEASED @@ -257,7 +258,6 @@ jobs: ./device-*.sh ./device-*.bat ./littlefs-*.bin - ./littlefswebui-*.bin ./bleota*bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 @@ -426,7 +426,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-firmware: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware] env: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 36ec22f17..309772b12 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -8,6 +8,7 @@ permissions: read-all jobs: trunk_check: + if: github.repository == 'meshtastic/firmware' name: Trunk Check and Upload runs-on: ubuntu-24.04 @@ -21,6 +22,7 @@ jobs: trunk-token: ${{ secrets.TRUNK_TOKEN }} trunk_upgrade: + if: github.repository == 'meshtastic/firmware' # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades name: Trunk Upgrade (PR) runs-on: ubuntu-24.04 diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index 6f216b411..ed2de1717 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - series: [plucky, oracular, noble, jammy] + series: [plucky, noble, jammy] uses: ./.github/workflows/package_ppa.yml with: ppa_repo: |- @@ -98,6 +98,7 @@ jobs: uses: peter-evans/create-pull-request@v7 with: base: ${{ github.event.repository.default_branch }} + branch: create-pull-request/bump-version title: Bump release version commit-message: automated bumps add-paths: | diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d7eef29b4..e391aa07b 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -13,6 +13,7 @@ permissions: jobs: semgrep-full: + if: github.repository == 'meshtastic/firmware' runs-on: ubuntu-24.04 container: image: semgrep/semgrep diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 5ae6bdfc9..5a11fdfa8 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -11,6 +11,7 @@ permissions: jobs: stale_issues: + if: github.repository == 'meshtastic/firmware' name: Close Stale Issues runs-on: ubuntu-latest diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 536d93665..dc05959fd 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -143,7 +143,7 @@ jobs: merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.1.0 + uses: dorny/test-reporter@v2.1.1 with: name: PlatformIO Tests path: testreport.xml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 28b6a40a5..34b28b39c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,9 +12,11 @@ permissions: jobs: native-tests: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/test_native.yml hardware-tests: + if: github.repository == 'meshtastic/firmware' runs-on: test-runner steps: - name: Checkout code diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index 5aa295b89..ccdcc19ae 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -33,6 +33,7 @@ jobs: - name: Create pull request uses: peter-evans/create-pull-request@v7 with: + branch: create-pull-request/update-protobufs title: Update protobufs and classes add-paths: | protobufs diff --git a/.gitignore b/.gitignore index b63f431d1..cc742c6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ release/ .vscode/extensions.json /compile_commands.json src/mesh/raspihttp/certificate.pem -src/mesh/raspihttp/private_key.pem \ No newline at end of file +src/mesh/raspihttp/private_key.pem + +# Ignore logo (set at build time with platformio-custom.py) +data/boot/logo.* diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index fd827e229..f0271c856 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,31 +4,31 @@ cli: plugins: sources: - id: trunk - ref: v1.7.0 + ref: v1.7.1 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.435 - - renovate@40.36.2 - - prettier@3.5.3 - - trufflehog@3.88.35 + - checkov@3.2.450 + - renovate@41.29.1 + - prettier@3.6.2 + - trufflehog@3.89.2 - yamllint@1.37.1 - - bandit@1.8.3 - - trivy@0.63.0 + - bandit@1.8.6 + - trivy@0.64.1 - taplo@0.9.3 - - ruff@0.11.12 + - ruff@0.12.2 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 - - svgo@3.3.2 + - svgo@4.0.0 - actionlint@1.7.7 - - flake8@7.2.0 + - flake8@7.3.0 - hadolint@2.12.1-beta - shfmt@3.6.0 - shellcheck@0.10.0 - black@25.1.0 - git-diff-check - - gitleaks@8.26.0 + - gitleaks@8.27.2 - clang-format@16.0.3 ignore: - linters: [ALL] diff --git a/README.md b/README.md index f34bf1839..a53fe9646 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,4 @@ Join our community and help improve Meshtastic! ๐Ÿš€ ## Stats ![Alt](https://repobeats.axiom.co/api/embed/8025e56c482ec63541593cc5bd322c19d5c0bdcf.svg "Repobeats analytics image") + diff --git a/alpine.Dockerfile b/alpine.Dockerfile index bf7cad6d4..670736241 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -3,7 +3,7 @@ # trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions -FROM python:3.13-alpine3.21 AS builder +FROM python:3.13-alpine3.22 AS builder ARG PIO_ENV=native ENV PIP_ROOT_USER_ACTION=ignore @@ -27,7 +27,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \ # ##### PRODUCTION BUILD ############# -FROM alpine:3.21 +FROM alpine:3.22 LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.url="https://meshtastic.org" \ diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index a6eff7bf9..6b9ebcb24 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -4,7 +4,7 @@ extends = arduino_base custom_esp32_kind = esp32 platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.10.0 + platformio/espressif32@6.11.0 build_src_filter = ${arduino_base.build_src_filter} - - - - - @@ -49,13 +49,13 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/896f1771ceb5979987a0b41028bf1b4e7aad419b.zip + https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib - lewisxhe/XPowersLib@^0.2.7 + lewisxhe/XPowersLib@0.3.0 # 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/esp32/esp32c6.ini b/arch/esp32/esp32c6.ini index 26b5c0f5b..1afb9b547 100644 --- a/arch/esp32/esp32c6.ini +++ b/arch/esp32/esp32c6.ini @@ -28,7 +28,7 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib - lewisxhe/XPowersLib@^0.2.7 + lewisxhe/XPowersLib@0.3.0 # 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/portduino/portduino.ini b/arch/portduino/portduino.ini index a19c50319..03a8a6583 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/622341c6de8a239704318b10c3dbb00c21a3eab3.zip + https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip framework = arduino build_src_filter = @@ -30,6 +30,8 @@ lib_deps = lovyan03/LovyanGFX@^1.2.0 # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip + # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library + adafruit/Adafruit seesaw Library@1.7.9 build_flags = ${arduino_base.build_flags} @@ -48,4 +50,7 @@ build_flags = -std=gnu17 -std=c++17 -lib_ignore = Adafruit NeoPixel +lib_ignore = + Adafruit NeoPixel + Adafruit ST7735 and ST7789 Library + SD diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index 60bc41d23..355a2e313 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.1.0 + platformio/ststm32@19.2.0 platform_packages = # TODO renovate platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip @@ -16,14 +16,17 @@ build_flags = ${arduino_base.build_flags} -flto -Isrc/platform/stm32wl -g - -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - -DMESHTASTIC_EXCLUDE_INPUTBROKER - -DMESHTASTIC_EXCLUDE_I2C - -DMESHTASTIC_EXCLUDE_POWERMON - -DMESHTASTIC_EXCLUDE_SCREEN - -DMESHTASTIC_EXCLUDE_MQTT - -DMESHTASTIC_EXCLUDE_BLUETOOTH - -DMESHTASTIC_EXCLUDE_GPS + -DMESHTASTIC_EXCLUDE_AUDIO=1 + -DMESHTASTIC_EXCLUDE_ATAK=1 ; ATAK is quite big, disable it for big flash savings. + -DMESHTASTIC_EXCLUDE_INPUTBROKER=1 + -DMESHTASTIC_EXCLUDE_POWERMON=1 + -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. + -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. ;-DDEBUG_MUTE -fmerge-all-constants -ffunction-sections @@ -38,10 +41,10 @@ debug_tool = stlink lib_deps = ${env.lib_deps} ${radiolib_base.lib_deps} + # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip lib_ignore = - Wire I2CKeyPad - mathertel/OneButton@2.6.1 + OneButton diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh index a0635e997..92836db23 100755 --- a/bin/build-esp32.sh +++ b/bin/build-esp32.sh @@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale -platformio pkg update -e $1 +platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f .pio/build/$1/firmware.* @@ -34,11 +34,12 @@ SRCBIN=.pio/build/$1/firmware.bin cp $SRCBIN $OUTDIR/$basename-update.bin echo "Building Filesystem for ESP32 targets" -pio run --environment $1 -t buildfs -cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin -# Remove webserver files from the filesystem and rebuild -ls -l data/static # Diagnostic list of files -rm -rf data/static +# If you want to build the webui, uncomment the following lines +# pio run --environment $1 -t buildfs +# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin +# # Remove webserver files from the filesystem and rebuild +# ls -l data/static # Diagnostic list of files +# rm -rf data/static pio run --environment $1 -t buildfs cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin cp bin/device-install.* $OUTDIR diff --git a/bin/build-native.sh b/bin/build-native.sh index 51379ad76..fff86e87e 100755 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -25,7 +25,7 @@ mkdir -p $OUTDIR/ rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale -pio pkg update --environment "$PIO_ENV" || platformioFailed +pio pkg install --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)" cp bin/native-install.* $OUTDIR diff --git a/bin/build-nrf52.sh b/bin/build-nrf52.sh index 9d0b3dfdd..deca209d2 100755 --- a/bin/build-nrf52.sh +++ b/bin/build-nrf52.sh @@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale -platformio pkg update -e $1 +platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f .pio/build/$1/firmware.* diff --git a/bin/build-rpi2040.sh b/bin/build-rp2xx0.sh similarity index 96% rename from bin/build-rpi2040.sh rename to bin/build-rp2xx0.sh index dad6a7e67..cb4865914 100755 --- a/bin/build-rpi2040.sh +++ b/bin/build-rp2xx0.sh @@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale -platformio pkg update -e $1 +platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f .pio/build/$1/firmware.* diff --git a/bin/build-stm32.sh b/bin/build-stm32wl.sh similarity index 95% rename from bin/build-stm32.sh rename to bin/build-stm32wl.sh index 76c5a75fb..f62df4842 100755 --- a/bin/build-stm32.sh +++ b/bin/build-stm32wl.sh @@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale -platformio pkg update -e $1 +platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f .pio/build/$1/firmware.* diff --git a/bin/check-all.sh b/bin/check-all.sh index d1b50a8aa..29d6b5532 100755 --- a/bin/check-all.sh +++ b/bin/check-all.sh @@ -23,4 +23,4 @@ for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done -pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high +pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 55e8648d9..b4cc81792 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -96,9 +96,9 @@ Lora: ### Some devices, like the pinedio, may require spidev0.1 as a workaround. # spidev: spidev0.0 -### Define GPIO buttons here: +### Deprecated location for User Button: -GPIO: +#GPIO: # User: 6 ### Define GPS @@ -115,17 +115,6 @@ I2C: Display: -### Waveshare 1.44inch LCD HAT -# Panel: ST7735S -# CS: 8 #Chip Select -# DC: 25 # Data/Command pin -# Backlight: 24 -# Width: 128 -# Height: 128 -# Reset: 27 -# OffsetX: 0 -# OffsetY: 0 - ### Adafruit PiTFT 2.8 TFT+Touchscreen # Panel: ILI9341 # CS: 8 @@ -180,6 +169,16 @@ Input: # KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd +### Standard User Button Config +# UserButton: 6 + +### Trackball/Joystick input +# TrackballUp: 6 +# TrackballDown: 19 +# TrackballLeft: 5 +# TrackballRight: 26 +# TrackballPress: 13 + ### Logging: @@ -200,6 +199,10 @@ HostMetrics: # UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString +Config: +# DisplayMode: TWOCOLOR # uncomment to force BaseUI +# DisplayMode: COLOR # uncomment to force MUI + General: MaxNodes: 200 MaxMessageQueue: 100 diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml new file mode 100644 index 000000000..d37f6cf6a --- /dev/null +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -0,0 +1,26 @@ +### Waveshare 1.44inch LCD HAT +Display: + Panel: ST7735S + spidev: spidev0.0 # Specify either the spidev here, or the CS below +# CS: 8 #Chip Select # Optional, as this is the default pin for spidev0.0 + DC: 25 # Data/Command pin + Backlight: 24 + Width: 128 + Height: 128 + Reset: 27 + OffsetX: 2 + OffsetY: 1 + + +# OffsetY: 31 # These two options are used to properly flip the screen 180 degrees +# OffsetRotate: 3 + + +Input: + TrackballUp: 6 + TrackballDown: 19 + TrackballLeft: 5 + TrackballRight: 26 + TrackballPress: 13 + TrackballDirection: FALLING +# User: 21 diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml index 554116b57..b47b5c996 100644 --- a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -6,6 +6,6 @@ Lora: IRQ: 16 Busy: 20 Reset: 24 - TXen: 13 + RXen: 12 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true diff --git a/bin/config.d/lora-RAK6421.yaml b/bin/config.d/lora-RAK6421.yaml new file mode 100644 index 000000000..bbf38a474 --- /dev/null +++ b/bin/config.d/lora-RAK6421.yaml @@ -0,0 +1,21 @@ +Lora: + + ### RAK13300in Slot 1 + Module: sx1262 + IRQ: 22 #IO6 + Reset: 16 # IO4 + Busy: 24 # IO5 + # Ant_sw: 13 # IO3 + 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 diff --git a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml new file mode 100644 index 000000000..2fd128ce8 --- /dev/null +++ b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml @@ -0,0 +1,18 @@ +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + gpiochip: 0 + MOSI: 12 + MISO: 13 + IRQ: 1 + Busy: 23 + Reset: 22 + RXen: 0 + gpiochip: 1 + CS: 9 + SCK: 11 +# TXen: bridge to DIO2 on E22 module + SX126X_MAX_POWER: 22 + spidev: spidev1.0 + spiSpeed: 2000000 diff --git a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml new file mode 100644 index 000000000..37d7e27d2 --- /dev/null +++ b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml @@ -0,0 +1,52 @@ +# https://www.waveshare.com/pico-lora-sx1262-868m.htm +# http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-Zero-3.html +# +# See Orange Pi Zero3 manual, chapter 3.16, page 124 for 26-pin header pinout +# +# Pin Connection +# Waveshare Orange Pi Zero3 +# 36 3.3V 17 +# 15 MOSI 19 +# 16 MISO 21 +# 14 CLK 23 +# 38 GND 25 +# 4 BUSY 18 +# 20 RESET 22 +# 5 CS 24 +# 26 DIO1/IRQ 26 + +Lora: + Module: sx1262 # Waveshare Raspberry Pico Lora module + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + # Specify either the spidev1_1 or the CS below, not both! + # On DietPi Linux, when using the user overlay dietpi-spi1_1.dtbo, CS will be configured with spidev1.1 + spidev: spidev1.1 # See Orange Pi Zero3 manual, chapter 3.18.3, page 130 +# CS: # CS PIN_24 -> chip 1, line 233 +# pin: 24 +# gpiochip: 1 +# line: 233 + SCK: # SCK PIN_23 -> chip 1, line 230 + pin: 23 + gpiochip: 1 + line: 230 + Busy: # BUSY PIN_18 -> chip 1, line 78 + pin: 18 + gpiochip: 1 + line: 78 + MOSI: # MOSI PIN_19 -> chip 1, line 231 + pin: 19 + gpiochip: 1 + line: 231 + MISO: # MISO PIN_21 -> chip 1, line 232 + pin: 21 + gpiochip: 1 + line: 232 + Reset: # NRST PIN_22 -> chip 1, line 71 + pin: 22 + gpiochip: 1 + line: 71 + IRQ: # DIO1 PIN_26 -> chip 1, line 74 + pin: 26 + gpiochip: 1 + line: 74 diff --git a/bin/device-install.bat b/bin/device-install.bat index 816d2fbba..12bfd4f6e 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -5,7 +5,6 @@ TITLE Meshtastic device-install SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" -SET "WEB_APP=0" SET "TFT_BUILD=0" SET "BIGDB8=0" SET "BIGDB16=0" @@ -25,7 +24,7 @@ GOTO getopts :help ECHO Flash image file to device, but first erasing and writing system information. ECHO. -ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web) [--1200bps-reset] +ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset] ECHO. ECHO Options: ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required) @@ -35,13 +34,12 @@ ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python) ECHO If supplied the script will use python. ECHO If not supplied the script will try to find esptool in Path. -ECHO --web Enable WebUI. (default: false) ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset) ECHO Some hardware requires this twice. ECHO. ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 -ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web +ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 GOTO eof :version @@ -61,7 +59,6 @@ IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT -IF /I "%~1"=="--web" SET "WEB_APP=1" IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1" SHIFT GOTO getopts @@ -153,9 +150,6 @@ IF %BPS_RESET% EQU 1 ( @REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3 IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!" - IF %WEB_APP% EQU 1 ( - CALL :LOG_MESSAGE ERROR "Cannot enable WebUI (--web) and MUI." & GOTO eof - ) SET "TFT_BUILD=1" ) ELSE ( CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!" @@ -209,13 +203,8 @@ SET "OTA_FILENAME=bleota.bin" :end_loop_c3 CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" -@REM Check if (--web) is enabled and prefix BASENAME with "littlefswebui-" else "littlefs-". -IF %WEB_APP% EQU 1 ( - CALL :LOG_MESSAGE INFO "WebUI selected." - SET "SPIFFS_FILENAME=littlefswebui-%BASENAME%" -) ELSE ( - SET "SPIFFS_FILENAME=littlefs-%BASENAME%" -) +@REM Set SPIFFS filename with "littlefs-" prefix. +SET "SPIFFS_FILENAME=littlefs-%BASENAME%" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!" @REM Default offsets. diff --git a/bin/device-install.sh b/bin/device-install.sh index 76765bb5f..4674113b6 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -1,7 +1,6 @@ #!/bin/bash PYTHON=${PYTHON:-$(which python3 python | head -n 1)} -WEB_APP=false BPS_RESET=false TFT_BUILD=false MCU="" @@ -33,93 +32,92 @@ BIGDB_16MB=( "ESP32-S3-Pico" "m5stack-cores3" "station-g2" - "t-eth-elite" - "t-watch-s3" + "t-eth-elite" + "t-watch-s3" + "elecrow-adv-35-tft" + "elecrow-adv-24-28-tft" + "elecrow-adv1-43-50-70-tft" ) S3_VARIANTS=( - "s3" - "-v3" - "t-deck" - "wireless-paper" - "wireless-tracker" - "station-g2" - "unphone" - "t-eth-elite" - "mesh-tab" - "dreamcatcher" - "ESP32-S3-Pico" - "seeed-sensecap-indicator" - "heltec_capsule_sensor_v3" - "vision-master" - "icarus" - "tracksenger" - "elecrow-adv" + "s3" + "-v3" + "t-deck" + "wireless-paper" + "wireless-tracker" + "station-g2" + "unphone" + "t-eth-elite" + "mesh-tab" + "dreamcatcher" + "ESP32-S3-Pico" + "seeed-sensecap-indicator" + "heltec_capsule_sensor_v3" + "vision-master" + "icarus" + "tracksenger" + "elecrow-adv" ) # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then - ESPTOOL_CMD="$PYTHON -m esptool" + ESPTOOL_CMD="$PYTHON -m esptool" elif command -v esptool >/dev/null 2>&1; then - ESPTOOL_CMD="esptool" + ESPTOOL_CMD="esptool" elif command -v esptool.py >/dev/null 2>&1; then - ESPTOOL_CMD="esptool.py" + ESPTOOL_CMD="esptool.py" else - echo "Error: esptool not found" - exit 1 + echo "Error: esptool not found" + exit 1 fi set -e # Usage info show_help() { - cat <&2 - exit 1 - ;; - esac - shift # Move to the next argument + case "$1" in + -h | --help) + show_help + exit 0 + ;; + -p) + ESPTOOL_CMD="$ESPTOOL_CMD --port $2" + shift + ;; + -P) + PYTHON="$2" + shift + ;; + -f) + FILENAME="$2" + shift + ;; + --1200bps-reset) + BPS_RESET=true + ;; + --) # Stop parsing options + shift + break + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac + shift # Move to the next argument done if [[ $BPS_RESET == true ]]; then @@ -127,100 +125,92 @@ if [[ $BPS_RESET == true ]]; then exit 0 fi -[ -z "$FILENAME" -a -n "$1" ] && { - FILENAME=$1 - shift +[ -z "$FILENAME" ] && [ -n "$1" ] && { + FILENAME="$1" + shift } -if [[ $FILENAME != firmware-* ]]; then +if [[ "$FILENAME" != firmware-* ]]; then echo "Filename must be a firmware-* file." exit 1 fi -# Check if FILENAME contains "-tft-" and prevent web/mui comingling. -if [[ ${FILENAME//-tft-/} != "$FILENAME" ]]; then - TFT_BUILD=true - if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then - echo "Cannot enable WebUI (--web) and MUI." - exit 1 - fi +# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. +if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then + TFT_BUILD=true fi # Extract BASENAME from %FILENAME% for later use. BASENAME="${FILENAME/firmware-/}" if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then - # Default littlefs* offset (--web). - OFFSET=0x300000 + # Default littlefs* offset. + OFFSET=0x300000 - # Default OTA Offset - OTA_OFFSET=0x260000 + # Default OTA Offset + OTA_OFFSET=0x260000 - # littlefs* offset for BigDB 8mb and OTA OFFSET. - for variant in "${BIGDB_8MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0x670000 - OTA_OFFSET=0x340000 - fi - done + # littlefs* offset for BigDB 8mb and OTA OFFSET. + for variant in "${BIGDB_8MB[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + OFFSET=0x670000 + OTA_OFFSET=0x340000 + fi + done - # littlefs* offset for BigDB 16mb and OTA OFFSET. - for variant in "${BIGDB_16MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0xc90000 - OTA_OFFSET=0x650000 - fi - done + # littlefs* offset for BigDB 16mb and OTA OFFSET. + for variant in "${BIGDB_16MB[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + OFFSET=0xc90000 + OTA_OFFSET=0x650000 + fi + done - # Account for S3 board's different OTA partition - # FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable - for variant in "${S3_VARIANTS[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - MCU="esp32s3" - fi - done + # Account for S3 board's different OTA partition + # FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable + for variant in "${S3_VARIANTS[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + MCU="esp32s3" + fi + done - if [ "$MCU" != "esp32s3" ]; then - if [ -n "${FILENAME##*"esp32c3"*}" ]; then - OTAFILE=bleota.bin - else - OTAFILE=bleota-c3.bin - fi - else - OTAFILE=bleota-s3.bin - fi + if [ "$MCU" != "esp32s3" ]; then + if [ -n "${FILENAME##*"esp32c3"*}" ]; then + OTAFILE=bleota.bin + else + OTAFILE=bleota-c3.bin + fi + else + OTAFILE=bleota-s3.bin + fi - # Check if WEB_APP (--web) is enabled and add "littlefswebui-" to BASENAME else "littlefs-". - if [ "$WEB_APP" = true ]; then - SPIFFSFILE=littlefswebui-${BASENAME} - else - SPIFFSFILE=littlefs-${BASENAME} - fi + # Set SPIFFS filename with "littlefs-" prefix. + SPIFFSFILE=littlefs-${BASENAME} - if [[ ! -f $FILENAME ]]; then - echo "Error: file ${FILENAME} wasn't found. Terminating." - exit 1 - fi - if [[ ! -f $OTAFILE ]]; then - echo "Error: file ${OTAFILE} wasn't found. Terminating." - exit 1 - fi - if [[ ! -f $SPIFFSFILE ]]; then - echo "Error: file ${SPIFFSFILE} wasn't found. Terminating." - exit 1 - fi + if [[ ! -f "$FILENAME" ]]; then + echo "Error: file ${FILENAME} wasn't found. Terminating." + exit 1 + fi + if [[ ! -f "$OTAFILE" ]]; then + echo "Error: file ${OTAFILE} wasn't found. Terminating." + exit 1 + fi + if [[ ! -f "$SPIFFSFILE" ]]; then + echo "Error: file ${SPIFFSFILE} wasn't found. Terminating." + exit 1 + fi - echo "Trying to flash ${FILENAME}, but first erasing and writing system information" - $ESPTOOL_CMD erase_flash - $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" - echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" - $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" - echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" - $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" + echo "Trying to flash ${FILENAME}, but first erasing and writing system information" + $ESPTOOL_CMD erase_flash + $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" + echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" + $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" + echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" + $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" else - show_help - echo "Invalid file: ${FILENAME}" + show_help + echo "Invalid file: ${FILENAME}" fi exit 0 diff --git a/bin/device-update.sh b/bin/device-update.sh index c32b953e6..ce0b5e434 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -18,8 +18,8 @@ fi # Usage info show_help() { cat << EOF -Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--change-mode] -Flash image file to device, leave existing system intact. +Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--change-mode] +Flash image file to device, leave existing system intact." -h Display this help and exit -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). @@ -30,6 +30,17 @@ Flash image file to device, leave existing system intact. EOF } +# Check for --change-mode and remove it from arguments +NEW_ARGS=() +for arg in "$@"; do + if [ "$arg" = "--change-mode" ]; then + CHANGE_MODE=true + else + NEW_ARGS+=("$arg") + fi +done + +set -- "${NEW_ARGS[@]}" while getopts ":hp:P:f:" opt; do case "${opt}" in @@ -38,16 +49,13 @@ while getopts ":hp:P:f:" opt; do exit 0 ;; p) ESPTOOL_CMD="$ESPTOOL_CMD --port ${OPTARG}" - ;; + ;; P) PYTHON=${OPTARG} ;; f) FILENAME=${OPTARG} ;; - --change-mode) - CHANGE_MODE=true - ;; *) - echo "Invalid flag." + echo "Invalid flag." show_help >&2 exit 1 ;; @@ -55,22 +63,22 @@ while getopts ":hp:P:f:" opt; do done shift "$((OPTIND-1))" -if [[ $CHANGE_MODE == true ]]; then +if [ "$CHANGE_MODE" = true ]; then $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status exit 0 fi -[ -z "$FILENAME" -a -n "$1" ] && { - FILENAME=$1 +[ -z "$FILENAME" ] && [ -n "$1" ] && { + FILENAME="$1" shift } if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then - printf "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud 115200 write_flash 0x10000 ${FILENAME} + echo "Trying to flash update ${FILENAME}" + $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}" else - show_help - echo "Invalid file: ${FILENAME}" + show_help + echo "Invalid file: ${FILENAME}" fi exit 0 diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index 7513ccff5..0ce6b0f6b 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -27,7 +27,7 @@ for subdir, dirs, files in os.walk(rootdir): if c.startswith("env:"): section = config[c].name[4:] if "extends" in config[config[c].name]: - if config[config[c].name]["extends"] == options[0] + "_base": + 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" diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 40f86fb0b..291fe7a7c 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,24 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.3 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.2 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.13 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.12 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.11 diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 600f9447f..be2a9ab71 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -131,3 +131,33 @@ for lb in env.GetLibBuilders(): if lb.name == "meshtastic-device-ui": lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])]) break + +# Get the display resolution from macros +def get_display_resolution(build_flags): + # Check "DISPLAY_SIZE" to determine the screen resolution + for flag in build_flags: + if isinstance(flag, tuple) and flag[0] == "DISPLAY_SIZE": + screen_width, screen_height = map(int, flag[1].split("x")) + return screen_width, screen_height + print("No screen resolution defined in build_flags. Please define DISPLAY_SIZE.") + exit(1) + +def load_boot_logo(source, target, env): + build_flags = env.get("CPPDEFINES", []) + logo_w, logo_h = get_display_resolution(build_flags) + print(f"TFT build with {logo_w}x{logo_h} resolution detected") + + # Load the boot logo from `branding/logo_x.png` if it exists + source_path = join(env["PROJECT_DIR"], "branding", f"logo_{logo_w}x{logo_h}.png") + dest_dir = join(env["PROJECT_DIR"], "data", "boot") + dest_path = join(dest_dir, "logo.png") + if env.File(source_path).exists(): + print(f"Loading boot logo from {source_path}") + # Prepare the destination + env.Execute(f"mkdir -p {dest_dir} && rm -f {dest_path}") + # Copy the logo to the `data/boot` directory + env.Execute(f"cp {source_path} {dest_path}") + +# Load the boot logo on TFT builds +if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): + env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo) diff --git a/boards/ThinkNode-M1.json b/boards/ThinkNode-M1.json index e55da3ec7..2d6dbc352 100644 --- a/boards/ThinkNode-M1.json +++ b/boards/ThinkNode-M1.json @@ -48,6 +48,6 @@ "require_upload_port": true, "wait_for_upload_port": true }, - "url": "FIXME", + "url": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html", "vendor": "ELECROW" } diff --git a/boards/gat562_mesh_trial_tracker.json b/boards/gat562_mesh_trial_tracker.json new file mode 100644 index 000000000..a3fb8a264 --- /dev/null +++ b/boards/gat562_mesh_trial_tracker.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": "GAT562 Mesh Trial Tracker", + "mcu": "nrf52840", + "variant": "gat562_mesh_trial_tracker", + "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": "GAT562 Mesh Trial Tracker", + "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": "http://www.gat-iot.com/", + "vendor": "GAT-IOT" +} diff --git a/boards/heltec_mesh_node_t114.json b/boards/heltec_mesh_node_t114.json index 2bd306eb9..eda0ac3df 100644 --- a/boards/heltec_mesh_node_t114.json +++ b/boards/heltec_mesh_node_t114.json @@ -10,7 +10,8 @@ "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], - ["0x239A", "0x002A"] + ["0x239A", "0x002A"], + ["0x2886", "0x1667"] ], "usb_product": "HT-n5262", "mcu": "nrf52840", @@ -48,6 +49,6 @@ "require_upload_port": true, "wait_for_upload_port": true }, - "url": "FIXME", + "url": "https://heltec.org/project/mesh-node-t114/", "vendor": "Heltec" } diff --git a/boards/seeed_wio_tracker_L1.json b/boards/seeed_wio_tracker_L1.json index 7c7bc62fa..e2bb93573 100644 --- a/boards/seeed_wio_tracker_L1.json +++ b/boards/seeed_wio_tracker_L1.json @@ -7,7 +7,10 @@ "cpu": "cortex-m4", "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", "f_cpu": "64000000L", - "hwids": [["0x2886", "0x1668"]], + "hwids": [ + ["0x2886", "0x1668"], + ["0x2886", "0x1667"] + ], "usb_product": "TRACKER L1", "mcu": "nrf52840", "variant": "seeed_wio_tracker_L1", diff --git a/boards/seeed_xiao_nrf52840_kit.json b/boards/seeed_xiao_nrf52840_kit.json index 4c5fdbeda..676733874 100644 --- a/boards/seeed_xiao_nrf52840_kit.json +++ b/boards/seeed_xiao_nrf52840_kit.json @@ -7,9 +7,7 @@ "cpu": "cortex-m4", "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", "f_cpu": "64000000L", - "hwids": [ - ["0x2886", "0x0166"] - ], + "hwids": [["0x2886", "0x0166"]], "usb_product": "XIAO-BOOT", "mcu": "nrf52840", "variant": "seeed_xiao_nrf52840_kit", diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index 2be716e22..9e8870041 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -11,7 +11,8 @@ ["0x239A", "0x8029"], ["0x239A", "0x0029"], ["0x239A", "0x002A"], - ["0x239A", "0x802A"] + ["0x239A", "0x802A"], + ["0x2886", "0x0057"] ], "usb_product": "T1000-E-BOOT", "mcu": "nrf52840", diff --git a/boards/wiscore_rak3312.json b/boards/wiscore_rak3312.json new file mode 100644 index 000000000..192e1c03c --- /dev/null +++ b/boards/wiscore_rak3312.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DRAK3312", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "rak3312" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "WisCore RAK3312 Board", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.rakwireless.com/en-us", + "vendor": "rakwireless" +} diff --git a/branding/README.md b/branding/README.md new file mode 100644 index 000000000..3a558bf20 --- /dev/null +++ b/branding/README.md @@ -0,0 +1,17 @@ +# Meshtastic Branding / Whitelabeling + +This directory is consumed during the creation of **event** firmware. + +`bin/platformio-custom.py` determines the display resolution, and locates the corresponding `logo_x.png`. + +Ex: + +- `logo_800x480.png` +- `logo_480x480.png` +- `logo_480x320.png` +- `logo_320x480.png` +- `logo_320x240.png` + +This file is copied to `data/boot/logo.png` before filesytem image compilation. + +For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33). diff --git a/debian/changelog b/debian/changelog index 4b67eecd4..b5009028a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.6.11.0) UNRELEASED; urgency=medium +meshtasticd (2.7.3.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -19,4 +19,16 @@ meshtasticd (2.6.11.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Mon, 02 Jun 2025 20:00:55 +0000 + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ Ubuntu ] + * GitHub Actions Automatic version bump + + -- Ubuntu Thu, 10 Jul 2025 16:29:27 +0000 diff --git a/platformio.ini b/platformio.ini index 4ed55ece0..2930d1a8d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -49,10 +49,11 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_DROPZONE=1 -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 -DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1 - -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware + -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 - #-DBUILD_EPOCH=$UNIX_TIME - #-D OLED_PL=1 + -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage + #-DBUILD_EPOCH=$UNIX_TIME + #-D OLED_PL=1 monitor_speed = 115200 monitor_filters = direct @@ -104,18 +105,18 @@ lib_deps = [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - jgromes/RadioLib@7.1.2 + jgromes/RadioLib@7.2.1 [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/649e0953508ee4aabf1171519ee2eb69fb125647.zip + https://github.com/meshtastic/device-ui/archive/86a09a7360f92d10053fbbf8d74f67f85b0ceb09.zip ; Common libs for environmental measurements in telemetry module [environmental_base] lib_deps = # renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO - adafruit/Adafruit BusIO@1.17.1 + adafruit/Adafruit BusIO@1.17.2 # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor adafruit/Adafruit Unified Sensor@1.1.15 # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library @@ -129,7 +130,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library adafruit/Adafruit MCP9808 Library@2.0.2 # renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library - adafruit/Adafruit INA260 Library@1.5.2 + adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor @@ -166,8 +167,10 @@ lib_deps = adafruit/Adafruit LTR390 Library@1.1.2 # renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075 adafruit/Adafruit PCT2075@1.0.5 - - ; (not included in native / portduino) + # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 + dfrobot/DFRobot_BMM150@1.0.0 + +; (not included in native / portduino) [environmental_extra] lib_deps = # renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library @@ -193,4 +196,8 @@ lib_deps = # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library boschsensortec/BME68x Sensor Library@1.3.40408 # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master - https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip \ No newline at end of file + https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip + # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core + sensirion/Sensirion Core@0.7.1 + # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x + sensirion/Sensirion I2C SCD4x@1.1.0 diff --git a/protobufs b/protobufs index 24c7a3d28..584f0a3a3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 24c7a3d287a4bd269ce191827e5dabd8ce8f57a7 +Subproject commit 584f0a3a359103acf0bfce506c1b1fc32c639841 diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index bff8846d6..e4ef3b443 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -59,82 +59,82 @@ class AmbientLightingThread : public concurrency::OSThread return; } LOG_DEBUG("AmbientLighting init"); -#if defined(HAS_NCP5623) || defined(HAS_LP5562) +#ifdef HAS_NCP5623 if (_type == ScanI2C::NCP5623) { rgb.begin(); #endif #ifdef HAS_LP5562 - } else if (_type == ScanI2C::LP5562) { - rgbw.begin(); + if (_type == ScanI2C::LP5562) { + rgbw.begin(); #endif #ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); + pinMode(RGBLED_RED, OUTPUT); + pinMode(RGBLED_GREEN, OUTPUT); + pinMode(RGBLED_BLUE, OUTPUT); #endif #ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); + pixels.begin(); // Initialise the pixel(s) + pixels.clear(); // Set all pixel colors to 'off' + pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - setLighting(); + setLighting(); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) - } + } #endif - } + } - protected: - int32_t runOnce() override - { + protected: + int32_t runOnce() override + { #ifdef HAS_RGB_LED #if defined(HAS_NCP5623) || defined(HAS_LP5562) - if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { + if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif - setLighting(); - return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification + setLighting(); + return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) + } +#endif +#endif + return disable(); } -#endif -#endif - return disable(); - } - // When shutdown() is issued, setLightingOff will be called. - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &AmbientLightingThread::setLightingOff); + // When shutdown() is issued, setLightingOff will be called. + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &AmbientLightingThread::setLightingOff); - private: - ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; + private: + ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; - // Turn RGB lighting off, is used in junction to shutdown() - int setLightingOff(void *unused) - { + // Turn RGB lighting off, is used in junction to shutdown() + int setLightingOff(void *unused) + { #ifdef HAS_NCP5623 - rgb.setCurrent(0); - rgb.setRed(0); - rgb.setGreen(0); - rgb.setBlue(0); - LOG_INFO("OFF: NCP5623 Ambient lighting"); + rgb.setCurrent(0); + rgb.setRed(0); + rgb.setGreen(0); + rgb.setBlue(0); + LOG_INFO("OFF: NCP5623 Ambient lighting"); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(0); - rgbw.setRed(0); - rgbw.setGreen(0); - rgbw.setBlue(0); - rgbw.setWhite(0); - LOG_INFO("OFF: LP5562 Ambient lighting"); + rgbw.setCurrent(0); + rgbw.setRed(0); + rgbw.setGreen(0); + rgbw.setBlue(0); + rgbw.setWhite(0); + LOG_INFO("OFF: LP5562 Ambient lighting"); #endif #ifdef HAS_NEOPIXEL - pixels.clear(); - pixels.show(); - LOG_INFO("OFF: NeoPixel Ambient lighting"); + pixels.clear(); + pixels.show(); + LOG_INFO("OFF: NeoPixel Ambient lighting"); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - 0); - analogWrite(RGBLED_GREEN, 255 - 0); - analogWrite(RGBLED_BLUE, 255 - 0); - LOG_INFO("OFF: Ambient light RGB Common Anode"); + analogWrite(RGBLED_RED, 255 - 0); + analogWrite(RGBLED_GREEN, 255 - 0); + analogWrite(RGBLED_BLUE, 255 - 0); + LOG_INFO("OFF: Ambient light RGB Common Anode"); #elif defined(RGBLED_RED) analogWrite(RGBLED_RED, 0); analogWrite(RGBLED_GREEN, 0); @@ -142,56 +142,57 @@ class AmbientLightingThread : public concurrency::OSThread LOG_INFO("OFF: Ambient light RGB Common Cathode"); #endif #ifdef UNPHONE - unphone.rgb(0, 0, 0); - LOG_INFO("OFF: unPhone Ambient lighting"); + unphone.rgb(0, 0, 0); + LOG_INFO("OFF: unPhone Ambient lighting"); #endif - return 0; - } + return 0; + } - void setLighting() - { + void setLighting() + { #ifdef HAS_NCP5623 - rgb.setCurrent(moduleConfig.ambient_lighting.current); - rgb.setRed(moduleConfig.ambient_lighting.red); - rgb.setGreen(moduleConfig.ambient_lighting.green); - rgb.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgb.setCurrent(moduleConfig.ambient_lighting.current); + rgb.setRed(moduleConfig.ambient_lighting.red); + rgb.setGreen(moduleConfig.ambient_lighting.green); + rgb.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", + moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(moduleConfig.ambient_lighting.current); - rgbw.setRed(moduleConfig.ambient_lighting.red); - rgbw.setGreen(moduleConfig.ambient_lighting.green); - rgbw.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgbw.setCurrent(moduleConfig.ambient_lighting.current); + rgbw.setRed(moduleConfig.ambient_lighting.red); + rgbw.setGreen(moduleConfig.ambient_lighting.green); + rgbw.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, + moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue), - 0, NEOPIXEL_COUNT); + pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue), + 0, NEOPIXEL_COUNT); // RadioMaster Bandit has addressable LED at the two buttons // this allow us to set different lighting for them in variant.h file. #ifdef RADIOMASTER_900_BANDIT #if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX) - pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); + pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) - pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); + pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); #endif #endif - pixels.show(); - LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue); + pixels.show(); + LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", + moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); + analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); + analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #elif defined(RGBLED_RED) analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); @@ -200,11 +201,12 @@ class AmbientLightingThread : public concurrency::OSThread moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef UNPHONE - unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif - } -}; + } + }; } // namespace concurrency diff --git a/src/AudioThread.h b/src/AudioThread.h index 04ff64a6e..286729909 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -47,6 +47,20 @@ class AudioThread : public concurrency::OSThread setCPUFast(false); } + void readAloud(const char *text) + { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; + } + + ESP8266SAM *sam = new ESP8266SAM; + sam->Say(audioOut, text); + delete sam; + setCPUFast(false); + } + protected: int32_t runOnce() override { diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 526b6f243..f6bb43cc2 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -88,10 +88,16 @@ class BluetoothStatus : public Status break; case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); +#ifdef BLE_LED + digitalWrite(BLE_LED, HIGH); +#endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); +#ifdef BLE_LED + digitalWrite(BLE_LED, LOW); +#endif break; } } @@ -102,4 +108,4 @@ class BluetoothStatus : public Status } // namespace meshtastic -extern meshtastic::BluetoothStatus *bluetoothStatus; \ No newline at end of file +extern meshtastic::BluetoothStatus *bluetoothStatus; diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp deleted file mode 100644 index 8db52c074..000000000 --- a/src/ButtonThread.cpp +++ /dev/null @@ -1,467 +0,0 @@ -#include "ButtonThread.h" - -#include "configuration.h" -#if !MESHTASTIC_EXCLUDE_GPS -#include "GPS.h" -#endif -#include "MeshService.h" -#include "PowerFSM.h" -#include "RadioLibInterface.h" -#include "buzz.h" -#include "main.h" -#include "modules/ExternalNotificationModule.h" -#include "power.h" -#include "sleep.h" -#ifdef ARCH_PORTDUINO -#include "platform/portduino/PortduinoGlue.h" -#endif - -#define DEBUG_BUTTONS 0 -#if DEBUG_BUTTONS -#define LOG_BUTTON(...) LOG_DEBUG(__VA_ARGS__) -#else -#define LOG_BUTTON(...) -#endif - -using namespace concurrency; - -ButtonThread *buttonThread; // Declared extern in header -volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE; - -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) -OneButton ButtonThread::userButton; // Get reference to static member -#endif -ButtonThread::ButtonThread() : OSThread("Button") -{ -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - -#if defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { - this->userButton = OneButton(settingsMap[user], true, true); - LOG_DEBUG("Use GPIO%02d for button", settingsMap[user]); - } -#elif defined(BUTTON_PIN) -#if !defined(USERPREFS_BUTTON_PIN) - int pin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; // Resolved button pin -#endif -#ifdef USERPREFS_BUTTON_PIN - int pin = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; // Resolved button pin -#endif -#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - this->userButton = OneButton(pin, false, false); -#elif defined(BUTTON_ACTIVE_LOW) - this->userButton = OneButton(pin, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP); -#else - this->userButton = OneButton(pin, true, true); -#endif - LOG_DEBUG("Use GPIO%02d for button", pin); -#endif - -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did -#ifdef BUTTON_SENSE_TYPE - pinMode(pin, BUTTON_SENSE_TYPE); -#else - pinMode(pin, INPUT_PULLUP_SENSE); -#endif -#endif - -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - userButton.attachClick(userButtonPressed); - userButton.setClickMs(BUTTON_CLICK_MS); - userButton.setPressMs(BUTTON_LONGPRESS_MS); - userButton.setDebounceMs(1); - userButton.attachDoubleClick(userButtonDoublePressed); - userButton.attachMultiClick(userButtonMultiPressed, this); // Reference to instance: get click count from non-static OneButton -#if !defined(T_DECK) && \ - !defined( \ - ELECROW_ThinkNode_M2) // T-Deck immediately wakes up after shutdown, Thinknode M2 has this on the smaller ALT button - userButton.attachLongPressStart(userButtonPressedLongStart); - userButton.attachLongPressStop(userButtonPressedLongStop); -#endif -#endif - -#ifdef BUTTON_PIN_ALT -#if defined(ELECROW_ThinkNode_M2) - this->userButtonAlt = OneButton(BUTTON_PIN_ALT, false, false); -#else - this->userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true); -#endif -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did - pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE); -#endif - userButtonAlt.attachClick(userButtonPressedScreen); - userButtonAlt.setClickMs(BUTTON_CLICK_MS); - userButtonAlt.setPressMs(BUTTON_LONGPRESS_MS); - userButtonAlt.setDebounceMs(1); - userButtonAlt.attachLongPressStart(userButtonPressedLongStart); - userButtonAlt.attachLongPressStop(userButtonPressedLongStop); -#endif - -#ifdef BUTTON_PIN_TOUCH - userButtonTouch = OneButton(BUTTON_PIN_TOUCH, true, true); - userButtonTouch.setPressMs(BUTTON_TOUCH_MS); - userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click? -#endif - -#ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); -#endif - - attachButtonInterrupts(); -#endif -} - -void ButtonThread::switchPage() -{ -#ifdef BUTTON_PIN -#if !defined(USERPREFS_BUTTON_PIN) - if (((config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN) != - moduleConfig.canned_message.inputbroker_pin_press) || - !(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) || - !moduleConfig.canned_message.enabled) { - powerFSM.trigger(EVENT_PRESS); - } -#endif -#if defined(USERPREFS_BUTTON_PIN) - if (((config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN) != - moduleConfig.canned_message.inputbroker_pin_press) || - !(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) || - !moduleConfig.canned_message.enabled) { - powerFSM.trigger(EVENT_PRESS); - } -#endif - -#endif -#if defined(ARCH_PORTDUINO) - if ((settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) && - (settingsMap[user] != moduleConfig.canned_message.inputbroker_pin_press) || - !moduleConfig.canned_message.enabled) { - powerFSM.trigger(EVENT_PRESS); - } -#endif -} - -void ButtonThread::sendAdHocPosition() -{ - service->refreshLocalMeshNode(); - auto sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); - if (screen) { - if (sentPosition) - screen->print("Sent ad-hoc position\n"); - else - screen->print("Sent ad-hoc nodeinfo\n"); - screen->forceDisplay(true); // Force a new UI frame, then force an EInk update - } -} - -int32_t ButtonThread::runOnce() -{ - // If the button is pressed we suppress CPU sleep until release - canSleep = true; // Assume we should not keep the board awake - -#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) - userButton.tick(); - canSleep &= userButton.isIdle(); -#elif defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { - userButton.tick(); - canSleep &= userButton.isIdle(); - } -#endif -#ifdef BUTTON_PIN_ALT - userButtonAlt.tick(); - canSleep &= userButtonAlt.isIdle(); -#endif -#ifdef BUTTON_PIN_TOUCH - userButtonTouch.tick(); - canSleep &= userButtonTouch.isIdle(); -#endif - - if (btnEvent != BUTTON_EVENT_NONE) { - switch (btnEvent) { - case BUTTON_EVENT_PRESSED: { - LOG_BUTTON("press!"); - // If a nag notification is running, stop it and prevent other actions - if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { - externalNotificationModule->stopNow(); - break; - } -#ifdef ELECROW_ThinkNode_M1 - sendAdHocPosition(); - break; -#endif - switchPage(); - break; - } - - case BUTTON_EVENT_PRESSED_SCREEN: { - LOG_BUTTON("AltPress!"); -#ifdef ELECROW_ThinkNode_M1 - // If a nag notification is running, stop it and prevent other actions - if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { - externalNotificationModule->stopNow(); - break; - } - switchPage(); - break; -#endif - // turn screen on or off - screen_flag = !screen_flag; - if (screen) - screen->setOn(screen_flag); - break; - } - - case BUTTON_EVENT_DOUBLE_PRESSED: { - LOG_BUTTON("Double press!"); -#ifdef ELECROW_ThinkNode_M1 - digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); - break; -#endif - sendAdHocPosition(); - break; - } - - case BUTTON_EVENT_MULTI_PRESSED: { - LOG_BUTTON("Mulitipress! %hux", multipressClickCount); - switch (multipressClickCount) { -#if HAS_GPS && !defined(ELECROW_ThinkNode_M1) - // 3 clicks: toggle GPS - case 3: - if (!config.device.disable_triple_click && (gps != nullptr)) { - gps->toggleGpsMode(); - if (screen) - screen->forceDisplay(true); // Force a new UI frame, then force an EInk update - } - break; -#elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - case 3: - LOG_INFO("3 clicks: toggle buzzer"); - buzzer_flag = !buzzer_flag; - if (!buzzer_flag) - noTone(PIN_BUZZER); - break; - -#endif - -#if defined(USE_EINK) && defined(PIN_EINK_EN) && !defined(ELECROW_ThinkNode_M1) // i.e. T-Echo - // 4 clicks: toggle backlight - case 4: - digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); - break; -#endif -#if !MESHTASTIC_EXCLUDE_SCREEN && HAS_SCREEN - // 5 clicks: start accelerometer/magenetometer calibration for 30 seconds - case 5: - if (accelerometerThread) { - accelerometerThread->calibrate(30); - } - break; - // 6 clicks: start accelerometer/magenetometer calibration for 60 seconds - case 6: - if (accelerometerThread) { - accelerometerThread->calibrate(60); - } - break; -#endif - // No valid multipress action - default: - break; - } // end switch: click count - - break; - } // end multipress event - - case BUTTON_EVENT_LONG_PRESSED: { - LOG_BUTTON("Long press!"); - powerFSM.trigger(EVENT_PRESS); - if (screen) { - screen->startAlert("Shutting down..."); - } - playBeep(); - break; - } - - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. - case BUTTON_EVENT_LONG_RELEASED: { - LOG_INFO("Shutdown from long press"); - playShutdownMelody(); - delay(3000); - power->shutdown(); - break; - } - -#ifdef BUTTON_PIN_TOUCH - case BUTTON_EVENT_TOUCH_LONG_PRESSED: { - LOG_BUTTON("Touch press!"); - // Ignore if: no screen - if (!screen) - break; - -#ifdef TTGO_T_ECHO - // Ignore if: TX in progress - // Uncommon T-Echo hardware bug, LoRa TX triggers touch button - if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) - break; -#endif - - // Wake if asleep - if (powerFSM.getState() == &stateDARK) - powerFSM.trigger(EVENT_PRESS); - - // Update display (legacy behaviour) - screen->forceDisplay(); - break; - } -#endif // BUTTON_PIN_TOUCH - - default: - break; - } - btnEvent = BUTTON_EVENT_NONE; - } - - return 50; -} - -/* - * Attach (or re-attach) hardware interrupts for buttons - * Public method. Used outside class when waking from MCU sleep - */ -void ButtonThread::attachButtonInterrupts() -{ -#if defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) - wakeOnIrq(settingsMap[user], FALLING); -#elif defined(BUTTON_PIN) - // Interrupt for user button, during normal use. Improves responsiveness. - attachInterrupt( -#if !defined(USERPREFS_BUTTON_PIN) - config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, -#endif -#if defined(USERPREFS_BUTTON_PIN) - config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN, -#endif - []() { - ButtonThread::userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - CHANGE); -#endif - -#ifdef BUTTON_PIN_ALT -#ifdef ELECROW_ThinkNode_M2 - wakeOnIrq(BUTTON_PIN_ALT, RISING); -#else - wakeOnIrq(BUTTON_PIN_ALT, FALLING); -#endif -#endif - -#ifdef BUTTON_PIN_TOUCH - wakeOnIrq(BUTTON_PIN_TOUCH, FALLING); -#endif -} - -/* - * Detach the "normal" button interrupts. - * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep - */ -void ButtonThread::detachButtonInterrupts() -{ -#if defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) - detachInterrupt(settingsMap[user]); -#elif defined(BUTTON_PIN) -#if !defined(USERPREFS_BUTTON_PIN) - detachInterrupt(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); -#endif -#if defined(USERPREFS_BUTTON_PIN) - detachInterrupt(config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN); -#endif -#endif - -#ifdef BUTTON_PIN_ALT - detachInterrupt(BUTTON_PIN_ALT); -#endif - -#ifdef BUTTON_PIN_TOUCH - detachInterrupt(BUTTON_PIN_TOUCH); -#endif -} - -#ifdef ARCH_ESP32 - -// Detach our class' interrupts before lightsleep -// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int ButtonThread::beforeLightSleep(void *unused) -{ - detachButtonInterrupts(); - return 0; // Indicates success -} - -// Reconfigure our interrupts -// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) -{ - attachButtonInterrupts(); - return 0; // Indicates success -} - -#endif - -/** - * Watch a GPIO and if we get an IRQ, wake the main thread. - * Use to add wake on button press - */ -void ButtonThread::wakeOnIrq(int irq, int mode) -{ - attachInterrupt( - irq, - [] { - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - runASAP = true; - }, - FALLING); -} - -// Static callback -void ButtonThread::userButtonMultiPressed(void *callerThread) -{ - // Grab click count from non-static button, while the info is still valid - ButtonThread *thread = (ButtonThread *)callerThread; - thread->storeClickCount(); - - // Then handle later, in the usual way - btnEvent = BUTTON_EVENT_MULTI_PRESSED; -} - -// Non-static method, runs during callback. Grabs info while still valid -void ButtonThread::storeClickCount() -{ -#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) - multipressClickCount = userButton.getNumberClicks(); -#endif -} - -void ButtonThread::userButtonPressedLongStart() -{ - if (millis() > c_holdOffTime) { - btnEvent = BUTTON_EVENT_LONG_PRESSED; - } -} - -void ButtonThread::userButtonPressedLongStop() -{ - if (millis() > c_holdOffTime) { - btnEvent = BUTTON_EVENT_LONG_RELEASED; - } -} \ No newline at end of file diff --git a/src/ButtonThread.h b/src/ButtonThread.h deleted file mode 100644 index 3af700dd0..000000000 --- a/src/ButtonThread.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include "OneButton.h" -#include "concurrency/OSThread.h" -#include "configuration.h" - -#ifndef BUTTON_CLICK_MS -#define BUTTON_CLICK_MS 250 -#endif - -#ifndef BUTTON_LONGPRESS_MS -#define BUTTON_LONGPRESS_MS 5000 -#endif - -#ifndef BUTTON_TOUCH_MS -#define BUTTON_TOUCH_MS 400 -#endif - -class ButtonThread : public concurrency::OSThread -{ - public: - static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot - - enum ButtonEventType { - BUTTON_EVENT_NONE, - BUTTON_EVENT_PRESSED, - BUTTON_EVENT_PRESSED_SCREEN, - BUTTON_EVENT_DOUBLE_PRESSED, - BUTTON_EVENT_MULTI_PRESSED, - BUTTON_EVENT_LONG_PRESSED, - BUTTON_EVENT_LONG_RELEASED, - BUTTON_EVENT_TOUCH_LONG_PRESSED, - }; - - ButtonThread(); - int32_t runOnce() override; - void attachButtonInterrupts(); - void detachButtonInterrupts(); - void storeClickCount(); - bool isBuzzing() { return buzzer_flag; } - void setScreenFlag(bool flag) { screen_flag = flag; } - bool getScreenFlag() { return screen_flag; } - - // Disconnect and reconnect interrupts for light sleep -#ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); -#endif - private: -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - static OneButton userButton; // Static - accessed from an interrupt -#endif -#ifdef BUTTON_PIN_ALT - OneButton userButtonAlt; -#endif -#ifdef BUTTON_PIN_TOUCH - OneButton userButtonTouch; -#endif - -#ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &ButtonThread::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &ButtonThread::afterLightSleep); -#endif - - // set during IRQ - static volatile ButtonEventType btnEvent; - bool buzzer_flag = false; - bool screen_flag = true; - - // Store click count during callback, for later use - volatile int multipressClickCount = 0; - - static void wakeOnIrq(int irq, int mode); - - static void sendAdHocPosition(); - static void switchPage(); - - // IRQ callbacks - static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; } - static void userButtonPressedScreen() { btnEvent = BUTTON_EVENT_PRESSED_SCREEN; } - static void userButtonDoublePressed() { btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; } - static void userButtonMultiPressed(void *callerThread); // Retrieve click count from non-static Onebutton while still valid - static void userButtonPressedLongStart(); - static void userButtonPressedLongStop(); - static void touchPressedLongStart() { btnEvent = BUTTON_EVENT_TOUCH_LONG_PRESSED; } -}; - -extern ButtonThread *buttonThread; diff --git a/src/Power.cpp b/src/Power.cpp index a9ed6360e..9c67977bd 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -103,7 +103,7 @@ NullSensor ina3221Sensor; #endif -#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_STM32WL) +#if !MESHTASTIC_EXCLUDE_I2C #include "modules/Telemetry/Sensor/MAX17048Sensor.h" #include extern std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1]; @@ -278,7 +278,7 @@ class AnalogBatteryLevel : public HasBatteryLevel } #endif -#if HAS_TELEMETRY && !defined(ARCH_STM32WL) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (hasINA()) { return getINAVoltage(); } @@ -456,8 +456,7 @@ class AnalogBatteryLevel : public HasBatteryLevel #ifdef EXT_CHRG_DETECT return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #else -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL) && \ - !defined(DISABLE_INA_CHARGING_DETECTION) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) if (hasINA()) { // get current flow from INA sensor - negative value means power flowing into the battery // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD @@ -503,7 +502,7 @@ class AnalogBatteryLevel : public HasBatteryLevel } #endif -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR uint16_t getINAVoltage() { if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { @@ -661,12 +660,14 @@ bool Power::analogInit() */ bool Power::setup() { - // initialise one power sensor (only) - bool found = axpChipInit(); - if (!found) - found = lipoInit(); - if (!found) - found = analogInit(); + bool found = false; + if (axpChipInit()) { + found = true; + } else if (lipoInit()) { + found = true; + } else if (analogInit()) { + found = true; + } #ifdef NRF_APM found = true; @@ -680,7 +681,7 @@ bool Power::setup() void Power::shutdown() { - LOG_INFO("Shutting down"); + LOG_INFO("Shutting Down"); #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #ifdef PIN_LED1 @@ -853,7 +854,8 @@ int32_t Power::runOnce() #ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? if (PMU->isPekeyLongPressIrq()) { LOG_DEBUG("PEK long button press"); - screen->setOn(false); + if (screen) + screen->setOn(false); } #endif @@ -1158,7 +1160,7 @@ bool Power::axpChipInit() #endif } -#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#if !MESHTASTIC_EXCLUDE_I2C && __has_include() /** * Wrapper class for an I2C MAX17048 Lipo battery sensor. diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index dbe4796cf..3b3f8080d 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -26,7 +26,7 @@ #ifndef SLEEP_TIME #define SLEEP_TIME 30 #endif -#if EXCLUDE_POWER_FSM +#if MESHTASTIC_EXCLUDE_POWER_FSM FakeFsm powerFSM; void PowerFSM_setup(){}; #else @@ -82,7 +82,8 @@ static uint32_t secsSlept; static void lsEnter() { LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs); - screen->setOn(false); + if (screen) + screen->setOn(false); secsSlept = 0; // How long have we been sleeping this time // LOG_INFO("lsEnter end"); @@ -160,7 +161,8 @@ static void lsExit() static void nbEnter() { LOG_DEBUG("State: NB"); - screen->setOn(false); + if (screen) + screen->setOn(false); #ifdef ARCH_ESP32 // Only ESP32 should turn off bluetooth setBluetoothEnable(false); @@ -172,22 +174,23 @@ static void nbEnter() static void darkEnter() { setBluetoothEnable(true); - screen->setOn(false); + if (screen) + screen->setOn(false); } static void serialEnter() { LOG_DEBUG("State: SERIAL"); setBluetoothEnable(false); - screen->setOn(true); - screen->print("Serial connected\n"); + if (screen) { + screen->setOn(true); + } } static void serialExit() { // Turn bluetooth back on when we leave serial stream API setBluetoothEnable(true); - screen->print("Serial disconnected\n"); } static void powerEnter() @@ -198,15 +201,10 @@ static void powerEnter() LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } else { - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); // within enter() the function getState() returns the state we came from - - // Mothballed: print change of power-state to device screen - /* if (strcmp(powerFSM.getState()->name, "BOOT") != 0 && strcmp(powerFSM.getState()->name, "POWER") != 0 && - strcmp(powerFSM.getState()->name, "DARK") != 0) { - screen->print("Powered...\n"); - }*/ } } @@ -221,18 +219,16 @@ static void powerIdle() static void powerExit() { - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); - - // Mothballed: print change of power-state to device screen - /*if (!isPowered()) - screen->print("Unpowered...\n");*/ } static void onEnter() { LOG_DEBUG("State: ON"); - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); } @@ -244,11 +240,6 @@ static void onIdle() } } -static void screenPress() -{ - screen->onPress(); -} - static void bootEnter() { LOG_DEBUG("State: BOOT"); @@ -292,9 +283,9 @@ void PowerFSM_setup() powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, screenPress, "Press"); - powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress, "Press"); // reenter On to restart our timers - powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, screenPress, + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, "Press"); // Allow button to work while in serial API // Handle critically low power battery by forcing deep sleep @@ -328,10 +319,10 @@ void PowerFSM_setup() // if any packet destined for phone arrives, turn on bluetooth at least powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); - // show the latest node when we get a new node db update - powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // Removed 2.7: we don't show the nodes individually for every node on the screen anymore + // powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); // Show the received text message powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 13dfdc4cc..6330a5fc6 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -11,7 +11,7 @@ #define EVENT_RECEIVED_MSG 5 // #define EVENT_BOOT 6 // now done with a timed transition #define EVENT_BLUETOOTH_PAIR 7 -#define EVENT_NODEDB_UPDATED 8 // NodeDB has a big enough change that we think you should turn on the screen +// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth #define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 @@ -22,7 +22,7 @@ #define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep) #define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen -#if EXCLUDE_POWER_FSM +#if MESHTASTIC_EXCLUDE_POWER_FSM class FakeFsm { public: diff --git a/src/PowerFSMThread.h b/src/PowerFSMThread.h index c842f4515..135f53298 100644 --- a/src/PowerFSMThread.h +++ b/src/PowerFSMThread.h @@ -18,7 +18,7 @@ class PowerFSMThread : public OSThread protected: int32_t runOnce() override { -#if !EXCLUDE_POWER_FSM +#if !MESHTASTIC_EXCLUDE_POWER_FSM powerFSM.run_machine(); /// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 07f873864..7c8d77651 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -352,8 +352,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16 for (uint16_t i = 0; i < len; i += 16) { if (i % 128 == 0) log(logLevel, " +------------------------------------------------+ +----------------+"); - char s[] = "| | | |\n"; - uint8_t ix = 1, iy = 52; + char s[] = " | | | |\n"; + uint8_t ix = 5, iy = 56; for (uint8_t j = 0; j < 16; j++) { if (i + j < len) { uint8_t c = buf[i + j]; @@ -367,10 +367,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16 } } uint8_t index = i / 16; - if (i < 256) - log(logLevel, " "); - log(logLevel, "%02x", index); - log(logLevel, "."); + sprintf(s, "%03x", index); + s[3] = '.'; log(logLevel, s); } log(logLevel, " +------------------------------------------------+ +----------------+"); @@ -393,4 +391,4 @@ std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) break; } return std::string(formatted.get()); -} \ No newline at end of file +} diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp new file mode 100644 index 000000000..2bd3158a3 --- /dev/null +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -0,0 +1,79 @@ +#include "BuzzerFeedbackThread.h" +#include "NodeDB.h" +#include "buzz.h" +#include "configuration.h" + +BuzzerFeedbackThread *buzzerFeedbackThread; + +BuzzerFeedbackThread::BuzzerFeedbackThread() : OSThread("BuzzerFeedback") +{ + if (inputBroker) + inputObserver.observe(inputBroker); +} + +int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) +{ + // Only provide feedback if buzzer is enabled for notifications + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + return 0; // Let other handlers process the event + } + + // Track last event time for potential future use + lastEventTime = millis(); + needsUpdate = true; + + // Handle different input events with appropriate buzzer feedback + switch (event->inputEvent) { + case INPUT_BROKER_USER_PRESS: + case INPUT_BROKER_ALT_PRESS: + case INPUT_BROKER_SELECT: + playBeep(); // Confirmation feedback + break; + + case INPUT_BROKER_UP: + case INPUT_BROKER_DOWN: + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: + playChirp(); // Navigation feedback + break; + + case INPUT_BROKER_CANCEL: + case INPUT_BROKER_BACK: + playBoop(); // Cancel/back feedback + break; + + case INPUT_BROKER_SEND_PING: + 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) { + // Typing feedback - very short boop + // Removing this for now, too chatty + // playChirp(); + } + break; + } + + return 0; // Allow other handlers to process the event +} + +int32_t BuzzerFeedbackThread::runOnce() +{ + // This thread is primarily event-driven, but we can use runOnce + // for any periodic tasks if needed in the future + + if (needsUpdate) { + needsUpdate = false; + // Could add any periodic processing here + } + + // Run every 100ms when active, less frequently when idle + return needsUpdate ? 100 : 1000; +} diff --git a/src/buzz/BuzzerFeedbackThread.h b/src/buzz/BuzzerFeedbackThread.h new file mode 100644 index 000000000..dedea9860 --- /dev/null +++ b/src/buzz/BuzzerFeedbackThread.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Observer.h" +#include "concurrency/OSThread.h" +#include "input/InputBroker.h" + +class BuzzerFeedbackThread : public concurrency::OSThread +{ + CallbackObserver inputObserver = + CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); + + public: + BuzzerFeedbackThread(); + int handleInputEvent(const InputEvent *event); + + protected: + virtual int32_t runOnce() override; + + private: + uint32_t lastEventTime = 0; + bool needsUpdate = false; +}; + +extern BuzzerFeedbackThread *buzzerFeedbackThread; diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6ba2f4140..b09d7a82c 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -38,6 +38,11 @@ const int DURATION_1_1 = 1000; // 1/1 note void playTones(const ToneDuration *tone_durations, int size) { + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + // Buzzer is disabled or not set to system tones + return; + } #ifdef PIN_BUZZER if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; @@ -54,7 +59,7 @@ void playTones(const ToneDuration *tone_durations, int size) void playBeep() { - ToneDuration melody[] = {{NOTE_B3, DURATION_1_4}}; + ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } @@ -87,3 +92,72 @@ void playShutdownMelody() ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } + +void playChirp() +{ + // A short, friendly "chirp" sound for key presses + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playBoop() +{ + // A short, friendly "boop" sound for button presses + ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playLongPressLeadUp() +{ + // An ascending lead-up sequence for long press - builds anticipation + ToneDuration melody[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +// Static state for progressive lead-up notes +static int leadUpNoteIndex = 0; +static const ToneDuration leadUpNotes[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis +}; +static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration); + +bool playNextLeadUpNote() +{ + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // All notes have been played + } + + // Use playTones to handle buzzer logic consistently + const auto ¬e = leadUpNotes[leadUpNoteIndex]; + playTones(¬e, 1); // Play single note using existing playTones function + + leadUpNoteIndex++; + return true; // Note was played (playTones handles buzzer availability internally) +} + +void resetLeadUpSequence() +{ + leadUpNoteIndex = 0; +} + +void playComboTune() +{ + // Quick high-pitched notes with trills + ToneDuration melody[] = { + {NOTE_G3, 80}, // Quick chirp + {NOTE_B3, 60}, // Higher chirp + {NOTE_CS4, 80}, // Even higher + {NOTE_G3, 60}, // Quick trill down + {NOTE_CS4, 60}, // Quick trill up + {NOTE_B3, 120} // Ending chirp + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index adeaca73d..c25a54a5b 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -5,4 +5,10 @@ void playLongBeep(); void playStartMelody(); void playShutdownMelody(); void playGPSEnableBeep(); -void playGPSDisableBeep(); \ No newline at end of file +void playGPSDisableBeep(); +void playComboTune(); +void playBoop(); +void playChirp(); +void playLongPressLeadUp(); +bool playNextLeadUpNote(); // Play the next note in the lead-up sequence +void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning \ No newline at end of file diff --git a/src/commands.h b/src/commands.h index f2b783010..603003e5c 100644 --- a/src/commands.h +++ b/src/commands.h @@ -12,7 +12,7 @@ enum class Cmd { STOP_ALERT_FRAME, START_FIRMWARE_UPDATE_SCREEN, STOP_BOOT_SCREEN, - PRINT, SHOW_PREV_FRAME, - SHOW_NEXT_FRAME + SHOW_NEXT_FRAME, + NOOP }; \ No newline at end of file diff --git a/src/concurrency/Lock.cpp b/src/concurrency/Lock.cpp index 11501359b..0fe80e455 100644 --- a/src/concurrency/Lock.cpp +++ b/src/concurrency/Lock.cpp @@ -9,17 +9,23 @@ namespace concurrency Lock::Lock() : handle(xSemaphoreCreateBinary()) { assert(handle); - assert(xSemaphoreGive(handle)); + if (xSemaphoreGive(handle) == false) { + abort(); + } } void Lock::lock() { - assert(xSemaphoreTake(handle, portMAX_DELAY)); + if (xSemaphoreTake(handle, portMAX_DELAY) == false) { + abort(); + } } void Lock::unlock() { - assert(xSemaphoreGive(handle)); + if (xSemaphoreGive(handle) == false) { + abort(); + } } #else Lock::Lock() {} diff --git a/src/configuration.h b/src/configuration.h index ab2143a03..f0c5132b0 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -81,7 +81,43 @@ along with this program. If not, see . // #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_SG_923 // Total system gain in dBm to subtract from Tx power to remain within regulatory and Tx PA limits -// This value should be set in variant.h and is PA gain + antenna gain (if variant has a non-removable antenna) +// The value consists of PA gain + antenna gain (if variant has a non-removable antenna) +// TX_GAIN_LORA should be set with definitions below for common modules, or in variant.h. + +// Gain for common modules with transmit PAs +#ifdef EBYTE_E22_900M30S +// 10dB PA gain and 30dB rated output; based on measurements from +// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/E22-900M30S%20power%20output%20testing.txt +#define TX_GAIN_LORA 7 +#define SX126X_MAX_POWER 22 +#endif + +#ifdef EBYTE_E22_900M33S +// 25dB PA gain and 33dB rated output; based on TX Power Curve from E22-900M33S_UserManual_EN_v1.0.pdf +#define TX_GAIN_LORA 25 +#define SX126X_MAX_POWER 8 +#endif + +#ifdef NICERF_MINIF27 +// Note that datasheet power level of 9 corresponds with SX1262 at 22dBm +// Maximum output power of 29dBm with VCC_PA = 5V +#define TX_GAIN_LORA 7 +#define SX126X_MAX_POWER 22 +#endif + +#ifdef NICERF_F30_HF +// Maximum output power of 29.6dBm with VCC = 5V and SX1262 at 22dBm +#define TX_GAIN_LORA 8 +#define SX126X_MAX_POWER 22 +#endif + +#ifdef NICERF_F30_LF +// Maximum output power of 32.0dBm with VCC = 5V and SX1262 at 22dBm +#define TX_GAIN_LORA 10 +#define SX126X_MAX_POWER 22 +#endif + +// Default system gain to 0 if not defined #ifndef TX_GAIN_LORA #define TX_GAIN_LORA 0 #endif @@ -154,6 +190,7 @@ along with this program. If not, see . #define DFROBOT_RAIN_ADDR 0x1d #define NAU7802_ADDR 0x2A #define MAX30102_ADDR 0x57 +#define SCD4X_ADDR 0x62 #define MLX90614_ADDR_DEF 0x5A #define CGRADSENS_ADDR 0x66 #define LTR390UV_ADDR 0x53 @@ -172,6 +209,7 @@ along with this program. If not, see . #define BMX160_ADDR 0x69 #define ICM20948_ADDR 0x69 #define ICM20948_ADDR_ALT 0x68 +#define BMM150_ADDR 0x13 // ----------------------------------------------------------------------------- // LED @@ -194,6 +232,15 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- #define FT6336U_ADDR 0x48 +// ----------------------------------------------------------------------------- +// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected) +// - the default i2c address for this sensor is 0x20, and users are instructed to +// set 0x21 and 0x22 for the second and third sensor if present. +// ----------------------------------------------------------------------------- +#define RAK120351_ADDR 0x20 +#define RAK120352_ADDR 0x21 +#define RAK120353_ADDR 0x22 + // ----------------------------------------------------------------------------- // BIAS-T Generator // ----------------------------------------------------------------------------- @@ -303,11 +350,41 @@ along with this program. If not, see . #error HW_VENDOR must be defined #endif +#ifndef TB_DOWN +#define TB_DOWN 255 +#endif +#ifndef TB_UP +#define TB_UP 255 +#endif +#ifndef TB_LEFT +#define TB_LEFT 255 +#endif +#ifndef TB_RIGHT +#define TB_RIGHT 255 +#endif +#ifndef TB_PRESS +#define TB_PRESS 255 +#endif + // Support multiple RGB LED configuration #if defined(HAS_NCP5623) || defined(HAS_LP5562) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) #define HAS_RGB_LED #endif +// default mapping of pins +#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN) +#define ALT_BUTTON_PIN PIN_BUTTON2 +#endif +#if defined ALT_BUTTON_PIN + +#ifndef ALT_BUTTON_ACTIVE_LOW +#define ALT_BUTTON_ACTIVE_LOW true +#endif +#ifndef ALT_BUTTON_ACTIVE_PULLUP +#define ALT_BUTTON_ACTIVE_PULLUP true +#endif +#endif + // ----------------------------------------------------------------------------- // Global switches to turn off features for a minimized build // ----------------------------------------------------------------------------- diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 881b75405..3f73107ad 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -37,8 +37,14 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P}; - return firstOfOrNONE(8, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; + return firstOfOrNONE(9, types); +} + +ScanI2C::FoundDevice ScanI2C::firstAQI() const +{ + ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + return firstOfOrNONE(2, types); } ScanI2C::FoundDevice ScanI2C::firstRGBLED() const @@ -80,4 +86,4 @@ bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) cons || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); } -ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} \ No newline at end of file +ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 72b2bda81..81d2510db 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -62,6 +62,7 @@ class ScanI2C FT6336U, STK8BAXX, ICM20948, + SCD4X, MAX30102, TPS65233, MPR121KB, @@ -71,8 +72,10 @@ class ScanI2C DFROBOT_RAIN, DPS310, LTR390UV, + RAK12035, TCA8418KB, PCT2075, + BMM150, } DeviceType; // typedef uint8_t DeviceAddress; @@ -125,6 +128,8 @@ class ScanI2C FoundDevice firstAccelerometer() const; + FoundDevice firstAQI() const; + FoundDevice firstRGBLED() const; virtual FoundDevice find(DeviceType) const; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index f8d9ae907..1c26d4c23 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -361,7 +361,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2); - if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c) { + if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c || registerValue == 0xc8d) { type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { @@ -426,9 +426,21 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("BMA423", (uint8_t)addr.address); } break; + case TCA9535_ADDR: + case RAK120352_ADDR: + case RAK120353_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); + if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) + type = RAK12035; + logFoundDevice("RAK12035", (uint8_t)addr.address); + } else { + type = TCA9535; + logFoundDevice("TCA9535", (uint8_t)addr.address); + } + + break; SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(TCA9535_ADDR, TCA9535, "TCA9535", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address); @@ -438,6 +450,8 @@ 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(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); #endif @@ -546,4 +560,4 @@ void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) { LOG_INFO("%s found at address 0x%x", device, address); } -#endif \ No newline at end of file +#endif diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 142241c43..345c738d6 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1536,7 +1536,10 @@ 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()); - perhapsSetRTC(RTCQualityGPS, t); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { + // Clear the GPS buffer if we got an invalid time + clearBuffer(); + } return true; } else return false; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index af964eab5..5054be3f0 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -105,7 +105,7 @@ void readFromRTC() * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ -bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) +RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) { static uint32_t lastSetMsec = 0; uint32_t now = millis(); @@ -113,7 +113,7 @@ bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - return false; + return RTCSetResultInvalidTime; } #endif @@ -184,9 +184,9 @@ bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) readFromRTC(); #endif - return true; + return RTCSetResultSuccess; } else { - return false; + return RTCSetResultNotSet; // RTC was already set with a higher quality time } } @@ -215,7 +215,7 @@ const char *RtcName(RTCQuality quality) * @param t The time to potentially set the RTC to. * @return True if the RTC was set to the provided time, false otherwise. */ -bool perhapsSetRTC(RTCQuality q, struct tm &t) +RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) { /* Convert to unix time The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 @@ -231,7 +231,7 @@ bool perhapsSetRTC(RTCQuality q, struct tm &t) // 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) { // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); - return false; + return RTCSetResultInvalidTime; } else { return perhapsSetRTC(q, &tv); } @@ -244,11 +244,15 @@ bool perhapsSetRTC(RTCQuality q, struct tm &t) */ int32_t getTZOffset() { +#if MESHTASTIC_EXCLUDE_TZ + return 0; +#else time_t now = getTime(false); struct tm *gmt; gmt = gmtime(&now); gmt->tm_isdst = -1; return (int32_t)difftime(now, mktime(gmt)); +#endif } /** diff --git a/src/gps/RTC.h b/src/gps/RTC.h index caa48dc06..96dec575b 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -22,13 +22,22 @@ enum RTCQuality { RTCQualityGPS = 4 }; +/// The RTC set result codes +/// Used to indicate the result of an attempt to set the RTC. +enum RTCSetResult { + RTCSetResultNotSet = 0, ///< RTC was set successfully + RTCSetResultSuccess = 1, ///< RTC was set successfully + RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) + RTCSetResultError = 4 ///< An error occurred while setting the RTC +}; + RTCQuality getRTCQuality(); extern uint32_t lastSetFromPhoneNtpOrGps; /// If we haven't yet set our RTC this boot, set it from a GPS derived time -bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); -bool perhapsSetRTC(RTCQuality q, struct tm &t); +RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); +RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t); /// Return a string name for the quality const char *RtcName(RTCQuality quality); diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 5a2749482..66c7938b5 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -6,6 +6,10 @@ #include "main.h" #include +#ifdef GXEPD2_DRIVER_0 +#include "einkDetect.h" +#endif + /* The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini Previously, these macros were defined at the top of this file. @@ -174,9 +178,8 @@ bool EInkDisplay::connect() } } -#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_WIRELESS_PAPER) || 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) +#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ + defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) { // Start HSPI hspi = new SPIClass(HSPI); @@ -232,6 +235,23 @@ bool EInkDisplay::connect() adafruitDisplay->init(); adafruitDisplay->setRotation(3); } +#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) + + // Detect display model, before starting SPI + EInkDetectionResult displayModel = detectEInk(); + + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + + // Create GxEPD2 object + adafruitDisplay = new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, + PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + #endif return true; diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 93be197b0..284337627 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -5,6 +5,10 @@ #include "GxEPD2_BW.h" #include +#ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models +#include "GxEPD2Multi.h" +#endif + /** * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * @@ -63,8 +67,15 @@ class EInkDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; - // AdafruitGFX display object - instantiated in connect(), variant specific +#ifdef GXEPD2_DRIVER_0 + // AdafruitGFX display object - wrapper for multiple drivers + // Allows runtime detection of multiple displays + // Avoid this situation if possible! + GxEPD2_Multi *adafruitDisplay = NULL; +#else + // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific GxEPD2_BW *adafruitDisplay = NULL; +#endif // If display uses HSPI #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ diff --git a/src/graphics/GxEPD2Multi.h b/src/graphics/GxEPD2Multi.h new file mode 100644 index 000000000..f3807c9de --- /dev/null +++ b/src/graphics/GxEPD2Multi.h @@ -0,0 +1,135 @@ +// Wrapper class for GxEPD2_BW + +// Generic signature at build-time, so that we can detect display model at run-time +// Workaround for issue of GxEPD2_BW objects not having a shared base class +// Only exposes methods which we are actually using + +template class GxEPD2_Multi +{ + public: + void drawPixel(int16_t x, int16_t y, uint16_t color) + { + if (which == 0) + driver0->drawPixel(x, y, color); + else + driver1->drawPixel(x, y, color); + } + + bool nextPage() + { + if (which == 0) + return driver0->nextPage(); + else + return driver1->nextPage(); + } + + void hibernate() + { + if (which == 0) + driver0->hibernate(); + else + driver1->hibernate(); + } + + void init(uint32_t serial_diag_bitrate = 0) + { + if (which == 0) + driver0->init(serial_diag_bitrate); + else + driver1->init(serial_diag_bitrate); + } + + void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) + { + if (which == 0) + driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + else + driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + } + + void setRotation(uint8_t x) + { + if (which == 0) + driver0->setRotation(x); + else + driver1->setRotation(x); + } + + void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) + { + if (which == 0) + driver0->setPartialWindow(x, y, w, h); + else + driver1->setPartialWindow(x, y, w, h); + } + + void setFullWindow() + { + if (which == 0) + driver0->setFullWindow(); + else + driver1->setFullWindow(); + } + + int16_t width() + { + if (which == 0) + return driver0->width(); + else + return driver1->width(); + } + + int16_t height() + { + if (which == 0) + return driver0->height(); + else + return driver1->height(); + } + + void clearScreen(uint8_t value = 0xFF) + { + if (which == 0) + driver0->clearScreen(); + else + driver1->clearScreen(); + } + + void endAsyncFull() + { + if (which == 0) + driver0->endAsyncFull(); + else + driver1->endAsyncFull(); + } + + // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd + class Epd2Wrapper + { + public: + bool isBusy() { return m_epd2->isBusy(); } + GxEPD2_EPD *m_epd2; + } epd2; + + // Constructor + // Select driver by passing whichDriver as 0 or 1 + GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) + { + assert(whichDriver == 0 || whichDriver == 1); + which = whichDriver; + LOG_DEBUG("GxEPD2_Multi driver: %d", which); + + if (which == 0) { + driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver0->epd2); + } else if (which == 1) { + driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver1->epd2); + } + } + + private: + uint8_t which; + GxEPD2_BW *driver0; + GxEPD2_BW *driver1; +}; \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 61999ee79..1f2e7e4d9 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1,9 +1,10 @@ /* +BaseUI -SSD1306 - Screen module - -Copyright (C) 2018 by Xose Pรฉrez - +Developed and Maintained By: +- Ronald Garcia (HarukiToreda) โ€“ Lead development and implementation. +- JasonP (Xaositek) โ€“ Screen layout and icon design, UI improvements and testing. +- TonyG (Tropho) โ€“ Project management, structural planning, and testing This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,6 +21,7 @@ along with this program. If not, see . */ #include "Screen.h" +#include "NodeDB.h" #include "PowerMon.h" #include "Throttle.h" #include "configuration.h" @@ -27,38 +29,53 @@ along with this program. If not, see . #include #include "DisplayFormatters.h" +#include "TimeFormatters.h" +#include "draw/ClockRenderer.h" +#include "draw/DebugRenderer.h" +#include "draw/MenuHandler.h" +#include "draw/MessageRenderer.h" +#include "draw/NodeListRenderer.h" +#include "draw/NotificationRenderer.h" +#include "draw/UIRenderer.h" +#include "modules/CannedMessageModule.h" + #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" +#include "buzz.h" #endif -#include "ButtonThread.h" +#include "FSCommon.h" #include "MeshService.h" -#include "NodeDB.h" +#include "RadioLibInterface.h" #include "error.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" #include "graphics/images.h" -#include "input/ScanAndSelect.h" #include "input/TouchScreenImpl1.h" #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "meshUtils.h" -#include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" +using graphics::Emote; +using graphics::emotes; +using graphics::numEmotes; + +extern uint16_t TFT_MESH; + #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif #ifdef ARCH_ESP32 -#include "esp_task_wdt.h" -#include "modules/StoreForwardModule.h" #endif #if ARCH_PORTDUINO @@ -82,14 +99,10 @@ namespace graphics // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; +// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization uint32_t logo_timeout = 5000; // 4 seconds for EACH logo -uint32_t hours_in_month = 730; - -// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function -uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; - // Threshold values for the GPS lock accuracy bar display uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; @@ -97,13 +110,9 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; // we'll need to hold onto pointers for the modules that can draw a frame. std::vector moduleFrames; -// Stores the last 4 of our hardware ID, to make finding the device for pairing easier -static char ourId[5]; - -// vector where symbols (string) are displayed in bottom corner of display. +// Global variables for screen function overlay symbols std::vector functionSymbol; -// string displayed in bottom right corner of display. Created from elements in functionSymbol vector -std::string functionSymbolString = ""; +std::string functionSymbolString; #if HAS_GPS // GeoCoord object for the screen @@ -114,257 +123,98 @@ GeoCoord geoCoord; static bool heartbeat = false; #endif -// Quick access to screen dimensions from static drawing functions -// DEPRECATED. To-do: move static functions inside Screen class -#define SCREEN_WIDTH display->getWidth() -#define SCREEN_HEIGHT display->getHeight() - #include "graphics/ScreenFonts.h" #include -#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) +// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display); +// End Functions to write date/time to the screen -// Check if the display can render a string (detect special chars; emoji) -static bool haveGlyphs(const char *str) +extern bool hasUnreadMessage; + +// ============================== +// Overlay Alert Banner Renderer +// ============================== +// Displays a temporary centered banner message (e.g., warning, status, etc.) +// The banner appears in the center of the screen and disappears after the specified duration + +void Screen::showSimpleBanner(const char *message, uint32_t durationMs) { -#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) - // Don't want to make any assumptions about custom language support - return true; -#endif - - // Check each character with the lookup function for the OLED library - // We're not really meant to use this directly.. - bool have = true; - for (uint16_t i = 0; i < strlen(str); i++) { - uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); - // If font doesn't support a character, it is substituted for ยฟ - if (result == 191 && (uint8_t)str[i] != 191) { - have = false; - break; - } - } - - LOG_DEBUG("haveGlyphs=%d", have); - return have; + BannerOverlayOptions options; + options.message = message; + options.durationMs = durationMs; + options.notificationType = notificationTypeEnum::text_banner; + showOverlayBanner(options); } -/** - * Draw the icon with extra info printed around the corners - */ -static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// Called to trigger a banner with custom message and duration +void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) { - // draw an xbm image. - // Please note that everything that should be transitioned - // needs to be drawn relative to x and y - - // draw centered icon left to right and centered above the one line of app text - display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, - icon_width, icon_height, icon_bits); - - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = "meshtastic.org"; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); - - // Draw version and short name in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); - - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code -} - -#ifdef USERPREFS_OEM_TEXT - -static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); - - switch (USERPREFS_OEM_FONT_SIZE) { - case 0: - display->setFont(FONT_SMALL); - break; - case 2: - display->setFont(FONT_LARGE); - break; - default: - display->setFont(FONT_MEDIUM); - break; - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = USERPREFS_OEM_TEXT; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); - - // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); - - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code -} - -static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawOEMIconScreen(region, display, state, x, y); -} - -#endif - -void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) -{ - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, message); -} - -// Used on boot when a certificate is being created -static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(64 + x, y, "Creating SSL certificate"); - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif - - display->setFont(FONT_SMALL); - if ((millis() / 1000) % 2) { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); - } else { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); - } -} - -// Used when booting without a region set -static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C"); - display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName()); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if ((millis() / 10000) % 2) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients."); - } else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information."); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, ""); - } - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif -} - -// draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active -static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // LOG_DEBUG("Draw function overlay"); - if (functionSymbol.begin() != functionSymbol.end()) { - char buf[64]; - display->setFont(FONT_SMALL); - snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str()); - display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); - } -} - #ifdef USE_EINK -/// Used on eink displays while in deep sleep -static void drawDeepSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - - // Next frame should use full-refresh, and block while running, else device will sleep before async callback - EINK_ADD_FRAMEFLAG(display, COSMETIC); - EINK_ADD_FRAMEFLAG(display, BLOCKING); - - LOG_DEBUG("Draw deep sleep screen"); - - // Display displayStr on the screen - drawIconScreen("Sleeping", display, state, x, y); -} - -/// Used on eink displays when screen updates are paused -static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - LOG_DEBUG("Draw screensaver overlay"); - - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh - - // Config - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *pauseText = "Screen Paused"; - const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name - constexpr uint16_t padding = 5; - constexpr uint8_t dividerGap = 1; - constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. - - // Dimensions - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars - const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); - const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; - const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; - - // Position - const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); - // const int16_t boxRight = boxLeft + boxWidth - 1; - const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); - const int16_t boxBottom = boxTop + boxHeight - 1; - const int16_t idTextLeft = boxLeft + padding; - const int16_t idTextTop = boxTop + padding; - const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; - const int16_t pauseTextTop = boxTop + padding; - const int16_t dividerX = boxLeft + padding + idTextWidth + padding; - const int16_t dividerTop = boxTop + 1 + dividerGap; - const int16_t dividerBottom = boxBottom - 1 - dividerGap; - - // Draw: box - display->setColor(EINK_WHITE); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box - display->setColor(EINK_BLACK); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - - // Draw: Text - if (useId) - display->drawString(idTextLeft, idTextTop, idText); - display->drawString(pauseTextLeft, pauseTextTop, pauseText); - display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold - - // Draw: divider - if (useId) - display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); -} + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = + (banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs; + NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr; + NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr; + NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount; + NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback; + NotificationRenderer::curSelected = banner_overlay_options.InitialSelected; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); +} + +// Called to trigger a banner with custom message and duration +void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback) +{ +#ifdef USE_EINK + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus +#endif + nodeDB->pause_sort(true); + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; + NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; + + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); +} + +// Called to trigger a banner with custom message and duration +void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, + std::function bannerCallback) +{ +#ifdef USE_EINK + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus +#endif + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; + NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker; + NotificationRenderer::numDigits = digits; + NotificationRenderer::currentNumber = 0; + + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); +} static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -388,875 +238,12 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int pi.drawFrame(display, state, x, y); } -static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, y, "Updating"); - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), - "Please be patient and do not power off."); -} - -/// Draw the last text message we received -static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - - char tempBuf[24]; - snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); - display->drawString(0 + x, 0 + y, tempBuf); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); -} - // Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) { return packet->from != 0 && !moduleConfig.store_forward.enabled; } -// Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. -static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) -{ - static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; - static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; - // Clear the bar area on the battery image - for (int i = 1; i < 14; i++) { - imgBuffer[i] = 0x81; - } - // If charging, draw a charging indicator - if (powerStatus->getIsCharging()) { - memcpy(imgBuffer + 3, lightning, 8); - // If not charging, Draw power bars - } else { - for (int i = 0; i < 4; i++) { - if (powerStatus->getBatteryChargePercent() >= 25 * i) - memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); - } - } - display->drawFastImage(x, y, 16, 8, imgBuffer); -} - -#if defined(DISPLAY_CLOCK_FRAME) - -void Screen::drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - if (digitalMode) { - uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; - uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); - uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); - - display->drawCircle(centerX, centerY, radius); - display->drawCircle(centerX, centerY, radius + 1); - display->drawLine(centerX, centerY, centerX, centerY - radius + 3); - display->drawLine(centerX, centerY, centerX + radius - 3, centerY); - } else { - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentOneX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; - - uint16_t segmentFourX = x; - uint16_t segmentFourY = y + segmentHeight + 2; - - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } -} - -// Draw a digital clock -void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - - drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } - - if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); - } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - hour = hour > 12 ? hour - 12 : hour; - - if (hour == 0) { - hour = 12; - } - - // hours string - String hourString = String(hour); - - // minutes string - String minuteString = minute < 10 ? "0" + String(minute) : String(minute); - - String timeString = hourString + ":" + minuteString; - - // seconds string - String secondString = second < 10 ? "0" + String(second) : String(second); - - float scale = 1.5; - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // calculate hours:minutes string width - uint16_t timeStringWidth = timeString.length() * 5; - - for (uint8_t i = 0; i < timeString.length(); i++) { - String character = String(timeString[i]); - - if (character == ":") { - timeStringWidth += segmentHeight; - } else { - timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; - } - } - - // calculate seconds string width - uint16_t secondStringWidth = (secondString.length() * 12) + 4; - - // sum these to get total string width - uint16_t totalWidth = timeStringWidth + secondStringWidth; - - uint16_t hourMinuteTextX = (display->getWidth() / 2) - (totalWidth / 2); - - uint16_t startingHourMinuteTextX = hourMinuteTextX; - - uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); - - // iterate over characters in hours:minutes string and draw segmented characters - for (uint8_t i = 0; i < timeString.length(); i++) { - String character = String(timeString[i]); - - if (character == ":") { - drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); - - hourMinuteTextX += segmentHeight + 6; - } else { - drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character.toInt(), scale); - - hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; - } - - hourMinuteTextX += 5; - } - - // draw seconds string - display->setFont(FONT_MEDIUM); - display->drawString(startingHourMinuteTextX + timeStringWidth + 4, - (display->getHeight() - hourMinuteTextY) - FONT_HEIGHT_MEDIUM + 6, secondString); - } -} - -void Screen::drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - - uint16_t topAndBottomX = x + (4 * scale); - - uint16_t quarterCellHeight = cellHeight / 4; - - uint16_t topY = y + quarterCellHeight; - uint16_t bottomY = y + (quarterCellHeight * 3); - - display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); - display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); -} - -void Screen::drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) -{ - // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of - // segment {innerIndex + 1} - // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. - uint8_t numbers[10][7] = { - {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key - {0, 1, 1, 0, 0, 0, 0}, // 1 1 - {1, 1, 0, 1, 1, 0, 1}, // 2 ___ - {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 - {0, 1, 1, 0, 0, 1, 1}, // 4 |_7ฬฒ_| - {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 - {1, 0, 1, 1, 1, 1, 1}, // 6 |___| - {1, 1, 1, 0, 0, 1, 0}, // 7 - {1, 1, 1, 1, 1, 1, 1}, // 8 4 - {1, 1, 1, 1, 0, 1, 1}, // 9 - }; - - // the width and height of each segment's central rectangle: - // _____________________ - // โ‹ฐ| (only this part, |โ‹ฑ - // โ‹ฐ | not including | โ‹ฑ - // โ‹ฑ | the triangles | โ‹ฐ - // โ‹ฑ| on the ends) |โ‹ฐ - // โ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พ - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // segment x and y coordinates - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentTwoX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; - - uint16_t segmentFourX = segmentOneX; - uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; - - uint16_t segmentFiveX = x; - uint16_t segmentFiveY = segmentThreeY; - - uint16_t segmentSixX = x; - uint16_t segmentSixY = segmentTwoY; - - uint16_t segmentSevenX = segmentOneX; - uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; - - if (numbers[number][0]) { - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - } - - if (numbers[number][1]) { - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - } - - if (numbers[number][2]) { - drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - } - - if (numbers[number][3]) { - drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } - - if (numbers[number][4]) { - drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); - } - - if (numbers[number][5]) { - drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); - } - - if (numbers[number][6]) { - drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); - } -} - -void Screen::drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, width, height); - - // draw end triangles - display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); - - display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); -} - -void Screen::drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, height, width); - - // draw end triangles - display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); - - display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); -} - -void Screen::drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) -{ - display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon); -} - -// Draw an analog clock -void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - - drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } - - if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); - } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); - - // clock face center coordinates - int16_t centerX = display->getWidth() / 2; - int16_t centerY = display->getHeight() / 2; - - // clock face radius - int16_t radius = (display->getWidth() / 2) * 0.8; - - // noon (0 deg) coordinates (outermost circle) - int16_t noonX = centerX; - int16_t noonY = centerY - radius; - - // second hand radius and y coordinate (outermost circle) - int16_t secondHandNoonY = noonY + 1; - - // tick mark outer y coordinate; (first nested circle) - int16_t tickMarkOuterNoonY = secondHandNoonY; - - // seconds tick mark inner y coordinate; (second nested circle) - double secondsTickMarkInnerNoonY = (double)noonY + 8; - - // hours tick mark inner y coordinate; (third nested circle) - double hoursTickMarkInnerNoonY = (double)noonY + 16; - - // minute hand y coordinate - int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; - - // hour string y coordinate - int16_t hourStringNoonY = minuteHandNoonY + 18; - - // hour hand radius and y coordinate - int16_t hourHandRadius = radius * 0.55; - int16_t hourHandNoonY = centerY - hourHandRadius; - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawCircle(centerX, centerY, radius); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - hour = hour > 12 ? hour - 12 : hour; - - int16_t degreesPerHour = 30; - int16_t degreesPerMinuteOrSecond = 6; - - double hourBaseAngle = hour * degreesPerHour; - double hourAngleOffset = ((double)minute / 60) * degreesPerHour; - double hourAngle = radians(hourBaseAngle + hourAngleOffset); - - double minuteBaseAngle = minute * degreesPerMinuteOrSecond; - double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; - double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); - - double secondAngle = radians(second * degreesPerMinuteOrSecond); - - double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; - double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; - - double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; - double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; - - double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; - double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; - - display->setFont(FONT_MEDIUM); - - // draw minute and hour tick marks and hour numbers - for (uint16_t angle = 0; angle < 360; angle += 6) { - double angleInRadians = radians(angle); - - double sineAngleInRadians = sin(-angleInRadians); - double cosineAngleInRadians = cos(-angleInRadians); - - double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; - double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; - - if (angle % degreesPerHour == 0) { - double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; - - // draw hour tick mark - display->drawLine(startX, startY, endX, endY); - - static char buffer[2]; - - uint8_t hourInt = (angle / 30); - - if (hourInt == 0) { - hourInt = 12; - } - - // hour number x offset needs to be adjusted for some cases - int8_t hourStringXOffset; - int8_t hourStringYOffset = 13; - - switch (hourInt) { - case 3: - hourStringXOffset = 5; - break; - case 9: - hourStringXOffset = 7; - break; - case 10: - case 11: - hourStringXOffset = 8; - break; - case 12: - hourStringXOffset = 13; - break; - default: - hourStringXOffset = 6; - break; - } - - double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; - double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; - - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } - - if (angle % degreesPerMinuteOrSecond == 0) { - double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - - // draw minute tick mark - display->drawLine(startX, startY, endX, endY); - } - } - - // draw hour hand - display->drawLine(centerX, centerY, hourX, hourY); - - // draw minute hand - display->drawLine(centerX, centerY, minuteX, minuteY); - - // draw second hand - display->drawLine(centerX, centerY, secondX, secondY); - } -} - -#endif - -// Get an absolute time from "seconds ago" info. Returns false if no valid timestamp possible -bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) -{ - // Cache the result - avoid frequent recalculation - static uint8_t hoursCached = 0, minutesCached = 0; - static uint32_t daysAgoCached = 0; - static uint32_t secondsAgoCached = 0; - static bool validCached = false; - - // Abort: if timezone not set - if (strlen(config.device.tzdef) == 0) { - validCached = false; - return validCached; - } - - // Abort: if invalid pointers passed - if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { - validCached = false; - return validCached; - } - - // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) - if (secondsAgo > SEC_PER_DAY * 30UL * 6) { - validCached = false; - return validCached; - } - - // If repeated request, don't bother recalculating - if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { - if (validCached) { - *hours = hoursCached; - *minutes = minutesCached; - *daysAgo = daysAgoCached; - } - return validCached; - } - - // Get local time - uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time - - // Abort: if RTC not set - if (!secondsRTC) { - validCached = false; - return validCached; - } - - // Get absolute time when last seen - uint32_t secondsSeenAt = secondsRTC - secondsAgo; - - // Calculate daysAgo - *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed - - // Get seconds since midnight - uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into hours and minutes - *hours = hms / SEC_PER_HOUR; - *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - - // Cache the result - daysAgoCached = *daysAgo; - hoursCached = *hours; - minutesCached = *minutes; - secondsAgoCached = secondsAgo; - - validCached = true; - return validCached; -} - -/// Draw the last text message we received -static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // the max length of this buffer is much longer than we can possibly print - static char tempBuf[237]; - - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - // LOG_DEBUG("Draw text message from 0x%x: %s", mp.from, - // mp.decoded.variant.data.decoded.bytes); - - // Demo for drawStringMaxWidth: - // with the third parameter you can define the width after which words will - // be wrapped. Currently only spaces and "-" are allowed for wrapping - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - // For time delta - uint32_t seconds = sinceReceived(&mp); - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - - // For timestamp - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); - - // If bold, draw twice, shifting right by one pixel - for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) { - // Show a timestamp if received today, but longer than 15 minutes ago - if (useTimestamp && minutes >= 15 && daysAgo == 0) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "At %02hu:%02hu from %s", timestampHours, timestampMinutes, - (node && node->has_user) ? node->user.short_name : "???"); - } - // Timestamp yesterday (if display is wide enough) - else if (useTimestamp && daysAgo == 1 && display->width() >= 200) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "Yesterday %02hu:%02hu from %s", timestampHours, timestampMinutes, - (node && node->has_user) ? node->user.short_name : "???"); - } - // Otherwise, show a time delta - else { - display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", - screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); - } - } - - display->setColor(WHITE); -#ifndef EXCLUDE_EMOJI - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - if (strcmp(msg, "\U0001F44D") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, - thumbup); - } else if (strcmp(msg, "\U0001F44E") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, - thumbdown); - } else if (strcmp(msg, "\U0001F60A") == 0 || strcmp(msg, "\U0001F600") == 0 || strcmp(msg, "\U0001F642") == 0 || - strcmp(msg, "\U0001F609") == 0 || - strcmp(msg, "\U0001F601") == 0) { // matches 5 different common smileys, so that the phone user doesn't have to - // remember which one is compatible - display->drawXbm(x + (SCREEN_WIDTH - smiley_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - smiley_height) / 2 + 2 + 5, smiley_width, smiley_height, - smiley); - } else if (strcmp(msg, "โ“") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - question_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - question_height) / 2 + 2 + 5, question_width, question_height, - question); - } else if (strcmp(msg, "โ€ผ๏ธ") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - bang_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - bang_height) / 2 + 2 + 5, - bang_width, bang_height, bang); - } else if (strcmp(msg, "\U0001F4A9") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - poo_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - poo_height) / 2 + 2 + 5, - poo_width, poo_height, poo); - } else if (strcmp(msg, "\U0001F923") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - haha_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - haha_height) / 2 + 2 + 5, - haha_width, haha_height, haha); - } else if (strcmp(msg, "\U0001F44B") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - wave_icon_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - wave_icon_height) / 2 + 2 + 5, wave_icon_width, - wave_icon_height, wave_icon); - } else if (strcmp(msg, "\U0001F920") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - cowboy_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cowboy_height) / 2 + 2 + 5, cowboy_width, cowboy_height, - cowboy); - } else if (strcmp(msg, "\U0001F42D") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - deadmau5_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - deadmau5_height) / 2 + 2 + 5, deadmau5_width, deadmau5_height, - deadmau5); - } else if (strcmp(msg, "\xE2\x98\x80\xEF\xB8\x8F") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - sun_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - sun_height) / 2 + 2 + 5, - sun_width, sun_height, sun); - } else if (strcmp(msg, "\u2614") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - rain_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - rain_height) / 2 + 2 + 10, - rain_width, rain_height, rain); - } else if (strcmp(msg, "โ˜๏ธ") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - cloud_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cloud_height) / 2 + 2 + 5, cloud_width, cloud_height, cloud); - } else if (strcmp(msg, "๐ŸŒซ๏ธ") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - fog_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - fog_height) / 2 + 2 + 5, - fog_width, fog_height, fog); - } else if (strcmp(msg, "\U0001F608") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - devil_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - devil_height) / 2 + 2 + 5, devil_width, devil_height, devil); - } else if (strcmp(msg, "โ™ฅ๏ธ") == 0 || strcmp(msg, "\U0001F9E1") == 0 || strcmp(msg, "\U00002763") == 0 || - strcmp(msg, "\U00002764") == 0 || strcmp(msg, "\U0001F495") == 0 || strcmp(msg, "\U0001F496") == 0 || - strcmp(msg, "\U0001F497") == 0 || strcmp(msg, "\U0001F498") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - heart_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - heart_height) / 2 + 2 + 5, heart_width, heart_height, heart); - } else { - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); - } -#else - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); -#endif -} - -/// Draw a series of fields in a column, wrapping to multiple columns if needed -void Screen::drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) -{ - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - const char **f = fields; - int xo = x, yo = y; - while (*f) { - display->drawString(xo, yo, *f); - if ((display->getColor() == BLACK) && config.display.heading_bold) - display->drawString(xo + 1, yo, *f); - - display->setColor(WHITE); - yo += FONT_HEIGHT_SMALL; - if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { - xo += SCREEN_WIDTH / 2; - yo = 0; - } - f++; - } -} - -// Draw nodes status -static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus) -{ - char usersString[20]; - snprintf(usersString, sizeof(usersString), "%d/%d", nodeStatus->getNumOnline(), nodeStatus->getNumTotal()); -#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(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, y + 3, 8, 8, imgUser); -#else - display->drawFastImage(x, y, 8, 8, imgUser); -#endif - display->drawString(x + 10, y - 2, usersString); - if (config.display.heading_bold) - display->drawString(x + 11, y - 2, usersString); -} -#if HAS_GPS -// Draw GPS status summary -static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - if (config.position.fixed_position) { - // GPS coordinates are currently fixed - display->drawString(x - 1, y - 2, "Fixed GPS"); - if (config.display.heading_bold) - display->drawString(x, y - 2, "Fixed GPS"); - return; - } - if (!gps->getIsConnected()) { - display->drawString(x, y - 2, "No GPS"); - if (config.display.heading_bold) - display->drawString(x + 1, y - 2, "No GPS"); - return; - } - display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); - if (!gps->getHasLock()) { - display->drawString(x + 8, y - 2, "No sats"); - if (config.display.heading_bold) - display->drawString(x + 9, y - 2, "No sats"); - return; - } else { - char satsString[3]; - uint8_t bar[2] = {0}; - - // Draw DOP signal bars - for (int i = 0; i < 5; i++) { - if (gps->getDOP() <= dopThresholds[i]) - bar[0] = ~((1 << (5 - i)) - 1); - else - bar[0] = 0b10000000; - // bar[1] = bar[0]; - display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); - } - - // Draw satellite image - display->drawFastImage(x + 24, y, 8, 8, imgSatellite); - - // Draw the number of satellites - snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); - display->drawString(x + 34, y - 2, satsString); - if (config.display.heading_bold) - display->drawString(x + 35, y - 2, satsString); - } -} - -// Draw status when GPS is disabled or not present -static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - String displayLine; - int pos; - if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - pos = SCREEN_WIDTH - display->getStringWidth(displayLine); - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" - : "GPS is disabled"; - pos = (SCREEN_WIDTH - display->getStringWidth(displayLine)) / 2; - } - display->drawString(x + pos, y, displayLine); -} - -static void drawGPSAltitude(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - String displayLine = ""; - if (!gps->getIsConnected() && !config.position.fixed_position) { - // displayLine = "No GPS Module"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - // displayLine = "No GPS Lock"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - displayLine = "Altitude: " + String(geoCoord.getAltitude()) + "m"; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = "Altitude: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } -} - -// Draw GPS status coordinates -static void drawGPScoordinates(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - auto gpsFormat = config.display.gps_format; - String displayLine = ""; - - if (!gps->getIsConnected() && !config.position.fixed_position) { - displayLine = "No GPS present"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - displayLine = "No GPS Lock"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - - if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { - char coordinateLine[22]; - if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, - geoCoord.getLongitude() * 1e-7); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), - geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), - geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), - geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region - snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); - else - snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), - geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); - } - - // If fixed position, display text "Fixed GPS" alternating with the coordinates. - if (config.position.fixed_position) { - if ((millis() / 10000) % 2) { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); - } else { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); - } - } else { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); - } - } else { - char latLine[22]; - char lonLine[22]; - snprintf(latLine, sizeof(latLine), "%2iยฐ %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), - geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(lonLine, sizeof(lonLine), "%3iยฐ %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), - geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, latLine); - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(lonLine))) / 2, y, lonLine); - } - } -} -#endif /** * Given a recent lat/lon return a guess of the heading the user is walking on. * @@ -1280,7 +267,7 @@ float Screen::estimatedHeading(double lat, double lon) if (d < 10) // haven't moved enough, just keep current bearing return b; - b = GeoCoord::bearing(oldLat, oldLon, lat, lon); + b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; oldLat = lat; oldLon = lon; @@ -1289,239 +276,10 @@ float Screen::estimatedHeading(double lat, double lon) /// We will skip one node - the one for us, so we just blindly loop over all /// nodes -static size_t nodeIndex; static int8_t prevFrame = -1; -// Draw the arrow pointing to a node's location -void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) -{ - Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; - Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); - - Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; - - for (int i = 0; i < 4; i++) { - arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(compassDiam * 0.6); - arrowPoints[i]->translate(compassX, compassY); - } - /* Old arrow - display->drawLine(tip.x, tip.y, tail.x, tail.y); - display->drawLine(leftArrow.x, leftArrow.y, tip.x, tip.y); - display->drawLine(rightArrow.x, rightArrow.y, tip.x, tip.y); - display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); - display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); - */ -#ifdef USE_EINK - display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#else - display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#endif - display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); -} - -// Get a string representation of the time passed since something happened -void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) -{ - // Use an absolute timestamp in some cases. - // Particularly useful with E-Ink displays. Static UI, fewer refreshes. - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); - - if (agoSecs < 120) // last 2 mins? - snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); - // -- if suitable for timestamp -- - else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); - else if (useTimestamp && daysAgo == 0) // Today - snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); - else if (useTimestamp && daysAgo == 1) // Yesterday - snprintf(timeStr, maxLength, "Seen yesterday"); - else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) - snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); - // -- if using time delta instead -- - else if (agoSecs < 120 * 60) // last 2 hrs - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); - // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. - else if ((agoSecs / 60 / 60) < (hours_in_month * 6)) - snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); - else - snprintf(timeStr, maxLength, "unknown age"); -} - -void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) -{ - // If north is supposed to be at the top of the compass we want rotation to be +0 - if (config.display.compass_north_top) - myHeading = -0; - /* N sign points currently not deleted*/ - Point N1(-0.04f, 0.65f), N2(0.04f, 0.65f); // N sign points (N1-N4) - Point N3(-0.04f, 0.55f), N4(0.04f, 0.55f); - Point NC1(0.00f, 0.50f); // north circle center point - Point *rosePoints[] = {&N1, &N2, &N3, &N4, &NC1}; - - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - - for (int i = 0; i < 5; i++) { - // North on compass will be negative of heading - rosePoints[i]->rotate(-myHeading); - rosePoints[i]->scale(compassDiam); - rosePoints[i]->translate(compassX, compassY); - } - - /* changed the N sign to a small circle on the compass circle. - display->drawLine(N1.x, N1.y, N3.x, N3.y); - display->drawLine(N2.x, N2.y, N4.x, N4.y); - display->drawLine(N1.x, N1.y, N4.x, N4.y); - */ - display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. -} - -uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) -{ - uint16_t diam = 0; - uint16_t offset = 0; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - offset = FONT_HEIGHT_SMALL; - - // get the smaller of the 2 dimensions and subtract 20 - if (displayWidth > (displayHeight - offset)) { - diam = displayHeight - offset; - // if 2/3 of the other size would be smaller, use that - if (diam > (displayWidth * 2 / 3)) { - diam = displayWidth * 2 / 3; - } - } else { - diam = displayWidth; - if (diam > ((displayHeight - offset) * 2 / 3)) { - diam = (displayHeight - offset) * 2 / 3; - } - } - - return diam - 20; -}; - -static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // We only advance our nodeIndex if the frame # has changed - because - // drawNodeInfo will be called repeatedly while the frame is shown - if (state->currentFrame != prevFrame) { - prevFrame = state->currentFrame; - - nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(nodeIndex); - if (n->num == nodeDB->getNodeNum()) { - // Don't show our node, just skip to next - nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); - n = nodeDB->getMeshNodeByIndex(nodeIndex); - } - } - - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(nodeIndex); - - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - } - - const char *username = node->has_user ? node->user.long_name : "Unknown Name"; - - static char signalStr[20]; - - // section here to choose whether to display hops away rather than signal strength if more than 0 hops away. - if (node->hops_away > 0) { - snprintf(signalStr, sizeof(signalStr), "Hops Away: %d", node->hops_away); - } else { - snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); - } - - static char lastStr[20]; - screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); - - static char distStr[20]; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - strncpy(distStr, "? mi ?ยฐ", sizeof(distStr)); // might not have location data - } else { - strncpy(distStr, "? km ?ยฐ", sizeof(distStr)); - } - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; - int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - - // coordinates for the center of the compass/circle - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; - compassY = y + SCREEN_HEIGHT / 2; - } else { - compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (SCREEN_HEIGHT - FONT_HEIGHT_SMALL) / 2; - } - bool hasNodeHeading = false; - - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { - const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); - - if (nodeDB->hasValidPosition(node)) { - // display direction toward node - hasNodeHeading = true; - const meshtastic_PositionLite &p = node->position; - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (!config.display.compass_north_top) - bearingToOther -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; - - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - if (d < (2 * MILES_TO_FEET)) - snprintf(distStr, sizeof(distStr), "%.0fft %.0fยฐ", d * METERS_TO_FEET, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fmi %.0fยฐ", d * METERS_TO_FEET / MILES_TO_FEET, - bearingToOtherDegrees); - } else { - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0fm %.0fยฐ", d, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fkm %.0fยฐ", d / 1000, bearingToOtherDegrees); - } - } - } - if (!hasNodeHeading) { - // direction to node is unknown so display question mark - // Debug info for gps lock errors - // LOG_DEBUG("ourNode %d, ourPos %d, theirPos %d", !!ourNode, ourNode && hasValidPosition(ourNode), - // hasValidPosition(node)); - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); - } - display->drawCircle(compassX, compassY, compassDiam / 2); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - // Must be after distStr is populated - screen->drawColumns(display, x, y, fields); -} +// Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes +// Uses a single frame and changes data every few seconds (E-Ink variant is separate) #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); @@ -1531,6 +289,20 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) { graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; + + LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color); + int32_t rawRGB = uiconfig.screen_rgb_color; + if (rawRGB > 0 && rawRGB <= 255255255) { + uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF; + uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF; + uint8_t TFT_MESH_b = rawRGB & 0xFF; + LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + + if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) { + TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + } + } + #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) dispdev = new SH1106Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -1541,6 +313,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #else dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif + static_cast(dispdev)->setRGB(TFT_MESH); #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -1557,15 +330,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif ARCH_PORTDUINO && !HAS_TFT - if (settingsMap[displayPanel] != no_screen) { - LOG_DEBUG("Make TFTDisplay!"); - dispdev = new TFTDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - } else { - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; +#elif ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (settingsMap[displayPanel] != no_screen) { + LOG_DEBUG("Make TFTDisplay!"); + dispdev = new TFTDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + } else { + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; + } } #else dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, @@ -1589,7 +364,7 @@ Screen::~Screen() void Screen::doDeepSleep() { #ifdef USE_EINK - setOn(false, drawDeepSleepScreen); + setOn(false, graphics::UIRenderer::drawDeepSleepFrame); #ifdef PIN_EINK_EN digitalWrite(PIN_EINK_EN, LOW); // power off backlight #endif @@ -1607,7 +382,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) if (on != screenOn) { if (on) { LOG_INFO("Turn on screen"); - buttonThread->setScreenFlag(true); powerMon->setState(meshtastic_PowerMon_State_Screen_On); #ifdef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_ALDO2); @@ -1651,8 +425,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) // eInkScreensaver parameter is usually NULL (default argument), default frame used instead setScreensaverFrames(einkScreensaver); #endif - LOG_INFO("Turn off screen"); - buttonThread->setScreenFlag(false); #ifdef ELECROW_ThinkNode_M1 if (digitalRead(PIN_EINK_EN) == HIGH) { digitalWrite(PIN_EINK_EN, LOW); @@ -1687,10 +459,23 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) void Screen::setup() { - // We don't set useDisplay until setup() is called, because some boards have a declaration of this object but the device - // is never found when probing i2c and therefore we don't call setup and never want to do (invalid) accesses to this device. + + // === Enable display rendering === useDisplay = true; + // === Load saved brightness from UI config === + // For OLED displays (SSD1306), default brightness is 255 if not set + if (uiconfig.screen_brightness == 0) { +#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + brightness = 255; // Default for OLED +#else + brightness = BRIGHTNESS_DEFAULT; +#endif + } else { + brightness = uiconfig.screen_brightness; + } + + // === Detect OLED subtype (if supported by board variant) === #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); @@ -1701,66 +486,69 @@ void Screen::setup() #endif #if defined(USE_ST7789) && defined(TFT_MESH) - // Heltec T114 and T190: honor a custom text color, if defined in variant.h + // Apply custom RGB color (e.g. Heltec T114/T190) static_cast(dispdev)->setRGB(TFT_MESH); #endif - // Initialising the UI will init the display too. + // === Initialize display and UI system === ui->init(); - displayWidth = dispdev->width(); displayHeight = dispdev->height(); - ui->setTimePerTransition(0); + ui->setTimePerTransition(0); // Disable animation delays + ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) + ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) + ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active + ui->disableAllIndicators(); // Disable page indicator dots + ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - ui->setIndicatorPosition(BOTTOM); - // Defines where the first frame is located in the bar. - ui->setIndicatorDirection(LEFT_RIGHT); - ui->setFrameAnimation(SLIDE_LEFT); - // Don't show the page swipe dots while in boot screen. - ui->disableAllIndicators(); - // Store a pointer to Screen so we can get to it from static functions. - ui->getUiState()->userData = this; + // === Apply loaded brightness === +#if defined(ST7789_CS) + static_cast(dispdev)->setDisplayBrightness(brightness); +#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + dispdev->setBrightness(brightness); +#endif + LOG_INFO("Applied screen brightness: %d", brightness); - // Set the utf8 conversion function + // === Set custom overlay callbacks === + static OverlayCallback overlays[] = { + graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + + // === Enable UTF-8 to display mapping === dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT - logo_timeout *= 2; // Double the time if we have a custom logo + logo_timeout *= 2; // Give more time for branded boot logos #endif - // Add frames. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + // === Configure alert frames (e.g., "Resuming..." or region name) === + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) { - drawFrameText(display, state, x, y, "Resuming..."); - } else + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) + graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); + else #endif { - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawIconScreen(region, display, state, x, y); + const char *region = myRegion ? myRegion->name : nullptr; + graphics::UIRenderer::drawIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); - // No overlays. - ui->setOverlays(nullptr, 0); + ui->disableAutoTransition(); // Require manual navigation between frames - // Require presses to switch between frames. - ui->disableAutoTransition(); - - // Set up a log buffer with 3 lines, 32 chars each. + // === Log buffer for on-screen logs (3 lines max) === dispdev->setLogBuffer(3, 32); + // === Optional screen mirroring or flipping (e.g. for T-Beam orientation) === #ifdef SCREEN_MIRROR dispdev->mirrorScreen(); #else - // Standard behaviour is to FLIP the screen (needed on T-Beam). If this config item is set, unflip it, and thereby logically - // flip it. If you have a headache now, you're welcome. if (!config.display.flip_screen) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \ - defined(ST7789_CS) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); @@ -1770,30 +558,31 @@ void Screen::setup() } #endif - // Get our hardware ID + // === Generate device ID from MAC address === uint8_t dmac[6]; getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + #if ARCH_PORTDUINO - handleSetOn(false); // force clean init + handleSetOn(false); // Ensure proper init for Arduino targets #endif - // Turn on the display. + // === Turn on display and trigger first draw === handleSetOn(true); - - // On some ssd1306 clones, the first draw command is discarded, so draw it - // twice initially. Skip this for EINK Displays to save a few seconds during boot + determineResolution(dispdev->height(), dispdev->width()); ui->update(); #ifndef USE_EINK - ui->update(); + ui->update(); // Some SSD1306 clones drop the first draw, so run twice #endif serialSinceMsec = millis(); -#if ARCH_PORTDUINO && !HAS_TFT - if (settingsMap[touchscreenModule]) { - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (settingsMap[touchscreenModule]) { + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); + } } #elif HAS_TOUCHSCREEN touchScreenImpl1 = @@ -1801,10 +590,11 @@ void Screen::setup() touchScreenImpl1->init(); #endif - // Subscribe to status updates + // === Subscribe to device status updates === powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); + #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif @@ -1813,7 +603,7 @@ void Screen::setup() if (inputBroker) inputObserver.observe(inputBroker); - // Modules can notify screen about refresh + // === Notify modules that support UI events === MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -1850,6 +640,11 @@ void Screen::forceDisplay(bool forceUiUpdate) // Tell EInk class to update the display static_cast(dispdev)->forceDisplay(); +#else + // No delay between UI frame rendering + if (forceUiUpdate) { + setFastFramerate(); + } #endif } @@ -1866,6 +661,7 @@ int32_t Screen::runOnce() if (displayHeight == 0) { displayHeight = dispdev->getHeight(); } + menuHandler::handleMenuSwitch(dispdev); // Show boot screen for first logo_timeout seconds, then switch to normal operation. // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup @@ -1881,7 +677,7 @@ int32_t Screen::runOnce() if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { LOG_INFO("Switch to OEM screen..."); // Change frames. - static FrameCallback bootOEMFrames[] = {drawOEMBootScreen}; + static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); ui->setFrames(bootOEMFrames, bootOEMFrameCount); ui->update(); @@ -1893,10 +689,13 @@ int32_t Screen::runOnce() #endif #ifndef DISABLE_WELCOME_UNSET - if (showingNormalScreen && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - setWelcomeFrames(); + if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + menuHandler::LoraRegionPicker(0); } #endif + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + showSimpleBanner("Rebooting...", 0); + } // Process incoming commands. for (;;) { @@ -1923,6 +722,7 @@ int32_t Screen::runOnce() case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away showingNormalScreen = false; + NotificationRenderer::pauseBanner = true; alertFrames[0] = alertFrame; #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please @@ -1936,13 +736,12 @@ int32_t Screen::runOnce() handleStartFirmwareUpdateScreen(); break; case Cmd::STOP_ALERT_FRAME: + NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame setFrames(); break; - case Cmd::PRINT: - handlePrint(cmd.print_text); - free(cmd.print_text); + case Cmd::NOOP: break; default: LOG_ERROR("Invalid screen cmd"); @@ -1962,6 +761,7 @@ int32_t Screen::runOnce() // Switch to a low framerate (to save CPU) when we are not in transition // but we should only call setTargetFPS when framestate changes, because // otherwise that breaks animations. + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; targetFramerate = IDLE_FRAMERATE; @@ -1977,8 +777,8 @@ int32_t Screen::runOnce() if (config.display.auto_screen_carousel_secs > 0 && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { -// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead -// Carousel is potentially a major source of E-Ink display wear + // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead + // Carousel is potentially a major source of E-Ink display wear #if !defined(EINK_BACKGROUND_USES_FAST) EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); #endif @@ -1996,47 +796,18 @@ int32_t Screen::runOnce() return (1000 / targetFramerate); } -void Screen::drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrame(display, state, x, y); -} - -void Screen::drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrameSettings(display, state, x, y); -} - -void Screen::drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrameWiFi(display, state, x, y); -} - /* show a message that the SSL cert is being built * it is expected that this will be used during the boot phase */ void Screen::setSSLFrames() { if (address_found.address) { // LOG_DEBUG("Show SSL frames"); - static FrameCallback sslFrames[] = {drawSSLScreen}; + static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; ui->setFrames(sslFrames, 1); ui->update(); } } -/* show a message that the SSL cert is being built - * it is expected that this will be used during the boot phase */ -void Screen::setWelcomeFrames() -{ - if (address_found.address) { - // LOG_DEBUG("Show Welcome frames"); - static FrameCallback frames[] = {drawWelcomeScreen}; - setFrameImmediateDraw(frames); - } -} - #ifdef USE_EINK /// Determine which screensaver frame to use, then set the FrameCallback void Screen::setScreensaverFrames(FrameCallback einkScreensaver) @@ -2059,7 +830,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Else, display the usual "overlay" screensaver else { - screensaverOverlay = drawScreensaverOverlay; + screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; ui->setOverlays(&screensaverOverlay, 1); } @@ -2095,125 +866,177 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) void Screen::setFrames(FrameFocus focus) { uint8_t originalPosition = ui->getUiState()->currentFrame; + uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter LOG_DEBUG("Show standard frames"); showingNormalScreen = true; + indicatorIcons.clear(); + + size_t numframes = 0; + + // If we have a critical fault, show it first + fsi.positions.fault = numframes; + if (error_code) { + normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; + indicatorIcons.push_back(icon_error); + focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame + } + +#if defined(DISPLAY_CLOCK_FRAME) + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); +#endif + + // Declare this early so itโ€™s available in FOCUS_PRESERVE block + bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); + + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); + +#ifndef USE_EINK + fsi.positions.nodelist = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); +#endif + +// Show detailed node views only on E-Ink builds #ifdef USE_EINK - // If user has disabled the screensaver, warn them after boot - static bool warnedScreensaverDisabled = false; - if (config.display.screen_on_secs == 0 && !warnedScreensaverDisabled) { - screen->print("Screensaver disabled\n"); - warnedScreensaverDisabled = true; + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); +#endif +#if HAS_GPS + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); +#endif + if (RadioLibInterface::instance) { + fsi.positions.lora = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + } + if (!dismissedFrames.memory) { + fsi.positions.memory = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; + indicatorIcons.push_back(icon_memory); + } +#if !defined(DISPLAY_CLOCK_FRAME) + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); +#endif + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + if (!dismissedFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); } #endif - moduleFrames = MeshModule::GetMeshModulesWithUIFrames(); + // Beware of what changes you make in this code! + // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! + // Inside of that callback, goes over to MeshModule.cpp and we run + // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr + // entries until we're ready to start building the matching entries. + // We are doing our best to keep the normalFrames vector + // and the moduleFrames vector in lock step. + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); LOG_DEBUG("Show %d module frames", moduleFrames.size()); -#ifdef DEBUG_PORT - int totalFrameCount = MAX_NUM_NODES + NUM_EXTRA_FRAMES + moduleFrames.size(); - LOG_DEBUG("Total frame count: %d", totalFrameCount); -#endif + + for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { + // Draw the module frame, using the hack described above + if (*i != nullptr) { + normalFrames[numframes] = drawModuleFrame; + + // Check if the module being drawn has requested focus + // We will honor this request later, if setFrames was triggered by a UIFrameEvent + MeshModule *m = *i; + if (m && m->isRequestingFocus()) + fsi.positions.focusedModule = numframes; + if (m && m == waypointModule) + fsi.positions.waypoint = numframes; + + indicatorIcons.push_back(icon_module); + numframes++; + } + } + + LOG_DEBUG("Added modules. numframes: %d", numframes); // We don't show the node info of our node (if we have it yet - we should) size_t numMeshNodes = nodeDB->getNumMeshNodes(); if (numMeshNodes > 0) numMeshNodes--; - size_t numframes = 0; + // Temporary array to hold favorite node frames + std::vector favoriteFrames; - // put all of the module frames first. - // this is a little bit of a dirty hack; since we're going to call - // the same drawModuleFrame handler here for all of these module frames - // and then we'll just assume that the state->currentFrame value - // is the same offset into the moduleFrames vector - // so that we can invoke the module's callback - for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { - // Draw the module frame, using the hack described above - normalFrames[numframes] = drawModuleFrame; - - // Check if the module being drawn has requested focus - // We will honor this request later, if setFrames was triggered by a UIFrameEvent - MeshModule *m = *i; - if (m->isRequestingFocus()) { - fsi.positions.focusedModule = numframes; + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); } - - // Identify the position of specific modules, if we need to know this later - if (m == waypointModule) - fsi.positions.waypoint = numframes; - - numframes++; } - LOG_DEBUG("Added modules. numframes: %d", numframes); - - // If we have a critical fault, show it first - fsi.positions.fault = numframes; - if (error_code) { - normalFrames[numframes++] = drawCriticalFaultFrame; - focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; } -#if defined(DISPLAY_CLOCK_FRAME) - normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; -#endif - - // If we have a text message - show it next, unless it's a phone message and we aren't using any special modules - if (devicestate.has_rx_text_message && shouldDrawMessage(&devicestate.rx_text_message)) { - fsi.positions.textMessage = numframes; - normalFrames[numframes++] = drawTextMessageFrame; - } - - // then all the nodes - // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens - size_t numToShow = min(numMeshNodes, 4U); - for (size_t i = 0; i < numToShow; i++) - normalFrames[numframes++] = drawNodeInfo; - - // then the debug info - // - // Since frames are basic function pointers, we have to use a helper to - // call a method on debugInfo object. - fsi.positions.log = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; - - // call a method on debugInfoScreen object (for more details) - fsi.positions.settings = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; - - fsi.positions.wifi = numframes; -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (isWifiAvailable()) { - // call a method on debugInfoScreen object (for more details) - normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; - } -#endif - - fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + this->frameCount = numframes; // โœ… Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); ui->setFrames(normalFrames, numframes); - ui->enableAllIndicators(); + ui->disableAllIndicators(); - // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback functionOverlay[] = {drawFunctionOverlay}; - static const int functionOverlayCount = sizeof(functionOverlay) / sizeof(functionOverlay[0]); - ui->setOverlays(functionOverlay, functionOverlayCount); + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list - // just changed) + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) // Focus on a specific frame, in the frame set we just created switch (focus) { case FOCUS_DEFAULT: - ui->switchToFrame(0); // First frame + ui->switchToFrame(fsi.positions.deviceFocused); break; case FOCUS_FAULT: ui->switchToFrame(fsi.positions.fault); break; case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // โœ… Clear when message is *viewed* ui->switchToFrame(fsi.positions.textMessage); break; case FOCUS_MODULE: @@ -2221,33 +1044,24 @@ void Screen::setFrames(FrameFocus focus) // If no module requested focus, will show the first frame instead ui->switchToFrame(fsi.positions.focusedModule); break; + case FOCUS_CLOCK: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.clock); + break; + case FOCUS_SYSTEM: + ui->switchToFrame(fsi.positions.memory); + break; case FOCUS_PRESERVE: - // If we can identify which type of frame "originalPosition" was, can move directly to it in the new frameset - const FramesetInfo &oldFsi = this->framesetInfo; - if (originalPosition == oldFsi.positions.log) - ui->switchToFrame(fsi.positions.log); - else if (originalPosition == oldFsi.positions.settings) - ui->switchToFrame(fsi.positions.settings); - else if (originalPosition == oldFsi.positions.wifi) - ui->switchToFrame(fsi.positions.wifi); - - // If frame count has decreased - else if (fsi.frameCount < oldFsi.frameCount) { - uint8_t numDropped = oldFsi.frameCount - fsi.frameCount; - // Move n frames backwards - if (numDropped <= originalPosition) - ui->switchToFrame(originalPosition - numDropped); - // Unless that would put us "out of bounds" (< 0) - else - ui->switchToFrame(0); - } - - // If we're not sure exactly which frame we were on, at least return to the same frame number - // (node frames; module frames) - else + // No more adjustment โ€” force stay on same index + if (previousFrameCount > fsi.frameCount) { + ui->switchToFrame(originalPosition - 1); + } else if (previousFrameCount < fsi.frameCount) { + ui->switchToFrame(originalPosition + 1); + } else { ui->switchToFrame(originalPosition); - + } break; } @@ -2275,18 +1089,25 @@ void Screen::dismissCurrentFrame() if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { LOG_INFO("Dismiss Text Message"); devicestate.has_rx_text_message = false; - dismissed = true; - } - - else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { LOG_DEBUG("Dismiss Waypoint"); devicestate.has_rx_waypoint = false; + dismissedFrames.waypoint = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.wifi) { + LOG_DEBUG("Dismiss WiFi Screen"); + dismissedFrames.wifi = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.memory) { + LOG_INFO("Dismiss Memory"); + dismissedFrames.memory = true; dismissed = true; } - // If we did make changes to dismiss, we now need to regenerate the frameset - if (dismissed) - setFrames(); + if (dismissed) { + setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE + } } void Screen::handleStartFirmwareUpdateScreen() @@ -2295,7 +1116,7 @@ void Screen::handleStartFirmwareUpdateScreen() showingNormalScreen = false; EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {drawFrameFirmware}; + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; setFrameImmediateDraw(frames); } @@ -2362,41 +1183,8 @@ void Screen::removeFunctionSymbol(std::string sym) setFastFramerate(); } -std::string Screen::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) -{ - std::string uptime; - - if (days > (hours_in_month * 6)) - uptime = "?"; - else if (days >= 2) - uptime = std::to_string(days) + "d"; - else if (hours >= 2) - uptime = std::to_string(hours) + "h"; - else if (minutes >= 1) - uptime = std::to_string(minutes) + "m"; - else - uptime = std::to_string(seconds) + "s"; - return uptime; -} - -void Screen::handlePrint(const char *text) -{ - // the string passed into us probably has a newline, but that would confuse the logging system - // so strip it - LOG_DEBUG("Screen: %.*s", strlen(text) - 1, text); - if (!useDisplay || !showingNormalScreen) - return; - - dispdev->print(text); -} - void Screen::handleOnPress() { - // If Canned Messages is using the "Scan and Select" input, dismiss the canned message frame when user button is pressed - // Minimize impact as a courtesy, as "scan and select" may be used as default config for some boards - if (scanAndSelectInput != nullptr && scanAndSelectInput->dismissCannedMessageFrame()) - return; - // If screen was off, just wake it, otherwise advance to next frame // If we are in a transition, the press must have bounced, drop it. if (ui->getUiState()->frameState == FIXED) { @@ -2442,321 +1230,6 @@ void Screen::setFastFramerate() runASAP = true; } -void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - char channelStr[20]; - { - concurrency::LockGuard guard(&lock); - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - } - - // Display power status - if (powerStatus->getHasBattery()) { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - drawBattery(display, x, y + 2, imgBattery, powerStatus); - } else { - drawBattery(display, x + 1, y + 3, imgBattery, powerStatus); - } - } else if (powerStatus->knowsUSB()) { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - } else { - display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - } - } - // Display nodes status - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); - } else { - drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); - } -#if HAS_GPS - // Display GPS status - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - drawGPSpowerstat(display, x, y + 2, gpsStatus); - } else { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); - } else { - drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); - } - } -#endif - display->setColor(WHITE); - // Draw the channel name - display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); - // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo - if (moduleConfig.store_forward.enabled) { -#ifdef ARCH_ESP32 - 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(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, - imgQuestionL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, - imgQuestionL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, - imgQuestion); -#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(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, - imgSFL1); - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, - imgSFL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, - imgSF); -#endif - } -#endif - } 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(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, - imgInfoL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, - imgInfoL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); -#endif - } - - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(ourId), y + FONT_HEIGHT_SMALL, ourId); - - // Draw any log messages - display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); - - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ -#ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; -#endif -} - -// Jm -void DebugInfo::drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - const char *wifiName = config.network.wifi_ssid; - - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, y, String("WiFi: Not Connected")); - if (config.display.heading_bold) - display->drawString(x + 1, y, String("WiFi: Not Connected")); - } else { - display->drawString(x, y, String("WiFi: Connected")); - if (config.display.heading_bold) - display->drawString(x + 1, y, String("WiFi: Connected")); - - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())), y, - "RSSI " + String(WiFi.RSSI())); - if (config.display.heading_bold) { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())) - 1, y, - "RSSI " + String(WiFi.RSSI())); - } - } - - display->setColor(WHITE); - - /* - - WL_CONNECTED: assigned when connected to a WiFi network; - - WL_NO_SSID_AVAIL: assigned when no SSID are available; - - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; - - WL_CONNECTION_LOST: assigned when the connection is lost; - - WL_DISCONNECTED: assigned when disconnected from a network; - - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of - attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); - - WL_SCAN_COMPLETED: assigned when the scan networks is completed; - - WL_NO_SHIELD: assigned when no WiFi shield is present; - - */ - if (WiFi.status() == WL_CONNECTED) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "IP: " + String(WiFi.localIP().toString().c_str())); - } else if (WiFi.status() == WL_NO_SSID_AVAIL) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "SSID Not Found"); - } else if (WiFi.status() == WL_CONNECTION_LOST) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Lost"); - } else if (WiFi.status() == WL_CONNECT_FAILED) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Failed"); - } else if (WiFi.status() == WL_IDLE_STATUS) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Idle ... Reconnecting"); - } -#ifdef ARCH_ESP32 - else { - // Codes: - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, - WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); - } -#else - else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Unkown status: " + String(WiFi.status())); - } -#endif - - display->drawString(x, y + FONT_HEIGHT_SMALL * 2, "SSID: " + String(wifiName)); - - display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); - - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ -#ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; -#endif -#endif -} - -void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - char batStr[20]; - if (powerStatus->getHasBattery()) { - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - - snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), - powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); - - // Line 1 - display->drawString(x, y, batStr); - if (config.display.heading_bold) - display->drawString(x + 1, y, batStr); - } else { - // Line 1 - display->drawString(x, y, String("USB")); - if (config.display.heading_bold) - display->drawString(x + 1, y, String("USB")); - } - - // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - // if (config.display.heading_bold) - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - - uint32_t currentMillis = millis(); - uint32_t seconds = currentMillis / 1000; - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - // currentMillis %= 1000; - // seconds %= 60; - // minutes %= 60; - // hours %= 24; - - // Show uptime as days, hours, minutes OR seconds - std::string uptime = screen->drawTimeDelta(days, hours, minutes, seconds); - - // Line 1 (Still) - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - if (config.display.heading_bold) - display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - - display->setColor(WHITE); - - // Setup string to assemble analogClock string - std::string analogClock = ""; - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - char timebuf[12]; - - if (config.display.use_12h_clock) { - std::string meridiem = "am"; - if (hour >= 12) { - if (hour > 12) - hour -= 12; - meridiem = "pm"; - } - if (hour == 00) { - hour = 12; - } - snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); - } else { - snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); - } - analogClock += timebuf; - } - - // Line 2 - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); - - // Display Channel Utilization - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); - -#if HAS_GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - // Line 3 - if (config.display.gps_format != - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude - drawGPSAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - - // Line 4 - drawGPScoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); - } else { - drawGPSpowerstat(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - } -#endif -/* Display a heartbeat pixel that blinks every time the frame is redrawn */ -#ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; -#endif -} - int Screen::handleStatusUpdate(const meshtastic::Status *arg) { // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); @@ -2772,16 +1245,63 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) return 0; } +// Handles when message is received; will jump to text message frame. int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { - // Outgoing message - if (packet->from == 0) - setFrames(FOCUS_PRESERVE); // Return to same frame (quietly hiding the rx text message frame) + if (packet->from == 0) { + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + dismissedFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies - // Incoming message - else - setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list + } else { + // Incoming message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + + // Only wake/force display if the configuration allows it + 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); + } + } } return 0; @@ -2809,20 +1329,25 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleInputEvent(const InputEvent *event) { + if (!screenOn) + return 0; -#if defined(DISPLAY_CLOCK_FRAME) - // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button - uint8_t watchFaceFrame = error_code ? 1 : 0; - - if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && - event->touchY >= 204 && event->touchY <= 240) { - screen->digitalWatchFace = !screen->digitalWatchFace; - - setFrames(); +#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + setFastFramerate(); // Draw ASAP +#endif + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + menuHandler::handleMenuSwitch(dispdev); return 0; } -#endif // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose @@ -2831,28 +1356,64 @@ int Screen::handleInputEvent(const InputEvent *event) // Ask any MeshModules if they're handling keyboard input right now bool inputIntercepted = false; for (MeshModule *module : moduleFrames) { - if (module->interceptingKeyboardInput()) + if (module && module->interceptingKeyboardInput()) inputIntercepted = true; } // If no modules are using the input, move between frames if (!inputIntercepted) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { showPrevFrame(); - else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { showNextFrame(); + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + menuHandler::homeBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + menuHandler::systemBaseMenu(); +#if HAS_GPS + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { + menuHandler::positionBaseMenu(); +#endif + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { + 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 (framesetInfo.positions.firstFavorite != 255 && + this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && + this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { + menuHandler::favoriteBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + menuHandler::nodeListMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { + menuHandler::wifiBaseMenu(); + } + } else if (event->inputEvent == INPUT_BROKER_BACK) { + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_CANCEL) { + setOn(false); + } } } return 0; } -int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) +int Screen::handleAdminMessage(AdminModule_ObserverData *arg) { - switch (arg->which_payload_variant) { + switch (arg->request->which_payload_variant) { // Node removed manually (i.e. via app) case meshtastic_AdminMessage_remove_by_nodenum_tag: setFrames(FOCUS_PRESERVE); + *arg->result = AdminMessageHandleResult::HANDLED; break; // Default no-op, in case the admin message observable gets used by other classes in future @@ -2862,7 +1423,33 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) return 0; } +bool Screen::isOverlayBannerShowing() +{ + return NotificationRenderer::isOverlayBannerShowing(); +} + } // namespace graphics + #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN + +bool shouldWakeOnReceivedMessage() +{ + /* + The goal here is to determine when we do NOT wake up the screen on message received: + - Any ext. notifications are turned on + - If role is not client / client_mute + - If the battery level is very low + */ + if (moduleConfig.external_notification.enabled) { + return false; + } + if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) { + return false; + } + if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { + return false; + } + return true; +} diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ce416156f..265900131 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -5,6 +5,28 @@ #include "detect/ScanI2C.h" #include "mesh/generated/meshtastic/config.pb.h" #include +#include +#include +#include + +#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) +namespace graphics +{ +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; + +struct BannerOverlayOptions { + const char *message; + uint32_t durationMs = 30000; + const char **optionsArrayPtr = nullptr; + const int *optionsEnumPtr = nullptr; + uint8_t optionsCount = 0; + std::function bannerCallback = nullptr; + int8_t InitialSelected = 0; + notificationTypeEnum notificationType = notificationTypeEnum::text_banner; +}; +} // namespace graphics + +bool shouldWakeOnReceivedMessage(); #if !HAS_SCREEN #include "power.h" @@ -14,11 +36,20 @@ namespace graphics class Screen { public: + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, + FOCUS_SYSTEM, + }; + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); void onPress() {} void setup() {} void setOn(bool) {} - void print(const char *) {} void doDeepSleep() {} void forceDisplay(bool forceUiUpdate = false) {} void startFirmwareUpdateScreen() {} @@ -27,6 +58,9 @@ class Screen void setFunctionSymbol(std::string) {} void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} + void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} + void showOverlayBanner(BannerOverlayOptions) {} + void setFrames(FrameFocus focus) {} void endAlert() {} }; } // namespace graphics @@ -60,10 +94,13 @@ class Screen #include "commands.h" #include "concurrency/LockGuard.h" #include "concurrency/OSThread.h" +#include "graphics/draw/MenuHandler.h" #include "input/InputBroker.h" #include "mesh/MeshModule.h" +#include "modules/AdminModule.h" #include "power.h" #include +#include // 0 to 255, though particular variants might define different defaults #ifndef BRIGHTNESS_DEFAULT @@ -90,7 +127,7 @@ class Screen /// Convert an integer GPS coords to a floating point #define DegD(i) (i * 1e-7) - +extern bool hasUnreadMessage; namespace { /// A basic 2D point class for drawing @@ -176,14 +213,31 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver inputObserver = CallbackObserver(this, &Screen::handleInputEvent); - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Screen::handleAdminMessage); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Screen::handleAdminMessage); public: + OLEDDisplay *getDisplayDevice() { return dispdev; } explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - + size_t frameCount = 0; // Total number of active frames ~Screen(); + // Which frame we want to be displayed, after we regen the frameset by calling setFrames + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, + FOCUS_SYSTEM, + }; + + // Regenerate the normal set of frames, focusing a specific frame if requested + // Call when a frame should be added / removed, or custom frames should be cleared + void setFrames(FrameFocus focus = FOCUS_DEFAULT); + + std::vector indicatorIcons; // Per-frame custom icon pointers Screen(const Screen &) = delete; Screen &operator=(const Screen &) = delete; @@ -191,6 +245,12 @@ class Screen : public concurrency::OSThread meshtastic_Config_DisplayConfig_OledType model; OLEDDISPLAY_GEOMETRY geometry; + bool isOverlayBannerShowing(); + + // Stores the last 4 of our hardware ID, to make finding the device for pairing easier + // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class + char ourId[5]; + /// Initializes the UI, turns on the display, starts showing boot screen. // // Not thread safe - must be called before any other methods are called. @@ -214,21 +274,9 @@ class Screen : public concurrency::OSThread void blink(); - void drawFrameText(OLEDDisplay *, OLEDDisplayUiState *, int16_t, int16_t, const char *); - - void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); - // Draw north - void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); - - static uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); - float estimatedHeading(double lat, double lon); - void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); - - void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); - /// Handle button press, trackball or swipe action) void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } @@ -260,6 +308,18 @@ class Screen : public concurrency::OSThread enqueueCmd(cmd); } + void showSimpleBanner(const char *message, uint32_t durationMs = 0); + void showOverlayBanner(BannerOverlayOptions); + + void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); + void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); + + void requestMenu(graphics::menuHandler::screenMenus menuToShow) + { + graphics::menuHandler::menuQueue = menuToShow; + runNow(); + } + void startFirmwareUpdateScreen() { ScreenCmd cmd; @@ -272,7 +332,7 @@ class Screen : public concurrency::OSThread void setHeading(long _heading) { hasCompass = true; - compassHeading = _heading; + compassHeading = fmod(_heading, 360); } bool hasHeading() { return hasCompass; } @@ -292,23 +352,12 @@ class Screen : public concurrency::OSThread /// Stops showing the boot screen. void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } - /// Writes a string to the screen. - void print(const char *text) + void runNow() { - ScreenCmd cmd; - cmd.cmd = Cmd::PRINT; - // TODO(girts): strdup() here is scary, but we can't use std::string as - // FreeRTOS queue is just dumbly copying memory contents. It would be - // nice if we had a queue that could copy objects by value. - cmd.print_text = strdup(text); - if (!enqueueCmd(cmd)) { - free(cmd.print_text); - } + setFastFramerate(); + enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP}); } - /// generates a very brief time delta display - std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); - /// Overrides the default utf8 character conversion, to replace empty space with question marks static char customFontTableLookup(const uint8_t ch) { @@ -533,7 +582,7 @@ class Screen : public concurrency::OSThread int handleTextMessage(const meshtastic_MeshPacket *arg); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); - int handleAdminMessage(const meshtastic_AdminMessage *arg); + int handleAdminMessage(AdminModule_ObserverData *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(bool forceUiUpdate = false); @@ -541,8 +590,6 @@ class Screen : public concurrency::OSThread /// Draws our SSL cert screen during boot (called from WebServer) void setSSLFrames(); - void setWelcomeFrames(); - // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) void dismissCurrentFrame(); @@ -591,7 +638,6 @@ class Screen : public concurrency::OSThread void handleOnPress(); void handleShowNextFrame(); void handleShowPrevFrame(); - void handlePrint(const char *text); void handleStartFirmwareUpdateScreen(); // Info collected by setFrames method. @@ -600,30 +646,37 @@ class Screen : public concurrency::OSThread // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo struct FramesetInfo { struct FramePositions { - uint8_t fault = 0; - uint8_t textMessage = 0; - uint8_t waypoint = 0; - uint8_t focusedModule = 0; - uint8_t log = 0; - uint8_t settings = 0; - uint8_t wifi = 0; + uint8_t fault = 255; + uint8_t waypoint = 255; + uint8_t focusedModule = 255; + uint8_t log = 255; + uint8_t settings = 255; + uint8_t wifi = 255; + uint8_t deviceFocused = 255; + uint8_t memory = 255; + uint8_t gps = 255; + uint8_t home = 255; + uint8_t textMessage = 255; + uint8_t nodelist = 255; + uint8_t nodelist_lastheard = 255; + uint8_t nodelist_hopsignal = 255; + uint8_t nodelist_distance = 255; + uint8_t nodelist_bearings = 255; + uint8_t clock = 255; + uint8_t firstFavorite = 255; + uint8_t lastFavorite = 255; + uint8_t lora = 255; } positions; uint8_t frameCount = 0; } framesetInfo; - // Which frame we want to be displayed, after we regen the frameset by calling setFrames - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_TEXTMESSAGE, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - }; - - // Regenerate the normal set of frames, focusing a specific frame if requested - // Call when a frame should be added / removed, or custom frames should be cleared - void setFrames(FrameFocus focus = FOCUS_DEFAULT); + struct DismissedFrames { + bool textMessage = false; + bool waypoint = false; + bool wifi = false; + bool memory = false; + } dismissedFrames; /// Try to start drawing ASAP void setFastFramerate(); @@ -631,34 +684,6 @@ class Screen : public concurrency::OSThread // Sets frame up for immediate drawing void setFrameImmediateDraw(FrameCallback *drawFrames); - /// Called when debug screen is to be drawn, calls through to debugInfo.drawFrame. - static void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - -#if defined(DISPLAY_CLOCK_FRAME) - static void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); - - static void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); - - static void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); - - static void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); - - static void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); - - static void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); - - // Whether we are showing the digital watch face or the analog one - bool digitalWatchFace = true; -#endif - /// callback for current alert frame FrameCallback alertFrame; @@ -691,4 +716,9 @@ class Screen : public concurrency::OSThread } // namespace graphics +// Extern declarations for function symbols used in UIRenderer +extern std::vector functionSymbol; +extern std::string functionSymbolString; +extern graphics::Screen *screen; + #endif \ No newline at end of file diff --git a/src/graphics/ScreenGlobals.cpp b/src/graphics/ScreenGlobals.cpp new file mode 100644 index 000000000..bc139faaf --- /dev/null +++ b/src/graphics/ScreenGlobals.cpp @@ -0,0 +1,6 @@ +#include +#include + +// Global variables for screen function overlay +std::vector functionSymbol; +std::string functionSymbolString; diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp new file mode 100644 index 000000000..7cd876ac5 --- /dev/null +++ b/src/graphics/SharedUIDisplay.cpp @@ -0,0 +1,394 @@ +#include "graphics/SharedUIDisplay.h" +#include "RTC.h" +#include "graphics/ScreenFonts.h" +#include "main.h" +#include "meshtastic/config.pb.h" +#include "power.h" +#include +#include + +namespace graphics +{ + +void determineResolution(int16_t screenheight, int16_t screenwidth) +{ + if (screenwidth > 128) { + isHighResolution = true; + } + + // Special case for Heltec Wireless Tracker v1.1 + if (screenwidth == 160 && screenheight == 80) { + isHighResolution = false; + } +} + +// === Shared External State === +bool hasUnreadMessage = false; +bool isMuted = false; +bool isHighResolution = false; + +// === Internal State === +bool isBoltVisibleShared = true; +uint32_t lastBlinkShared = 0; +bool isMailIconVisible = true; +uint32_t lastMailBlink = 0; + +// ********************************* +// * Rounded Header when inverted * +// ********************************* +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) +{ + // Draw the center and side rectangles + display->fillRect(x + r, y, w - 2 * r, h); // center bar + display->fillRect(x, y + r, r, h - 2 * r); // left edge + display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge + + // Draw the rounded corners using filled circles + display->fillCircle(x + r + 1, y + r, r); // top-left + display->fillCircle(x + w - r - 1, y + r, r); // top-right + display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right +} + +// ************************* +// * Common Header Drawing * +// ************************* +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only) +{ + constexpr int HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const int xOffset = 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; + + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + if (!battery_only) { + // === Inverted Header Background === + if (isInverted) { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + if (isHighResolution) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } + } + + // === Screen Title === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(SCREEN_WIDTH / 2, y, titleStr); + if (config.display.heading_bold) { + display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + } + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Battery State === + int chargePercent = powerStatus->getBatteryChargePercent(); + bool isCharging = powerStatus->getIsCharging(); + bool usbPowered = powerStatus->getHasUSB(); + + if (chargePercent >= 100) { + isCharging = false; + } + if (chargePercent == 101) { + usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable + // plugged in + } + + uint32_t now = millis(); + +#ifndef USE_EINK + if (isCharging && now - lastBlinkShared > 500) { + isBoltVisibleShared = !isBoltVisibleShared; + lastBlinkShared = now; + } +#endif + + bool useHorizontalBattery = (isHighResolution && screenW >= screenH); + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + + int batteryX = 1; + int batteryY = HEADER_OFFSET_Y + 1; + + // === Battery Icons === + if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging + batteryX += 1; + batteryY += 2; + if (isHighResolution) { + display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); + batteryX += 20; // Icon + 1 pixel + } else { + display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); + batteryX += 11; // Icon + 1 pixel + } + } else { + if (useHorizontalBattery) { + batteryX += 1; + batteryY += 2; + display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); + display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); + else { + display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); + display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); + int fillWidth = 14 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); + } + batteryX += 18; // Icon + 2 pixels + } else { +#ifdef USE_EINK + batteryY += 2; +#endif + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); + else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + } + batteryX += 9; // Icon + 2 pixels + } + } + + if (chargePercent != 101) { + // === Battery % Display === + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); + display->drawString(batteryX, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); + if (isBold) { + display->drawString(batteryX + 1, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth, textY, "%"); + } + } + + // === Time and Right-aligned Icons === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char timeStr[10] = "--:--"; // Fallback display + int timeStrWidth = display->getStringWidth("12:34"); // Default alignment + int timeX = screenW - xOffset - timeStrWidth + 4; + + if (rtc_sec > 0 && !battery_only) { + // === Build Time String === + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + hour %= 12; + if (hour == 0) + hour = 12; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + } + + timeStrWidth = display->getStringWidth(timeStr); + timeX = screenW - xOffset - timeStrWidth + 3; + + // === Show Mail or Mute Icon to the Left of Time === + int iconRightEdge = timeX - 1; + + bool showMail = false; + +#ifndef USE_EINK + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(WHITE); + } + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - (mail_width - 2); + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + if (isHighResolution) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + + } else { + // === No Time Available: Mail/Mute Icon Moves to Far Right === + int iconRightEdge = screenW - xOffset; + + bool showMail = false; + +#ifndef USE_EINK + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + if (isHighResolution) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + } + + display->setColor(WHITE); // Reset for other UI +} + +const int *getTextPositions(OLEDDisplay *display) +{ + static int textPositions[7]; // Static array that persists beyond function scope + + if (isHighResolution) { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine_medium; + textPositions[2] = textSecondLine_medium; + textPositions[3] = textThirdLine_medium; + textPositions[4] = textFourthLine_medium; + textPositions[5] = textFifthLine_medium; + textPositions[6] = textSixthLine_medium; + } else { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine; + textPositions[2] = textSecondLine; + textPositions[3] = textThirdLine; + textPositions[4] = textFourthLine; + textPositions[5] = textFifthLine; + textPositions[6] = textSixthLine; + } + return textPositions; +} + +bool isAllowedPunctuation(char c) +{ + const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; + return allowed.find(c) != std::string::npos; +} + +std::string sanitizeString(const std::string &input) +{ + std::string output; + bool inReplacement = false; + + for (char c : input) { + if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { + output += c; + inReplacement = false; + } else { + if (!inReplacement) { + output += 0xbf; // ISO-8859-1 for inverted question mark + inReplacement = true; + } + } + } + + return output; +} + +} // namespace graphics diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h new file mode 100644 index 000000000..b8d82795e --- /dev/null +++ b/src/graphics/SharedUIDisplay.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +namespace graphics +{ + +// ======================= +// Shared UI Helpers +// ======================= + +#define textZeroLine 0 +// Consistent Line Spacing - this is standard for all display and the fall-back spacing +#define textFirstLine (FONT_HEIGHT_SMALL - 1) +#define textSecondLine (textFirstLine + (FONT_HEIGHT_SMALL - 5)) +#define textThirdLine (textSecondLine + (FONT_HEIGHT_SMALL - 5)) +#define textFourthLine (textThirdLine + (FONT_HEIGHT_SMALL - 5)) +#define textFifthLine (textFourthLine + (FONT_HEIGHT_SMALL - 5)) +#define textSixthLine (textFifthLine + (FONT_HEIGHT_SMALL - 5)) + +// Consistent Line Spacing for devices like T114 and TEcho/ThinkNode M1 of devices +#define textFirstLine_medium (FONT_HEIGHT_SMALL + 1) +#define textSecondLine_medium (textFirstLine_medium + FONT_HEIGHT_SMALL) +#define textThirdLine_medium (textSecondLine_medium + FONT_HEIGHT_SMALL) +#define textFourthLine_medium (textThirdLine_medium + FONT_HEIGHT_SMALL) +#define textFifthLine_medium (textFourthLine_medium + FONT_HEIGHT_SMALL) +#define textSixthLine_medium (textFifthLine_medium + FONT_HEIGHT_SMALL) + +// Consistent Line Spacing for devices like VisionMaster T190 +#define textFirstLine_large (FONT_HEIGHT_SMALL + 1) +#define textSecondLine_large (textFirstLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textThirdLine_large (textSecondLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textFourthLine_large (textThirdLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textFifthLine_large (textFourthLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textSixthLine_large (textFifthLine_large + (FONT_HEIGHT_SMALL + 5)) + +// Quick screen access +#define SCREEN_WIDTH display->getWidth() +#define SCREEN_HEIGHT display->getHeight() + +// Shared state (declare inside namespace) +extern bool hasUnreadMessage; +extern bool isMuted; +extern bool isHighResolution; +void determineResolution(int16_t screenheight, int16_t screenwidth); + +// Rounded highlight (used for inverted headers) +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); + +// Shared battery/time/mail header +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false); + +const int *getTextPositions(OLEDDisplay *display); + +bool isAllowedPunctuation(char c); + +std::string sanitizeString(const std::string &input); + +} // namespace graphics diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 14787baff..3e9bafc6c 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1,5 +1,6 @@ #include "configuration.h" #include "main.h" + #if ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -14,8 +15,10 @@ extern SX1509 gpioExtender; #endif -#ifndef TFT_MESH -#define TFT_MESH COLOR565(0x67, 0xEA, 0x94) +#ifdef TFT_MESH_OVERRIDE +uint16_t TFT_MESH = TFT_MESH_OVERRIDE; +#else +uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif #if defined(ST7735S) @@ -467,18 +470,27 @@ class LGFX : public lgfx::LGFX_Device // The following setting values โ€‹โ€‹are general initial values โ€‹โ€‹for each panel, so please comment out any // unknown items and try them. - - 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) +#if defined(T_WATCH_S3) + cfg.panel_width = 240; + cfg.panel_height = 240; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; // No vertical shift needed โ€” panel is top-aligned + cfg.offset_rotation = 2; // Rotate 180ยฐ to correct upside-down layout +#else + 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) +#endif #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 = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // 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 @@ -653,7 +665,7 @@ static LGFX *tft = nullptr; #include // Graphics and font library for ILI9342 driver chip static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h -#elif ARCH_PORTDUINO && HAS_SCREEN != 0 && !HAS_TFT +#elif ARCH_PORTDUINO #include // Graphics and font library for ST7735 driver chip class LGFX : public lgfx::LGFX_Device @@ -697,11 +709,16 @@ class LGFX : public lgfx::LGFX_Device _panel_instance->setBus(&_bus_instance); // set the bus on the panel. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]); + LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]); cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = settingsMap[displayReset]; - cfg.panel_width = settingsMap[displayWidth]; // actual displayable width - cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + if (settingsMap[displayRotate]) { + cfg.panel_width = settingsMap[displayHeight]; // actual displayable width + cfg.panel_height = settingsMap[displayWidth]; // actual displayable height + } else { + cfg.panel_width = settingsMap[displayWidth]; // actual displayable width + cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + } cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) @@ -978,9 +995,9 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g #if ARCH_PORTDUINO if (settingsMap[displayRotate]) { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); - } else { setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + } else { + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); } #elif defined(SCREEN_ROTATE) @@ -1169,6 +1186,8 @@ bool TFTDisplay::connect() tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) tft->setRotation(2); // T-Watch S3 left-handed orientation +#elif ARCH_PORTDUINO + tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif diff --git a/src/graphics/TimeFormatters.cpp b/src/graphics/TimeFormatters.cpp new file mode 100644 index 000000000..47036078b --- /dev/null +++ b/src/graphics/TimeFormatters.cpp @@ -0,0 +1,103 @@ +#include "TimeFormatters.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "mesh/NodeDB.h" +#include + +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) +{ + // Cache the result - avoid frequent recalculation + static uint8_t hoursCached = 0, minutesCached = 0; + static uint32_t daysAgoCached = 0; + static uint32_t secondsAgoCached = 0; + static bool validCached = false; + + // Abort: if timezone not set + if (strlen(config.device.tzdef) == 0) { + validCached = false; + return validCached; + } + + // Abort: if invalid pointers passed + if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { + validCached = false; + return validCached; + } + + // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) + if (secondsAgo > SEC_PER_DAY * 30UL * 6) { + validCached = false; + return validCached; + } + + // If repeated request, don't bother recalculating + if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { + if (validCached) { + *hours = hoursCached; + *minutes = minutesCached; + *daysAgo = daysAgoCached; + } + return validCached; + } + + // Get local time + uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time + + // Abort: if RTC not set + if (!secondsRTC) { + validCached = false; + return validCached; + } + + // Get absolute time when last seen + uint32_t secondsSeenAt = secondsRTC - secondsAgo; + + // Calculate daysAgo + *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed + + // Get seconds since midnight + uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into hours and minutes + *hours = hms / SEC_PER_HOUR; + *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Cache the result + daysAgoCached = *daysAgo; + hoursCached = *hours; + minutesCached = *minutes; + secondsAgoCached = secondsAgo; + + validCached = true; + return validCached; +} + +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) +{ + // Use an absolute timestamp in some cases. + // Particularly useful with E-Ink displays. Static UI, fewer refreshes. + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); + + if (agoSecs < 120) // last 2 mins? + snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); + // -- if suitable for timestamp -- + else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); + else if (useTimestamp && daysAgo == 0) // Today + snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); + else if (useTimestamp && daysAgo == 1) // Yesterday + snprintf(timeStr, maxLength, "Seen yesterday"); + else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) + snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); + // -- if using time delta instead -- + else if (agoSecs < 120 * 60) // last 2 hrs + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); + // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. + else if ((agoSecs / 60 / 60) < (730 * 6)) + snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); + else + snprintf(timeStr, maxLength, "unknown age"); +} diff --git a/src/graphics/TimeFormatters.h b/src/graphics/TimeFormatters.h new file mode 100644 index 000000000..b3d8413a2 --- /dev/null +++ b/src/graphics/TimeFormatters.h @@ -0,0 +1,26 @@ +#pragma once + +#include "configuration.h" +#include "gps/RTC.h" +#include +#include + +/** + * Convert a delta in seconds ago to timestamp information (hours, minutes, days ago). + * + * @param secondsAgo Number of seconds ago to convert + * @param hours Pointer to store the hours (0-23) + * @param minutes Pointer to store the minutes (0-59) + * @param daysAgo Pointer to store the number of days ago + * @return true if conversion was successful, false if invalid input or time not available + */ +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo); + +/** + * Get a human-readable string representing the time ago in a format like "2 days, 3 hours, 15 minutes". + * + * @param agoSecs Number of seconds ago to convert + * @param timeStr Pointer to store the resulting string + * @param maxLength Maximum length of the resulting string buffer + */ +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp new file mode 100644 index 000000000..8d7e91000 --- /dev/null +++ b/src/graphics/draw/ClockRenderer.cpp @@ -0,0 +1,516 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "ClockRenderer.h" +#include "NodeDB.h" +#include "UIRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "graphics/images.h" +#include "main.h" + +#if !MESHTASTIC_EXCLUDE_BLUETOOTH +#include "nimble/NimbleBluetooth.h" +#endif + +namespace graphics +{ + +namespace ClockRenderer +{ + +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) +{ + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; + + uint16_t topAndBottomX = x + (4 * scale); + + uint16_t quarterCellHeight = cellHeight / 4; + + uint16_t topY = y + quarterCellHeight; + uint16_t bottomY = y + (quarterCellHeight * 3); + + display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); +} + +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) +{ + // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of + // segment {innerIndex + 1} + // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. + uint8_t numbers[10][7] = { + {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key + {0, 1, 1, 0, 0, 0, 0}, // 1 1 + {1, 1, 0, 1, 1, 0, 1}, // 2 ___ + {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 + {0, 1, 1, 0, 0, 1, 1}, // 4 |_7ฬฒ_| + {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 + {1, 0, 1, 1, 1, 1, 1}, // 6 |___| + {1, 1, 1, 0, 0, 1, 0}, // 7 + {1, 1, 1, 1, 1, 1, 1}, // 8 4 + {1, 1, 1, 1, 0, 1, 1}, // 9 + }; + + // the width and height of each segment's central rectangle: + // _____________________ + // โ‹ฐ| (only this part, |โ‹ฑ + // โ‹ฐ | not including | โ‹ฑ + // โ‹ฑ | the triangles | โ‹ฐ + // โ‹ฑ| on the ends) |โ‹ฐ + // โ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พ + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // segment x and y coordinates + uint16_t segmentOneX = x + segmentHeight + 2; + uint16_t segmentOneY = y; + + uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; + uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; + + uint16_t segmentThreeX = segmentTwoX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; + + uint16_t segmentFourX = segmentOneX; + uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; + + uint16_t segmentFiveX = x; + uint16_t segmentFiveY = segmentThreeY; + + uint16_t segmentSixX = x; + uint16_t segmentSixY = segmentTwoY; + + uint16_t segmentSevenX = segmentOneX; + uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; + + if (numbers[number][0]) { + graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + } + + if (numbers[number][1]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + } + + if (numbers[number][2]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + } + + if (numbers[number][3]) { + graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + } + + if (numbers[number][4]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + } + + if (numbers[number][5]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + } + + if (numbers[number][6]) { + graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); + } +} + +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) +{ + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, width, height); + + // draw end triangles + display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); + + display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); +} + +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) +{ + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, height, width); + + // draw end triangles + display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); + + display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); +} + +/* +void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) +{ + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + if (digitalMode) { + uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; + uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); + uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); + + display->drawCircle(centerX, centerY, radius); + display->drawCircle(centerX, centerY, radius + 1); + display->drawLine(centerX, centerY, centerX, centerY - radius + 3); + display->drawLine(centerX, centerY, centerX + radius - 3, centerY); + } else { + uint16_t segmentOneX = x + segmentHeight + 2; + uint16_t segmentOneY = y; + + uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; + uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; + + uint16_t segmentThreeX = segmentOneX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; + + uint16_t segmentFourX = x; + uint16_t segmentFourY = y + segmentHeight + 2; + + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + } +} +*/ +// Draw a digital clock +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true); + +#ifdef T_WATCH_S3 + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14); + } +#endif + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + char timeString[16]; + int hour = 0; + int minute = 0; + int second = 0; + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + } + + bool isPM = hour >= 12; + // hour = hour > 12 ? hour - 12 : hour; + if (config.display.use_12h_clock) { + 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); + } + + // Format seconds string + char secondString[8]; + snprintf(secondString, sizeof(secondString), "%02d", second); + +#ifdef T_WATCH_S3 + float scale = 1.5; +#else + float scale = 0.75; + if (isHighResolution) { + scale = 1.5; + } +#endif + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // calculate hours:minutes string width + uint16_t timeStringWidth = strlen(timeString) * 5; + + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; + + if (character == ':') { + timeStringWidth += segmentHeight; + } else { + timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; + } + } + + uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); + + uint16_t startingHourMinuteTextX = hourMinuteTextX; + + uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); + + // iterate over characters in hours:minutes string and draw segmented characters + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; + + if (character == ':') { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); + + hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; + } + + hourMinuteTextX += 5; + } + + // draw seconds string + display->setFont(FONT_SMALL); + int xOffset = (isHighResolution) ? 0 : -1; + if (hour >= 10) { + xOffset += (isHighResolution) ? 32 : 18; + } + int yOffset = (isHighResolution) ? 3 : 1; +#ifdef SENSECAP_INDICATOR + yOffset -= 3; +#endif + if (config.display.use_12h_clock) { + display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, + isPM ? "pm" : "am"); + } +#ifndef USE_EINK + xOffset = (isHighResolution) ? 18 : 10; + display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, + secondString); +#endif +} + +void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) +{ + display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon); +} + +// Draw an analog clock +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true); + +#ifdef T_WATCH_S3 + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14); + } +#endif + // clock face center coordinates + int16_t centerX = display->getWidth() / 2; + int16_t centerY = display->getHeight() / 2; + + // clock face radius + int16_t radius = 0; + if (display->getHeight() < display->getWidth()) { + radius = (display->getHeight() / 2) * 0.9; + } else { + radius = (display->getWidth() / 2) * 0.9; + } +#ifdef T_WATCH_S3 + radius = (display->getWidth() / 2) * 0.8; +#endif + + // noon (0 deg) coordinates (outermost circle) + int16_t noonX = centerX; + int16_t noonY = centerY - radius; + + // second hand radius and y coordinate (outermost circle) + int16_t secondHandNoonY = noonY + 1; + + // tick mark outer y coordinate; (first nested circle) + int16_t tickMarkOuterNoonY = secondHandNoonY; + + // seconds tick mark inner y coordinate; (second nested circle) + double secondsTickMarkInnerNoonY = (double)noonY + 4; + if (isHighResolution) { + secondsTickMarkInnerNoonY = (double)noonY + 8; + } + + // hours tick mark inner y coordinate; (third nested circle) + double hoursTickMarkInnerNoonY = (double)noonY + 6; + if (isHighResolution) { + hoursTickMarkInnerNoonY = (double)noonY + 16; + } + + // minute hand y coordinate + int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; + + // hour string y coordinate + int16_t hourStringNoonY = minuteHandNoonY + 18; + + // hour hand radius and y coordinate + int16_t hourHandRadius = radius * 0.35; + if (isHighResolution) { + int16_t hourHandRadius = radius * 0.55; + } + int16_t hourHandNoonY = centerY - hourHandRadius; + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + display->drawCircle(centerX, centerY, radius); + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + bool isPM = hour >= 12; + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + display->setFont(FONT_SMALL); + int yOffset = isHighResolution ? 1 : 0; +#ifdef USE_EINK + yOffset += 3; +#endif + display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, + isPM ? "pm" : "am"); + } + hour %= 12; + if (hour == 0) + hour = 12; + + int16_t degreesPerHour = 30; + int16_t degreesPerMinuteOrSecond = 6; + + double hourBaseAngle = hour * degreesPerHour; + double hourAngleOffset = ((double)minute / 60) * degreesPerHour; + double hourAngle = radians(hourBaseAngle + hourAngleOffset); + + double minuteBaseAngle = minute * degreesPerMinuteOrSecond; + double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; + double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); + + double secondAngle = radians(second * degreesPerMinuteOrSecond); + + double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; + double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; + + double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; + double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; + + double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; + double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; + + display->setFont(FONT_MEDIUM); + + // draw minute and hour tick marks and hour numbers + for (uint16_t angle = 0; angle < 360; angle += 6) { + double angleInRadians = radians(angle); + + double sineAngleInRadians = sin(-angleInRadians); + double cosineAngleInRadians = cos(-angleInRadians); + + double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; + double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; + + if (angle % degreesPerHour == 0) { + double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; + + // draw hour tick mark + display->drawLine(startX, startY, endX, endY); + + static char buffer[2]; + + uint8_t hourInt = (angle / 30); + + if (hourInt == 0) { + hourInt = 12; + } + + // hour number x offset needs to be adjusted for some cases + int8_t hourStringXOffset; + int8_t hourStringYOffset = 13; + + switch (hourInt) { + case 3: + hourStringXOffset = 5; + break; + case 9: + hourStringXOffset = 7; + break; + case 10: + case 11: + hourStringXOffset = 8; + break; + case 12: + hourStringXOffset = 13; + break; + default: + hourStringXOffset = 6; + break; + } + + double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; + double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; + +#ifdef T_WATCH_S3 + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); +#else +#ifdef USE_EINK + if (isHighResolution) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } +#else + if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } +#endif +#endif + } + + if (angle % degreesPerMinuteOrSecond == 0) { + double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; + + if (isHighResolution) { + // draw minute tick mark + display->drawLine(startX, startY, endX, endY); + } + } + } + + // draw hour hand + display->drawLine(centerX, centerY, hourX, hourY); + + // draw minute hand + display->drawLine(centerX, centerY, minuteX, minuteY); +#ifndef USE_EINK + // draw second hand + display->drawLine(centerX, centerY, secondX, secondY); +#endif + } +} + +} // namespace ClockRenderer + +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h new file mode 100644 index 000000000..c8ba62868 --- /dev/null +++ b/src/graphics/draw/ClockRenderer.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +namespace ClockRenderer +{ + +// Clock frame functions +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Segmented display functions +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); + +// UI elements for clock displays +// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); +void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); + +} // namespace ClockRenderer + +} // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp new file mode 100644 index 000000000..0e5a1d727 --- /dev/null +++ b/src/graphics/draw/CompassRenderer.cpp @@ -0,0 +1,137 @@ +#include "CompassRenderer.h" +#include "NodeDB.h" +#include "UIRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include + +namespace graphics +{ +namespace CompassRenderer +{ + +// Point helper class for compass calculations +struct Point { + float x, y; + Point(float x, float y) : x(x), y(y) {} + + void rotate(float angle) + { + float cos_a = cos(angle); + float sin_a = sin(angle); + float new_x = x * cos_a - y * sin_a; + float new_y = x * sin_a + y * cos_a; + x = new_x; + y = new_y; + } + + void scale(float factor) + { + x *= factor; + y *= factor; + } + + void translate(float dx, float dy) + { + x += dx; + y += dy; + } +}; + +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) +{ + // Show the compass heading (not implemented in original) + // This could draw a "N" indicator or north arrow + // For now, we'll draw a simple north indicator + // const float radius = 17.0f; + if (isHighResolution) { + radius += 4; + } + Point north(0, -radius); + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + north.rotate(-myHeading); + north.translate(compassX, compassY); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setColor(BLACK); + if (isHighResolution) { + display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + } else { + display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + } + display->setColor(WHITE); + display->drawString(north.x, north.y - 3, "N"); +} + +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) +{ + Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially + float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; + Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); + + Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; + + for (int i = 0; i < 4; i++) { + arrowPoints[i]->rotate(headingRadian); + arrowPoints[i]->scale(compassDiam * 0.6); + arrowPoints[i]->translate(compassX, compassY); + } + +#ifdef USE_EINK + display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); +#else + display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); +#endif + display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); +} + +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) +{ + float radians = bearing * DEG_TO_RAD; + + Point tip(0, -size / 2); + Point left(-size / 6, size / 4); + Point right(size / 6, size / 4); + Point tail(0, size / 4.5); + + tip.rotate(radians); + left.rotate(radians); + right.rotate(radians); + tail.rotate(radians); + + tip.translate(x, y); + left.translate(x, y); + right.translate(x, y); + tail.translate(x, y); + + display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); +} + +float estimatedHeading(double lat, double lon) +{ + // Simple magnetic declination estimation + // This is a very basic implementation - the original might be more sophisticated + return 0.0f; // Return 0 for now, indicating no heading available +} + +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) +{ + // Calculate appropriate compass diameter based on display size + uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; + uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension + + // Ensure minimum and maximum bounds + if (maxDiam < 16) + maxDiam = 16; + if (maxDiam > 64) + maxDiam = 64; + + return maxDiam; +} + +} // namespace CompassRenderer +} // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h new file mode 100644 index 000000000..ca7532b66 --- /dev/null +++ b/src/graphics/draw/CompassRenderer.h @@ -0,0 +1,33 @@ +#pragma once + +#include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Compass and navigation drawing functions + * + * Contains all functions related to drawing compass elements, headings, + * navigation arrows, and location-based UI components. + */ +namespace CompassRenderer +{ +// Compass drawing functions +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius); +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); + +// Navigation and location functions +float estimatedHeading(double lat, double lon); +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); + +} // namespace CompassRenderer + +} // namespace graphics diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp new file mode 100644 index 000000000..b1a901f99 --- /dev/null +++ b/src/graphics/draw/DebugRenderer.cpp @@ -0,0 +1,622 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "../Screen.h" +#include "DebugRenderer.h" +#include "FSCommon.h" +#include "NodeDB.h" +#include "Throttle.h" +#include "UIRenderer.h" +#include "airtime.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include "mesh/Channels.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" +#include "sleep.h" + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#include "mesh/wifi/WiFiAPClient.h" +#include +#ifdef ARCH_ESP32 +#include "mesh/wifi/WiFiAPClient.h" +#endif +#endif + +#ifdef ARCH_ESP32 +#include "modules/StoreForwardModule.h" +#endif +#include +#include +#include + +using namespace meshtastic; + +// External variables +extern graphics::Screen *screen; +extern PowerStatus *powerStatus; +extern NodeStatus *nodeStatus; +extern GPSStatus *gpsStatus; +extern Channels channels; +extern AirTime *airTime; + +// External functions from Screen.cpp +extern bool heartbeat; + +#ifdef ARCH_ESP32 +extern StoreForwardModule *storeForwardModule; +#endif + +namespace graphics +{ +namespace DebugRenderer +{ + +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + char channelStr[20]; + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + // Display nodes status + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); + } else { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); + } +#if HAS_GPS + // Display GPS status + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); + } else { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + } else { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); + } + } +#endif + display->setColor(WHITE); + // Draw the channel name + display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); + // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo + if (moduleConfig.store_forward.enabled) { +#ifdef ARCH_ESP32 + 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(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, + 8, imgQuestion); +#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(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, + 8, imgSF); +#endif + } +#endif + } 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(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, + imgInfo); +#endif + } + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); + + // Draw any log messages + display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +// **************************** +// * WiFi Screen * +// **************************** +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = "WiFi"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + const char *wifiName = config.network.wifi_ssid; + + if (WiFi.status() != WL_CONNECTED) { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); + } else { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); + + char rssiStr[32]; + snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); + display->drawString(x, getTextPositions(display)[line++], rssiStr); + } + + /* + - WL_CONNECTED: assigned when connected to a WiFi network; + - WL_NO_SSID_AVAIL: assigned when no SSID are available; + - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; + - WL_CONNECTION_LOST: assigned when the connection is lost; + - WL_DISCONNECTED: assigned when disconnected from a network; + - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of + attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); + - WL_SCAN_COMPLETED: assigned when the scan networks is completed; + - WL_NO_SHIELD: assigned when no WiFi shield is present; + + */ + if (WiFi.status() == WL_CONNECTED) { + char ipStr[64]; + snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); + display->drawString(x, getTextPositions(display)[line++], ipStr); + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); + } +#ifdef ARCH_ESP32 + else { + // Codes: + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + display->drawString(x, getTextPositions(display)[line++], + WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); + } +#else + else { + char statusStr[32]; + snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); + display->drawString(x, getTextPositions(display)[line++], statusStr); + } +#endif + + char ssidStr[64]; + snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); + display->drawString(x, getTextPositions(display)[line++], ssidStr); + + display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +#endif +} + +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + + snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), + powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); + + // Line 1 + display->drawString(x, y, batStr); + if (config.display.heading_bold) + display->drawString(x + 1, y, batStr); + } else { + // Line 1 + display->drawString(x, y, "USB"); + if (config.display.heading_bold) + display->drawString(x + 1, y, "USB"); + } + + // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); + + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); + // if (config.display.heading_bold) + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); + + uint32_t currentMillis = millis(); + uint32_t seconds = currentMillis / 1000; + uint32_t minutes = seconds / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + // currentMillis %= 1000; + // seconds %= 60; + // minutes %= 60; + // hours %= 24; + + // Show uptime as days, hours, minutes OR seconds + std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); + + // Line 1 (Still) + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + + display->setColor(WHITE); + + // Setup string to assemble analogClock string + std::string analogClock = ""; + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + char timebuf[12]; + + if (config.display.use_12h_clock) { + std::string meridiem = "am"; + if (hour >= 12) { + if (hour > 12) + hour -= 12; + meridiem = "pm"; + } + if (hour == 00) { + hour = 12; + } + snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); + } else { + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); + } + analogClock += timebuf; + } + + // Line 2 + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); + + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); + +#if HAS_GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // Line 3 + if (config.display.gps_format != + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + + // Line 4 + UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); + } else { + UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + } +#endif +/* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +// Trampoline functions for DebugInfo class access +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrame(display, state, x, y); +} + +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameSettings(display, state, x, y); +} + +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameWiFi(display, state, x, y); +} + +// **************************** +// * LoRa Focused Screen * +// **************************** +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === First Row: Region / BLE Name === + graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); + + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); + int textWidth = display->getStringWidth(shortnameble); + int nameX = (SCREEN_WIDTH - textWidth); + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + + // === Second Row: Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + if (region != nullptr) { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + } + textWidth = display->getStringWidth(regionradiopreset); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); + + // === Third Row: Frequency / ChanNum === + char frequencyslot[35]; + char freqStr[16]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqStr, sizeof(freqStr), "%.3f", freq); + if (config.lora.channel_num == 0) { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); + } else { + 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) { + frequencyslot[len - 4] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(frequencyslot); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); + + // === Fourth Row: Channel Utilization === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (isHighResolution) ? 100 : 50; + int chutil_bar_height = (isHighResolution) ? 12 : 7; + int extraoffset = (isHighResolution) ? 6 : 3; + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + + display->drawString(starting_position, getTextPositions(display)[line++], chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0โ€“25% + float weight2 = 0.35; // Weight for 25โ€“40% + float weight3 = 0.20; // Weight for 40โ€“100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], + chUtilPercentage); +} + +// **************************** +// * Memory Screen * +// **************************** +void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Set Title + const char *titleStr = "System"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Layout === + int line = 1; + const int barHeight = 6; + const int labelX = x; + int barsOffset = (isHighResolution) ? 24 : 0; +#ifdef USE_EINK + barsOffset -= 12; +#endif + const int barX = x + 40 + barsOffset; + + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { + if (total == 0) + return; + + int percent = (used * 100) / total; + + char combinedStr[24]; + if (isHighResolution) { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, + total / 1024); + } else { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); + } + + int textWidth = display->getStringWidth(combinedStr); + int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; + if (adjustedBarWidth < 10) + adjustedBarWidth = 10; + + int fillWidth = (used * adjustedBarWidth) / total; + + // Label + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(labelX, getTextPositions(display)[line], label); + + // Bar + int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; + display->setColor(WHITE); + display->drawRect(barX, barY, adjustedBarWidth, barHeight); + + display->fillRect(barX, barY, fillWidth, barHeight); + display->setColor(WHITE); + + // Value string + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); + }; + + // === Memory values === + uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); + uint32_t heapTotal = memGet.getHeapSize(); + + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); + + uint32_t flashUsed = 0, flashTotal = 0; +#ifdef ESP32 + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); +#endif + + uint32_t sdUsed = 0, sdTotal = 0; + bool hasSD = false; + /* + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif + */ + // === Draw memory rows + drawUsageRow("Heap:", heapUsed, heapTotal, true); +#ifdef ESP32 + if (psramUsed > 0) { + line += 1; + drawUsageRow("PSRAM:", psramUsed, psramTotal); + } + if (flashTotal > 0) { + line += 1; + drawUsageRow("Flash:", flashUsed, flashTotal); + } +#endif + if (hasSD && sdTotal > 0) { + line += 1; + drawUsageRow("SD:", sdUsed, sdTotal); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + // System Uptime + if (line < 2) { + line += 1; + } + line += 1; + char appversionstr[35]; + snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION)); + int textWidth = display->getStringWidth(appversionstr); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line], appversionstr); + + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it + line += 1; + char uptimeStr[32] = ""; + uint32_t uptime = millis() / 1000; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + textWidth = display->getStringWidth(uptimeStr); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line], uptimeStr); + } +} +} // namespace DebugRenderer +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h new file mode 100644 index 000000000..f4d484f58 --- /dev/null +++ b/src/graphics/draw/DebugRenderer.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; +class DebugInfo; + +/** + * @brief Debug and diagnostic drawing functions + * + * Contains all functions related to drawing debug information, + * WiFi status, settings screens, and diagnostic data. + */ +namespace DebugRenderer +{ +// Debug frame functions +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Trampoline functions for framework callback compatibility +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// LoRa information display +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Memory screen display +void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +} // namespace DebugRenderer + +} // namespace graphics diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h new file mode 100644 index 000000000..6f1929ebd --- /dev/null +++ b/src/graphics/draw/DrawRenderers.h @@ -0,0 +1,38 @@ +#pragma once + +/** + * @brief Master include file for all Screen draw renderers + * + * This file includes all the individual renderer headers to provide + * a convenient single include for accessing all draw functions. + */ + +#include "graphics/draw/ClockRenderer.h" +#include "graphics/draw/CompassRenderer.h" +#include "graphics/draw/DebugRenderer.h" +#include "graphics/draw/NodeListRenderer.h" +#include "graphics/draw/ScreenRenderer.h" +#include "graphics/draw/UIRenderer.h" + +namespace graphics +{ + +/** + * @brief Collection of all draw renderers + * + * This namespace provides access to all the specialized rendering + * functions organized by category. + */ +namespace DrawRenderers +{ +// Re-export all renderer namespaces for convenience +using namespace ClockRenderer; +using namespace CompassRenderer; +using namespace DebugRenderer; +using namespace NodeListRenderer; +using namespace ScreenRenderer; +using namespace UIRenderer; + +} // namespace DrawRenderers + +} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp new file mode 100644 index 000000000..f6b250ebc --- /dev/null +++ b/src/graphics/draw/MenuHandler.cpp @@ -0,0 +1,1145 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "ClockRenderer.h" +#include "GPS.h" +#include "MenuHandler.h" +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "buzz.h" +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/draw/UIRenderer.h" +#include "main.h" +#include "modules/AdminModule.h" +#include "modules/CannedMessageModule.h" +#include "modules/KeyVerificationModule.h" + +extern uint16_t TFT_MESH; + +namespace graphics +{ +menuHandler::screenMenus menuHandler::menuQueue = menu_none; +bool test_enabled = false; +uint8_t test_count = 0; + +void menuHandler::LoraRegionPicker(uint32_t duration) +{ + static const char *optionsArray[] = {"Back", + "US", + "EU_433", + "EU_868", + "CN", + "JP", + "ANZ", + "KR", + "TW", + "RU", + "IN", + "NZ_865", + "TH", + "LORA_24", + "UA_433", + "UA_868", + "MY_433", + "MY_" + "919", + "SG_" + "923", + "PH_433", + "PH_868", + "PH_915", + "ANZ_433", + "KZ_433", + "KZ_863"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Set the LoRa region"; + bannerOptions.durationMs = duration; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 25; + bannerOptions.InitialSelected = 0; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { + config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + // This is needed as we wait til picking the LoRa region to generate keys for the first time. + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + // public key is derived from private, so this will always have the same result. + 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; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TwelveHourPicker() +{ + static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; + enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Time Format"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + } else if (selected == twelve) { + config.display.use_12h_clock = true; + } else { + config.display.use_12h_clock = false; + } + service->reloadConfig(SEGMENT_CONFIG); + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::ClockFacePicker() +{ + static const char *optionsArray[] = {"Back", "Digital", "Analog"}; + enum optionsNumbers { Back = 0, Digital = 1, Analog = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Which Face?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + } else if (selected == Digital) { + uiconfig.is_clockface_analog = false; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + } else { + uiconfig.is_clockface_analog = true; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + } + }; + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TZPicker() +{ + static const char *optionsArray[] = {"Back", + "US/Hawaii", + "US/Alaska", + "US/Pacific", + "US/Arizona", + "US/Mountain", + "US/Central", + "US/Eastern", + "UTC", + "EU/Western", + "EU/" + "Central", + "EU/Eastern", + "Asia/Kolkata", + "Asia/Hong_Kong", + "AU/AWST", + "AU/ACST", + "AU/AEST", + "Pacific/NZ"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Pick Timezone"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + } else if (selected == 1) { // Hawaii + strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); + } else if (selected == 2) { // Alaska + strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 3) { // Pacific + strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 4) { // Arizona + strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef)); + } else if (selected == 5) { // Mountain + strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 6) { // Central + 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 + strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); + } else if (selected == 10) { // 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 + strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); + } else if (selected == 12) { // Asia/Kolkata + strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); + } else if (selected == 13) { // China + strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); + } else if (selected == 14) { // AU/AWST + strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); + } else if (selected == 15) { // 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 + strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); + } else if (selected == 17) { // NZ + strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); + } + if (selected != 0) { + setenv("TZ", config.device.tzdef, 1); + service->reloadConfig(SEGMENT_CONFIG); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::clockMenu() +{ + static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; + enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Clock Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Clock) { + menuHandler::menuQueue = menuHandler::clock_face_picker; + screen->runNow(); + } else if (selected == Time) { + menuHandler::menuQueue = menuHandler::twelve_hour_picker; + screen->runNow(); + } else if (selected == Timezone) { + menuHandler::menuQueue = menuHandler::TZ_picker; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageResponseMenu() +{ + enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 }; + + static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"}; + static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset}; + int options = 3; + + if (kb_found) { + optionsArray[options] = "Reply via Freetext"; + optionsEnumArray[options++] = Freetext; + } + +#ifdef HAS_I2S + optionsArray[options] = "Read Aloud"; + optionsEnumArray[options++] = Aloud; +#endif + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Dismiss) { + screen->dismissCurrentFrame(); + } else if (selected == Preset) { + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); + } + } else if (selected == Freetext) { + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + } + } +#ifdef HAS_I2S + else if (selected == Aloud) { + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + + audioThread->readAloud(msg); + } +#endif + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::homeBaseMenu() +{ + enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Bluetooth, Sleep, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + +#ifdef PIN_EINK_EN + optionsArray[options] = "Toggle Backlight"; + optionsEnumArray[options++] = Backlight; +#else + optionsArray[options] = "Sleep Screen"; + optionsEnumArray[options++] = Sleep; +#endif + + optionsArray[options] = "Send Position"; + optionsEnumArray[options++] = Position; + optionsArray[options] = "New Preset Msg"; + optionsEnumArray[options++] = Preset; + if (kb_found) { + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; + } + optionsArray[options] = "Bluetooth Toggle"; + optionsEnumArray[options++] = Bluetooth; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Home Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Backlight) { +#ifdef PIN_EINK_EN + if (digitalRead(PIN_EINK_EN) == HIGH) { + digitalWrite(PIN_EINK_EN, LOW); + } else { + digitalWrite(PIN_EINK_EN, HIGH); + } +#endif + } else if (selected == Sleep) { + screen->setOn(false); + } else if (selected == Position) { + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (selected == Preset) { + 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::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 }; + 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 + optionsArray[options] = "Screen Options"; + optionsEnumArray[options++] = ScreenOptions; +#endif + + optionsArray[options] = "Reboot/Shutdown"; + optionsEnumArray[options++] = PowerMenu; + + if (test_enabled) { + optionsArray[options] = "Test Menu"; + optionsEnumArray[options++] = Test; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "System Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Notifications) { + menuHandler::menuQueue = menuHandler::notifications_menu; + screen->runNow(); + } else if (selected == ScreenOptions) { + menuHandler::menuQueue = menuHandler::screen_options_menu; + screen->runNow(); + } else if (selected == PowerMenu) { + menuHandler::menuQueue = menuHandler::power_menu; + screen->runNow(); + } else if (selected == Test) { + menuHandler::menuQueue = menuHandler::test_menu; + screen->runNow(); + } else if (selected == Back && !test_enabled) { + test_count++; + if (test_count > 4) { + test_enabled = true; + } + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::favoriteBaseMenu() +{ + enum optionsNumbers { Back, Preset, Freetext, Remove, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; + static int optionsEnumArray[enumEnd] = {Back, Preset}; + int options = 2; + + if (kb_found) { + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; + } + optionsArray[options] = "Remove Favorite"; + optionsEnumArray[options++] = Remove; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Favorites Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if (selected == 2 && kb_found) { + cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if ((!kb_found && selected == 2) || (selected == 3 && kb_found)) { + menuHandler::menuQueue = menuHandler::remove_favorite; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::positionBaseMenu() +{ + enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"}; + static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu}; + int options = 3; + + if (accelerometerThread) { + optionsArray[options] = "Compass Calibrate"; + optionsEnumArray[options++] = CompassCalibrate; + } + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Position Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == GPSToggle) { + menuQueue = gps_toggle_menu; + screen->runNow(); + } else if (selected == CompassMenu) { + menuQueue = compass_point_north_menu; + screen->runNow(); + } else if (selected == CompassCalibrate) { + accelerometerThread->calibrate(30); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::nodeListMenu() +{ + enum optionsNumbers { Back, Favorite, Verify, Reset }; + static const char *optionsArray[] = {"Back", "Add Favorite", "Key Verification", "Reset NodeDB"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Node Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Favorite) { + menuQueue = add_favorite; + screen->runNow(); + } else if (selected == Verify) { + menuQueue = key_verification_init; + screen->runNow(); + } else if (selected == Reset) { + menuQueue = reset_node_db_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::resetNodeDBMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Confirm Reset NodeDB"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + disableBluetooth(); + LOG_INFO("Initiate node-db reset"); + nodeDB->resetNodes(); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::compassNorthMenu() +{ + enum optionsNumbers { Back, Dynamic, Fixed, Freeze }; + static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "North Directions?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.InitialSelected = uiconfig.compass_mode + 1; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Dynamic) { + if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) { + uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } else if (selected == Fixed) { + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { + uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } else if (selected == Freeze) { + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } else if (selected == Back) { + menuQueue = position_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +#if !MESHTASTIC_EXCLUDE_GPS +void menuHandler::GPSToggleMenu() +{ + + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle GPS"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + playGPSEnableBeep(); + gps->enable(); + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 2) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + playGPSDisableBeep(); + gps->disable(); + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuQueue = position_base_menu; + screen->runNow(); + } + }; + bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} +#endif + +void menuHandler::BluetoothToggleMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Bluetooth"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1 || selected == 2) { + InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + }; + bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BuzzerModeMenu() +{ + static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Buzzer Mode"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; + service->reloadConfig(SEGMENT_CONFIG); + }; + bannerOptions.InitialSelected = config.device.buzzer_mode; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BrightnessPickerMenu() +{ + static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; + + // Get current brightness level to set initial selection + int currentSelection = 1; // Default to Medium + if (uiconfig.screen_brightness >= 255) { + currentSelection = 3; // Very High + } else if (uiconfig.screen_brightness >= 128) { + currentSelection = 2; // High + } else { + currentSelection = 1; // Medium + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Brightness"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { // Medium + uiconfig.screen_brightness = 64; + } else if (selected == 2) { // High + uiconfig.screen_brightness = 128; + } else if (selected == 3) { // Very High + uiconfig.screen_brightness = 255; + } + + if (selected != 0) { // Not "Back" + // Apply brightness immediately +#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) + 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); +#endif + + // Save to device + saveUIConfig(); + + LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); + } + }; + bannerOptions.InitialSelected = currentSelection; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::switchToMUIMenu() +{ + static const char *optionsArray[] = {"No", "Yes"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Switch to MUI?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) +{ + static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal", + "Pink", "White"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Select Screen Color"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 10; + bannerOptions.bannerCallback = [display](int selected) -> void { + uint8_t TFT_MESH_r = 0; + uint8_t TFT_MESH_g = 0; + uint8_t TFT_MESH_b = 0; + if (selected == 1) { + LOG_INFO("Setting color to system default or defined variant"); + // Given just before we set all these to zero, we will allow this to go through + } else if (selected == 2) { + LOG_INFO("Setting color to Meshtastic Green"); + TFT_MESH_r = 103; + TFT_MESH_g = 234; + TFT_MESH_b = 148; + } else if (selected == 3) { + LOG_INFO("Setting color to Yellow"); + TFT_MESH_r = 255; + TFT_MESH_g = 255; + TFT_MESH_b = 128; + } else if (selected == 4) { + LOG_INFO("Setting color to Red"); + TFT_MESH_r = 255; + TFT_MESH_g = 64; + TFT_MESH_b = 64; + } else if (selected == 5) { + LOG_INFO("Setting color to Orange"); + TFT_MESH_r = 255; + TFT_MESH_g = 160; + TFT_MESH_b = 20; + } else if (selected == 6) { + LOG_INFO("Setting color to Purple"); + TFT_MESH_r = 204; + TFT_MESH_g = 153; + TFT_MESH_b = 255; + } else if (selected == 7) { + LOG_INFO("Setting color to Teal"); + TFT_MESH_r = 64; + TFT_MESH_g = 224; + TFT_MESH_b = 208; + } else if (selected == 8) { + LOG_INFO("Setting color to Pink"); + TFT_MESH_r = 255; + TFT_MESH_g = 105; + TFT_MESH_b = 180; + } else if (selected == 9) { + LOG_INFO("Setting color to White"); + TFT_MESH_r = 255; + TFT_MESH_g = 255; + TFT_MESH_b = 255; + } else { + menuQueue = system_base_menu; + 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); + display->setColor(WHITE); + + if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { +#ifdef TFT_MESH_OVERRIDE + TFT_MESH = TFT_MESH_OVERRIDE; +#else + TFT_MESH = COLOR565(0x67, 0xEA, 0x94); +#endif + } else { + TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + } + +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) + static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); +#endif + + screen->setFrames(graphics::Screen::FOCUS_SYSTEM); + if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + uiconfig.screen_rgb_color = 0; + } else { + uiconfig.screen_rgb_color = (TFT_MESH_r << 16) | (TFT_MESH_g << 8) | TFT_MESH_b; + } + LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); + saveUIConfig(); + } +#endif + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::rebootMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot Device?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } else { + menuQueue = power_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::shutdownMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Shutdown Device?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + IF_SCREEN(screen->showSimpleBanner("Shutting Down...", 0)); + nodeDB->saveToDisk(); + power->shutdown(); + } else { + menuQueue = power_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::addFavoriteMenu() +{ + screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void { + LOG_WARN("Nodenum: %u", nodenum); + nodeDB->set_favorite(true, nodenum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); +} + +void menuHandler::removeFavoriteMenu() +{ + + static const char *optionsArray[] = {"Back", "Yes"}; + BannerOverlayOptions bannerOptions; + std::string message = "Unfavorite This Node?\n"; + auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); + if (node && node->has_user) { + message += sanitizeString(node->user.long_name).substr(0, 15); + } + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::testMenu() +{ + + static const char *optionsArray[] = {"Back", "Number Picker"}; + BannerOverlayOptions bannerOptions; + std::string message = "Test to Run?\n"; + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + menuQueue = number_test; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::numberTest() +{ + screen->showNumberPicker("Pick a number\n ", 30000, 4, + [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); +} + +void menuHandler::wifiBaseMenu() +{ + enum optionsNumbers { Back, Wifi_toggle }; + + static const char *optionsArray[] = {"Back", "WiFi Toggle"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "WiFi Menu"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_toggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::wifiToggleMenu() +{ + enum optionsNumbers { Back, Wifi_toggle }; + + static const char *optionsArray[] = {"Back", "Disable"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Disable Wifi and\nEnable Bluetooth?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_toggle) { + config.network.wifi_enabled = false; + config.bluetooth.enabled = true; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::notificationsMenu() +{ + enum optionsNumbers { Back, BuzzerActions }; + static const char *optionsArray[] = {"Back", "Buzzer Actions"}; + static int optionsEnumArray[] = {Back, BuzzerActions}; + int options = 2; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Notifications"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == BuzzerActions) { + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +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 + hasSupportBrightness = true; +#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) { + 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 + optionsArray[options] = "Screen Color"; + optionsEnumArray[options++] = ScreenColor; +#endif + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Screen Options"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Brightness) { + menuHandler::menuQueue = menuHandler::brightness_picker; + screen->runNow(); + } else if (selected == ScreenColor) { + menuHandler::menuQueue = menuHandler::tftcolormenupicker; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::powerMenu() +{ + + enum optionsNumbers { Back, Reboot, Shutdown, MUI }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; + + optionsArray[options] = "Reboot"; + optionsEnumArray[options++] = Reboot; + + optionsArray[options] = "Shutdown"; + optionsEnumArray[options++] = Shutdown; + +#if HAS_TFT + optionsArray[options] = "Switch to MUI"; + optionsEnumArray[options++] = MUI; +#endif + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot / Shutdown"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Reboot) { + menuHandler::menuQueue = menuHandler::reboot_menu; + screen->runNow(); + } else if (selected == Shutdown) { + menuHandler::menuQueue = menuHandler::shutdown_menu; + screen->runNow(); + } else if (selected == MUI) { + menuHandler::menuQueue = menuHandler::mui_picker; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::keyVerificationInitMenu() +{ + screen->showNodePicker("Node to Verify", 30000, + [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); }); +} + +void menuHandler::keyVerificationFinalPrompt() +{ + char message[40] = {0}; + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet + + if (screen) { + static const char *optionsArray[] = {"Reject", "Accept"}; + graphics::BannerOverlayOptions options; + options.message = message; + options.durationMs = 30000; + options.optionsArrayPtr = optionsArray; + options.optionsCount = 2; + options.notificationType = graphics::notificationTypeEnum::selection_picker; + options.bannerCallback = [=](int selected) { + if (selected == 1) { + auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode()); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + }; + screen->showOverlayBanner(options); + } +} + +void menuHandler::handleMenuSwitch(OLEDDisplay *display) +{ + if (menuQueue != menu_none) + test_count = 0; + switch (menuQueue) { + case menu_none: + break; + case lora_picker: + LoraRegionPicker(); + break; + case TZ_picker: + TZPicker(); + break; + case twelve_hour_picker: + TwelveHourPicker(); + break; + case clock_face_picker: + ClockFacePicker(); + break; + case clock_menu: + clockMenu(); + break; + case system_base_menu: + systemBaseMenu(); + break; + case position_base_menu: + positionBaseMenu(); + break; +#if !MESHTASTIC_EXCLUDE_GPS + case gps_toggle_menu: + GPSToggleMenu(); + break; +#endif + case compass_point_north_menu: + compassNorthMenu(); + break; + case reset_node_db_menu: + resetNodeDBMenu(); + break; + case buzzermodemenupicker: + BuzzerModeMenu(); + break; + case mui_picker: + switchToMUIMenu(); + break; + case tftcolormenupicker: + TFTColorPickerMenu(display); + break; + case brightness_picker: + BrightnessPickerMenu(); + break; + case reboot_menu: + rebootMenu(); + break; + case shutdown_menu: + shutdownMenu(); + break; + case add_favorite: + addFavoriteMenu(); + break; + case remove_favorite: + removeFavoriteMenu(); + break; + case test_menu: + testMenu(); + break; + case number_test: + numberTest(); + break; + case wifi_toggle_menu: + wifiToggleMenu(); + break; + case key_verification_init: + keyVerificationInitMenu(); + break; + case key_verification_final_prompt: + keyVerificationFinalPrompt(); + break; + case bluetooth_toggle_menu: + BluetoothToggleMenu(); + break; + case notifications_menu: + notificationsMenu(); + break; + case screen_options_menu: + screenOptionsMenu(); + break; + case power_menu: + powerMenu(); + break; + case throttle_message: + screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); + break; + } + menuQueue = menu_none; +} + +void menuHandler::saveUIConfig() +{ + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); +} + +} // namespace graphics + +#endif \ No newline at end of file diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h new file mode 100644 index 000000000..2273dbbed --- /dev/null +++ b/src/graphics/draw/MenuHandler.h @@ -0,0 +1,82 @@ +#pragma once +#if HAS_SCREEN +#include "configuration.h" +namespace graphics +{ + +class menuHandler +{ + public: + enum screenMenus { + menu_none, + lora_picker, + TZ_picker, + twelve_hour_picker, + clock_face_picker, + clock_menu, + position_base_menu, + gps_toggle_menu, + compass_point_north_menu, + reset_node_db_menu, + buzzermodemenupicker, + mui_picker, + tftcolormenupicker, + brightness_picker, + reboot_menu, + shutdown_menu, + add_favorite, + remove_favorite, + test_menu, + number_test, + wifi_toggle_menu, + bluetooth_toggle_menu, + notifications_menu, + screen_options_menu, + power_menu, + system_base_menu, + key_verification_init, + key_verification_final_prompt, + throttle_message + }; + static screenMenus menuQueue; + + static void LoraRegionPicker(uint32_t duration = 30000); + static void handleMenuSwitch(OLEDDisplay *display); + static void clockMenu(); + static void TZPicker(); + static void TwelveHourPicker(); + static void ClockFacePicker(); + static void messageResponseMenu(); + static void homeBaseMenu(); + static void systemBaseMenu(); + static void favoriteBaseMenu(); + static void positionBaseMenu(); + static void compassNorthMenu(); + static void GPSToggleMenu(); + static void BuzzerModeMenu(); + static void switchToMUIMenu(); + static void TFTColorPickerMenu(OLEDDisplay *display); + static void nodeListMenu(); + static void resetNodeDBMenu(); + static void BrightnessPickerMenu(); + static void rebootMenu(); + static void shutdownMenu(); + static void addFavoriteMenu(); + static void removeFavoriteMenu(); + static void testMenu(); + static void numberTest(); + static void wifiBaseMenu(); + static void wifiToggleMenu(); + static void notificationsMenu(); + static void screenOptionsMenu(); + static void powerMenu(); + + private: + static void saveUIConfig(); + static void keyVerificationInitMenu(); + static void keyVerificationFinalPrompt(); + static void BluetoothToggleMenu(); +}; + +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp new file mode 100644 index 000000000..3df8a003c --- /dev/null +++ b/src/graphics/draw/MessageRenderer.cpp @@ -0,0 +1,447 @@ +/* +BaseUI + +Developed and Maintained By: +- Ronald Garcia (HarukiToreda) โ€“ Lead development and implementation. +- JasonP (Xaositek) โ€“ Screen layout and icon design, UI improvements and testing. +- TonyG (Tropho) โ€“ Project management, structural planning, and testing + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "configuration.h" +#if HAS_SCREEN +#include "MessageRenderer.h" + +// Core includes +#include "NodeDB.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "main.h" +#include "meshUtils.h" + +// Additional includes for UI rendering +#include "UIRenderer.h" +#include "graphics/TimeFormatters.h" + +// Additional includes for dependencies +#include +#include + +// External declarations +extern bool hasUnreadMessage; +extern meshtastic_DeviceState devicestate; + +using graphics::Emote; +using graphics::emotes; +using graphics::numEmotes; + +namespace graphics +{ +namespace MessageRenderer +{ + +// Simple cache based on text hash +static size_t cachedKey = 0; +static std::vector cachedLines; +static std::vector cachedHeights; + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) +{ + int cursorX = x; + const int fontHeight = FONT_HEIGHT_SMALL; + + // === Step 1: Find tallest emote in the line === + int maxIconHeight = fontHeight; + for (size_t i = 0; i < line.length();) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (line.compare(i, emojiLen, emotes[e].label) == 0) { + if (emotes[e].height > maxIconHeight) + maxIconHeight = emotes[e].height; + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + uint8_t c = static_cast(line[i]); + if ((c & 0xE0) == 0xC0) + i += 2; + else if ((c & 0xF0) == 0xE0) + i += 3; + else if ((c & 0xF8) == 0xF0) + i += 4; + else + i += 1; + } + } + + // === Step 2: Baseline alignment === + int lineHeight = std::max(fontHeight, maxIconHeight); + int baselineOffset = (lineHeight - fontHeight) / 2; + int fontY = y + baselineOffset; + int fontMidline = fontY + fontHeight / 2; + + // === Step 3: Render line in segments === + size_t i = 0; + bool inBold = false; + + while (i < line.length()) { + // Check for ** start/end for faux bold + if (line.compare(i, 2, "**") == 0) { + inBold = !inBold; + i += 2; + continue; + } + + // Look ahead for the next emote match + size_t nextEmotePos = std::string::npos; + const Emote *matchedEmote = nullptr; + size_t emojiLen = 0; + + for (int e = 0; e < emoteCount; ++e) { + size_t pos = line.find(emotes[e].label, i); + if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { + nextEmotePos = pos; + matchedEmote = &emotes[e]; + emojiLen = strlen(emotes[e].label); + } + } + + // Render normal text segment up to the emote or bold toggle + size_t nextControl = std::min(nextEmotePos, line.find("**", i)); + if (nextControl == std::string::npos) + nextControl = line.length(); + + if (nextControl > i) { + std::string textChunk = line.substr(i, nextControl - i); + if (inBold) { + // Faux bold: draw twice, offset by 1px + display->drawString(cursorX + 1, fontY, textChunk.c_str()); + } + display->drawString(cursorX, fontY, textChunk.c_str()); + cursorX += display->getStringWidth(textChunk.c_str()); + i = nextControl; + continue; + } + + // Render the emote (if found) + if (matchedEmote && i == nextEmotePos) { + int iconY = fontMidline - matchedEmote->height / 2 - 1; + display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); + cursorX += matchedEmote->width + 1; + i += emojiLen; + } else { + // No more emotes โ€” render the rest of the line + std::string remaining = line.substr(i); + if (inBold) { + display->drawString(cursorX + 1, fontY, remaining.c_str()); + } + display->drawString(cursorX, fontY, remaining.c_str()); + cursorX += display->getStringWidth(remaining.c_str()); + break; + } + } +} + +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Clear the unread message indicator when viewing the message + hasUnreadMessage = false; + + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + const int navHeight = FONT_HEIGHT_SMALL; + const int scrollBottom = SCREEN_HEIGHT - navHeight; + const int usableHeight = scrollBottom; + const int textWidth = SCREEN_WIDTH; + + bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isBold = config.display.heading_bold; + + // === Set Title + const char *titleStr = "Messages"; + + // Check if we have more than an empty message to show + char messageBuf[237]; + snprintf(messageBuf, sizeof(messageBuf), "%s", msg); + if (strlen(messageBuf) == 0) { + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + const char *messageString = "No messages"; + int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); + display->drawString(center_text, getTextPositions(display)[2], messageString); + return; + } + + // === Header Construction === + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + char headerStr[80]; + const char *sender = "???"; + if (node && node->has_user) { + if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { + sender = node->user.long_name; + } else { + sender = node->user.short_name; + } + } + uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + + if (useTimestamp && minutes >= 15 && daysAgo == 0) { + std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; + if (config.display.use_12h_clock) { + bool isPM = timestampHours >= 12; + timestampHours = timestampHours % 12; + if (timestampHours == 0) + timestampHours = 12; + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, + isPM ? "p" : "a", sender); + } else { + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, + sender); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), + sender); + } + + uint32_t now = millis(); +#ifndef EXCLUDE_EMOJI + // === Bounce animation setup === + static uint32_t lastBounceTime = 0; + static int bounceY = 0; + const int bounceRange = 2; // Max pixels to bounce up/down + const int bounceInterval = 10; // How quickly to change bounce direction (ms) + + if (now - lastBounceTime >= bounceInterval) { + lastBounceTime = now; + bounceY = (bounceY + 1) % (bounceRange * 2); + } + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (strcmp(msg, e.label) == 0) { + int headerY = getTextPositions(display)[1]; // same as scrolling header line + display->drawString(x + 3, headerY, headerStr); + if (isInverted && isBold) + display->drawString(x + 4, headerY, headerStr); + + // Draw separator (same as scroll version) + for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { + display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13)); + } + + // Center the emote below the header line + separator + nav + int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight; + int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; + display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + + // Draw header at the end to sort out overlapping elements + graphics::drawCommonHeader(display, x, y, titleStr); + return; + } + } +#endif + // === Generate the cache key === + size_t currentKey = (size_t)mp.from; + currentKey ^= ((size_t)mp.to << 8); + currentKey ^= ((size_t)mp.rx_time << 16); + currentKey ^= ((size_t)mp.id << 24); + + if (cachedKey != currentKey) { + LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey); + + // Cache miss - regenerate lines and heights + cachedLines = generateLines(display, headerStr, messageBuf, textWidth); + cachedHeights = calculateLineHeights(cachedLines, emotes); + cachedKey = currentKey; + } else { + // Cache hit but update the header line with current time information + cachedLines[0] = std::string(headerStr); + // The header always has a fixed height since it doesn't contain emotes + // As per calculateLineHeights logic for lines without emotes: + cachedHeights[0] = FONT_HEIGHT_SMALL - 2; + if (cachedHeights[0] < 8) + cachedHeights[0] = 8; // minimum safety + } + + // === Scrolling logic === + int totalHeight = 0; + for (size_t i = 1; i < cachedHeights.size(); ++i) { + totalHeight += cachedHeights[i]; + } + int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height + int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); + + static float scrollY = 0.0f; + static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; + static bool waitingToReset = false, scrollStarted = false; + + // === Smooth scrolling adjustment === + // You can tweak this divisor to change how smooth it scrolls. + // Lower = smoother, but can feel slow. + float delta = (now - lastTime) / 400.0f; + lastTime = now; + + const float scrollSpeed = 2.0f; // pixels per second + + // Delay scrolling start by 2 seconds + if (scrollStartDelay == 0) + scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) + scrollStarted = true; + + if (totalHeight > usableScrollHeight) { + if (scrollStarted) { + if (!waitingToReset) { + scrollY += delta * scrollSpeed; + if (scrollY >= scrollStop) { + scrollY = scrollStop; + waitingToReset = true; + pauseStart = lastTime; + } + } else if (lastTime - pauseStart > 3000) { + scrollY = 0; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = lastTime; + } + } + } else { + scrollY = 0; + } + + int scrollOffset = static_cast(scrollY); + int yOffset = -scrollOffset + getTextPositions(display)[1]; + for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { + display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13)); + } + + // === Render visible lines === + renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold); + + // Draw header at the end to sort out overlapping elements + graphics::drawCommonHeader(display, x, y, titleStr); +} + +std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) +{ + std::vector lines; + lines.push_back(std::string(headerStr)); // Header line is always first + + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { + char ch = messageBuf[i]; + if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && + (unsigned char)messageBuf[i + 2] == 0x99) { + ch = '\''; // plain apostrophe + i += 2; // skip over the extra UTF-8 bytes + } + if (ch == '\n') { + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); + } 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) { + if (!line.empty()) + lines.push_back(line); + line = word; + word.clear(); + } + } + } + + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + + return lines; +} + +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes) +{ + std::vector rowHeights; + + for (const auto &_line : lines) { + int lineHeight = FONT_HEIGHT_SMALL; + bool hasEmote = false; + + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (_line.find(e.label) != std::string::npos) { + lineHeight = std::max(lineHeight, e.height); + hasEmote = true; + } + } + + // Apply tighter spacing if no emotes on this line + if (!hasEmote) { + lineHeight -= 2; // reduce by 2px for tighter spacing + if (lineHeight < 8) + lineHeight = 8; // minimum safety + } + + rowHeights.push_back(lineHeight); + } + + return rowHeights; +} + +void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, + int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold) +{ + for (size_t i = 0; i < lines.size(); ++i) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += rowHeights[j]; + if (lineY > -rowHeights[i] && lineY < scrollBottom) { + if (i == 0 && isInverted) { + display->drawString(x, lineY, lines[i].c_str()); + if (isBold) + display->drawString(x, lineY, lines[i].c_str()); + } else { + drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); + } + } + } +} + +} // namespace MessageRenderer +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h new file mode 100644 index 000000000..c15a699f7 --- /dev/null +++ b/src/graphics/draw/MessageRenderer.h @@ -0,0 +1,30 @@ +#pragma once +#include "OLEDDisplay.h" +#include "OLEDDisplayUi.h" +#include "graphics/emotes.h" +#include +#include + +namespace graphics +{ +namespace MessageRenderer +{ + +// Text and emote rendering +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); + +/// Draws the text message frame for displaying received messages +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Function to generate lines with word wrapping +std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); + +// Function to calculate heights for each line +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes); + +// Function to render the message content +void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, + int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold); + +} // namespace MessageRenderer +} // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp new file mode 100644 index 000000000..d8746fb69 --- /dev/null +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -0,0 +1,567 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "CompassRenderer.h" +#include "NodeDB.h" +#include "NodeListRenderer.h" +#include "UIRenderer.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" // for getTime() function +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "meshUtils.h" +#include + +// Forward declarations for functions defined in Screen.cpp +namespace graphics +{ +extern bool haveGlyphs(const char *str); +} // namespace graphics + +// Global screen instance +extern graphics::Screen *screen; + +namespace graphics +{ +namespace NodeListRenderer +{ + +// Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) +{ + for (int row = 0; row < height; row++) { + uint8_t rowMask = (1 << row); + for (int col = 0; col < width; col++) { + uint8_t colData = pgm_read_byte(&bitmapXBM[col]); + if (colData & rowMask) { + // Note: rows become X, columns become Y after transpose + display->fillRect(x + row * 2, y + col * 2, 2, 2); + } + } + } +} + +// Static variables for dynamic cycling +static NodeListMode currentMode = MODE_LAST_HEARD; +static int scrollIndex = 0; + +// ============================= +// Utility Functions +// ============================= + +const char *getSafeNodeName(meshtastic_NodeInfoLite *node) +{ + static char nodeName[16] = "?"; + if (node->has_user && strlen(node->user.short_name) > 0) { + bool valid = true; + const char *name = node->user.short_name; + for (size_t i = 0; i < strlen(name); i++) { + uint8_t c = (uint8_t)name[i]; + if (c < 32 || c > 126) { + valid = false; + break; + } + } + if (valid) { + strncpy(nodeName, name, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; + } else { + snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); + } + } else { + snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); + } + return nodeName; +} + +const char *getCurrentModeTitle(int screenWidth) +{ + switch (currentMode) { + case MODE_LAST_HEARD: + return "Last Heard"; + case MODE_HOP_SIGNAL: +#ifdef USE_EINK + return "Hops/Sig"; +#else + return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; +#endif + case MODE_DISTANCE: + return "Distance"; + default: + return "Nodes"; + } +} + +// Use dynamic timing based on mode +unsigned long getModeCycleIntervalMs() +{ + return 3000; +} + +int calculateMaxScroll(int totalEntries, int visibleRows) +{ + return std::max(0, (totalEntries - 1) / (visibleRows * 2)); +} + +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) +{ + int columnWidth = display->getWidth() / 2; + int separatorX = x + columnWidth - 2; + for (int y = yStart; y <= yEnd; y += 2) { + display->setPixel(separatorX, y); + } +} + +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) +{ + if (totalEntries <= visibleNodeRows * columns) + return; + + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = display->getHeight() - scrollStartY - 10; + int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); + int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); + int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); + + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } +} + +// ============================= +// Entry Renderers +// ============================= + +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + + const char *nodeName = getSafeNodeName(node); + + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "?"); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName); + if (node->is_favorite) { + if (isHighResolution) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + int rightEdge = x + columnWidth - timeOffset; + if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time + rightEdge -= 1; + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); +} + +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + int nameMaxWidth = columnWidth - 25; + int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + + int barsXOffset = columnWidth - barsOffset; + + const char *nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (isHighResolution) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + // Draw signal strength bars + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + } + } + + // Draw hop count + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + + if (hopStr[0] != '\0') { + int rightEdge = x + columnWidth - hopOffset; + int textWidth = display->getStringWidth(hopStr); + display->drawString(rightEdge - textWidth, y, hopStr); + } +} + +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + const char *nodeName = getSafeNodeName(node); + char distStr[10] = ""; + + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet < 1000) + snprintf(distStr, sizeof(distStr), "%dft", feet); + else + snprintf(distStr, sizeof(distStr), "ยผmi"); // 4-char max + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles < 1000) + snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); + else + snprintf(distStr, sizeof(distStr), "999"); // Max display cap + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters < 1000) + snprintf(distStr, sizeof(distStr), "%dm", meters); + else + snprintf(distStr, sizeof(distStr), "1k"); + } else { + int km = (int)(distanceKm + 0.5); + if (km < 1000) + snprintf(distStr, sizeof(distStr), "%dk", km); + else + snprintf(distStr, sizeof(distStr), "999"); + } + } + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (isHighResolution) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + if (strlen(distStr) > 0) { + int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distStr); + display->drawString(rightEdge - textWidth, y, distStr); + } +} + +void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + switch (currentMode) { + case MODE_LAST_HEARD: + drawEntryLastHeard(display, node, x, y, columnWidth); + break; + case MODE_HOP_SIGNAL: + drawEntryHopSignal(display, node, x, y, columnWidth); + break; + case MODE_DISTANCE: + drawNodeDistance(display, node, x, y, columnWidth); + break; + default: + break; + } +} + +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + const char *nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (isHighResolution) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } +} + +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon) +{ + if (!nodeDB->hasValidPosition(node)) + return; + + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + + int centerX = x + columnWidth - arrowXOffset; + int centerY = y + FONT_HEIGHT_SMALL / 2; + + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); + float bearingToNode = RAD_TO_DEG * bearing; + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float angle = relativeBearing * DEG_TO_RAD; + // Shrink size by 2px + int size = FONT_HEIGHT_SMALL - 5; + CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); + /* + float halfSize = size / 2.0; + + // Point of the arrow + int tipX = centerX + halfSize * cos(angle); + int tipY = centerY - halfSize * sin(angle); + + float baseAngle = radians(35); + float sideLen = halfSize * 0.95; + float notchInset = halfSize * 0.35; + + // Left and right corners + int leftX = centerX + sideLen * cos(angle + PI - baseAngle); + int leftY = centerY - sideLen * sin(angle + PI - baseAngle); + + int rightX = centerX + sideLen * cos(angle + PI + baseAngle); + int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + + // Center notch (cut-in) + int notchX = centerX - notchInset * cos(angle); + int notchY = centerY + notchInset * sin(angle); + + // Draw the chevron-style arrowhead + display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); + display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); + */ +} + +// ============================= +// Main Screen Functions +// ============================= + +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) +{ + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + + int columnWidth = display->getWidth() / 2; + + display->clear(); + + // Draw the battery/time header + graphics::drawCommonHeader(display, x, y, title); + + // Space below header + y += COMMON_HEADER_HEIGHT; + + int totalEntries = nodeDB->getNumMeshNodes(); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + + int visibleNodeRows = totalRowsAvailable; + int totalColumns = 2; + + int startIndex = scrollIndex * visibleNodeRows * totalColumns; + if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { + startIndex++; // skip own node + } + int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + + int yOffset = 0; + int col = 0; + int lastNodeY = y; + int shownCount = 0; + int rowCount = 0; + + for (int i = startIndex; i < endIndex; ++i) { + int xPos = x + (col * columnWidth); + int yPos = y + yOffset; + renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth); + + if (extras) { + extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon); + } + + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; + shownCount++; + rowCount++; + + if (rowCount >= totalRowsAvailable) { + yOffset = 0; + rowCount = 0; + col++; + if (col > (totalColumns - 1)) + break; + } + } + + // Draw column separator + if (shownCount > 0) { + const int firstNodeY = y + 3; + drawColumnSeparator(display, x, firstNodeY, lastNodeY); + } + + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); +} + +// ============================= +// Screen Frame Functions +// ============================= + +#ifndef USE_EINK +void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static NodeListMode lastRenderedMode = MODE_COUNT; + static unsigned long modeStartTime = 0; + + unsigned long now = millis(); + + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT) { + currentMode = MODE_LAST_HEARD; + modeStartTime = now; + } + + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode = static_cast((currentMode + 1) % MODE_COUNT); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode; +} +#endif + +#ifdef USE_EINK +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Last Heard"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +} + +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#ifdef USE_EINK + const char *title = "Hops/Sig"; +#else + + const char *title = "Hops/Signal"; +#endif + drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); +} + +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Distance"; + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); +} +#endif + +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + float heading = 0; + bool validHeading = false; + auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + double lat = DegD(ourNode->position.latitude_i); + double lon = DegD(ourNode->position.longitude_i); + + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { +#if HAS_GPS + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } +#endif + + if (!validHeading) + return; + } + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); +} + +/// Draw a series of fields in a column, wrapping to multiple columns if needed +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +{ + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const char **f = fields; + int xo = x, yo = y; + while (*f) { + display->drawString(xo, yo, *f); + if ((display->getColor() == BLACK) && config.display.heading_bold) + display->drawString(xo + 1, yo, *f); + + display->setColor(WHITE); + yo += FONT_HEIGHT_SMALL; + if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { + xo += SCREEN_WIDTH / 2; + yo = 0; + } + f++; + } +} + +} // namespace NodeListRenderer +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h new file mode 100644 index 000000000..ea8df8bd9 --- /dev/null +++ b/src/graphics/draw/NodeListRenderer.h @@ -0,0 +1,62 @@ +#pragma once + +#include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Node list and entry rendering functions + * + * Contains all functions related to drawing node lists and individual node entries + * including last heard, hop signal, distance, and compass views. + */ +namespace NodeListRenderer +{ +// Entry renderer function types +typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); + +// Node list mode enumeration +enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; + +// Main node list screen function +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, + double lon = 0); + +// Entry renderers +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); + +// Extras renderers +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon); + +// Screen frame functions +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Utility functions +const char *getCurrentModeTitle(int screenWidth); +const char *getSafeNodeName(meshtastic_NodeInfoLite *node); +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); + +// Bitmap drawing function +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); + +} // namespace NodeListRenderer + +} // namespace graphics diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp new file mode 100644 index 000000000..7350c204f --- /dev/null +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -0,0 +1,579 @@ +#include "configuration.h" +#if HAS_SCREEN + +#include "DisplayFormatters.h" +#include "NodeDB.h" +#include "NotificationRenderer.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include +#include +#include + +#ifdef ARCH_ESP32 +#include "esp_task_wdt.h" +#endif + +using namespace meshtastic; + +// External references to global variables from Screen.cpp +extern std::vector functionSymbol; +extern std::string functionSymbolString; +extern bool hasUnreadMessage; + +namespace graphics +{ + +InputEvent NotificationRenderer::inEvent; +int8_t NotificationRenderer::curSelected = 0; +char NotificationRenderer::alertBannerMessage[256] = {0}; +uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever +uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options +const char **NotificationRenderer::optionsArrayPtr = nullptr; +const int *NotificationRenderer::optionsEnumPtr = nullptr; +std::function NotificationRenderer::alertBannerCallback = NULL; +bool NotificationRenderer::pauseBanner = false; +notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; +uint32_t NotificationRenderer::numDigits = 0; +uint32_t NotificationRenderer::currentNumber = 0; + +uint32_t pow_of_10(uint32_t n) +{ + uint32_t ret = 1; + for (uint32_t i = 0; i < n; i++) { + ret *= 10; + } + return ret; +} + +// Used on boot when a certificate is being created +void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(64 + x, y, "Creating SSL certificate"); + +#ifdef ARCH_ESP32 + yield(); + esp_task_wdt_reset(); +#endif + + display->setFont(FONT_SMALL); + if ((millis() / 1000) % 2) { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); + } else { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); + } +} + +void NotificationRenderer::resetBanner() +{ + alertBannerMessage[0] = '\0'; + current_notification_type = notificationTypeEnum::none; + + inEvent.inputEvent = INPUT_BROKER_NONE; + inEvent.kbchar = 0; + curSelected = 0; + alertBannerOptions = 0; // last x lines are seelctable options + optionsArrayPtr = nullptr; + optionsEnumPtr = nullptr; + alertBannerCallback = NULL; + pauseBanner = false; + numDigits = 0; + currentNumber = 0; + + nodeDB->pause_sort(false); +} + +void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') + resetBanner(); + if (!isOverlayBannerShowing() || pauseBanner) + return; + switch (current_notification_type) { + case notificationTypeEnum::none: + // Do nothing - no notification to display + break; + case notificationTypeEnum::text_banner: + case notificationTypeEnum::selection_picker: + drawAlertBannerOverlay(display, state); + break; + case notificationTypeEnum::node_picker: + drawNodePicker(display, state); + break; + case notificationTypeEnum::number_picker: + drawNumberPicker(display, state); + break; + } +} + +void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + // Find lines + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + // modulo to extract + uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); + // Handle input + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + if (this_digit == 9) { + currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber += (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + if (this_digit == 0) { + currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber -= (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) { + if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit + currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1)); + currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1)); + curSelected++; + } + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { + curSelected--; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && + alertBannerUntil != 0) { + resetBanner(); + return; + } + if (curSelected == numDigits) { + alertBannerCallback(currentNumber); + resetBanner(); + return; + } + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + 2; + const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (uint16_t i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + std::string digits = " "; + std::string arrowPointer = " "; + for (uint16_t i = 0; i < numDigits; i++) { + // Modulo minus modulo to return just the current number + digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; + if (curSelected == i) { + arrowPointer += "^ "; + } else { + arrowPointer += "_ "; + } + } + + linePointers[lineCount++] = digits.c_str(); + linePointers[lineCount++] = arrowPointer.c_str(); + + drawNotificationBox(display, state, linePointers, totalLines, 0); +} + +void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + static uint32_t selectedNodenum = 0; + + // === Layout Configuration === + constexpr uint16_t vPadding = 2; + alertBannerOptions = nodeDB->getNumMeshNodes() - 1; + + // let the box drawing function calculate the widths? + + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + + // Handle input + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + curSelected--; + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + alertBannerCallback(selectedNodenum); + resetBanner(); + return; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && + alertBannerUntil != 0) { + resetBanner(); + return; + } + + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + char scratchLineBuffer[visibleTotalLines - lineCount][40]; + + uint8_t firstOptionToShow = 0; + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + int scratchLineNum = 0; + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + char temp_name[16] = {0}; + if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { + std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); + strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + + } else { + snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + } + // make temp buffer for name + // fi + if (i == curSelected) { + selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + if (isHighResolution) { + strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + } else { + strncpy(scratchLineBuffer[scratchLineNum], ">", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + } + scratchLineBuffer[scratchLineNum][39] = '\0'; + } else { + strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36); + } + linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; + } + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); +} + +void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // === Layout Configuration === + constexpr uint16_t vPadding = 2; + + uint16_t optionWidths[alertBannerOptions] = {0}; + uint16_t maxWidth = 0; + uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); + uint16_t lineWidths[MAX_LINES] = {0}; + uint16_t lineLengths[MAX_LINES] = {0}; + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + char lineBuffer[40] = {0}; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; + lineCount++; + } + + // Measure option widths + for (int i = 0; i < alertBannerOptions; i++) { + optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); + if (optionWidths[i] > maxWidth) + maxWidth = optionWidths[i]; + if (optionWidths[i] + arrowsWidth > maxWidth) + maxWidth = optionWidths[i] + arrowsWidth; + } + + // Handle input + if (alertBannerOptions > 0) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + curSelected--; + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + if (optionsEnumPtr != nullptr) { + alertBannerCallback(optionsEnumPtr[curSelected]); + optionsEnumPtr = nullptr; + } else { + alertBannerCallback(curSelected); + } + resetBanner(); + return; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && + alertBannerUntil != 0) { + resetBanner(); + return; + } + + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + } else { + if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_ALT_LONG || + inEvent.inputEvent == INPUT_BROKER_CANCEL) { + resetBanner(); + return; + } + } + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + + uint8_t firstOptionToShow = 0; + if (alertBannerOptions > 0) { + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + } + + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + if (i == curSelected) { + if (isHighResolution) { + strncpy(lineBuffer, "> ", 3); + strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); + } else { + strncpy(lineBuffer, ">", 2); + strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); + } + lineBuffer[39] = '\0'; + linePointers[linesShown] = lineBuffer; + } else { + linePointers[linesShown] = optionsArrayPtr[i]; + } + } + if (alertBannerOptions > 0) { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); + } else { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); + } +} + +void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], + uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth) +{ + + bool is_picker = false; + uint16_t lineCount = 0; + // === Layout Configuration === + constexpr uint16_t hPadding = 5; + constexpr uint16_t vPadding = 2; + bool needs_bell = false; + uint16_t lineWidths[totalLines] = {0}; + uint16_t lineLengths[totalLines] = {0}; + + if (maxWidth != 0) + is_picker = true; + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + while (lines[lineCount] != nullptr) { + auto newlinePointer = strchr(lines[lineCount], '\n'); + if (newlinePointer) + lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first + else // if the newline wasn't found, then pull string length from strlen + lineLengths[lineCount] = strlen(lines[lineCount]); + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + if (!is_picker) { + needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; + } + lineCount++; + } + // count lines + + uint16_t boxWidth = hPadding * 2 + maxWidth; + if (needs_bell) { + if (isHighResolution && boxWidth <= 150) + boxWidth += 26; + if (!isHighResolution && boxWidth <= 100) + boxWidth += 20; + } + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; + uint16_t boxHeight = contentHeight + vPadding * 2; + if (visibleTotalLines == 1) { + boxHeight += (isHighResolution) ? 4 : 3; + } + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + if (totalLines > visibleTotalLines) { + boxWidth += (isHighResolution) ? 4 : 2; + } + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + + // === Draw Box === + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); + + // === Draw Content === + int16_t lineY = boxTop + vPadding; + for (int i = 0; i < lineCount; i++) { + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + if (needs_bell && i == 0) { + int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); + display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); + } + char lineBuffer[lineLengths[i] + 1]; + strncpy(lineBuffer, lines[i], lineLengths[i]); + lineBuffer[lineLengths[i]] = '\0'; + // Determine if this is a pop-up or a pick list + if (alertBannerOptions > 0 && i == 0) { + // Pick List + display->setColor(WHITE); + int background_yOffset = 1; + // Determine if we have low hanging characters + if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { + background_yOffset = -1; + } + display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); + display->setColor(BLACK); + int yOffset = 3; + display->drawString(textX, lineY - yOffset, lineBuffer); + display->setColor(WHITE); + lineY += (effectiveLineHeight - 2 - background_yOffset); + } else { + // Pop-up + display->drawString(textX, lineY, lineBuffer); + lineY += (effectiveLineHeight); + } + } + + // === Scroll Bar (Thicker, inside box, not over title) === + if (totalLines > visibleTotalLines) { + const uint8_t scrollBarWidth = 5; + + int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line + uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + + float ratio = (float)visibleTotalLines / totalLines; + uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); + float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); + uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + + display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); + display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); + } +} + +/// Draw the last text message we received +void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + + char tempBuf[24]; + snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); + display->drawString(0 + x, 0 + y, tempBuf); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); +} + +void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(64 + x, y, "Updating"); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), + "Please be patient and do not power off."); +} + +bool NotificationRenderer::isOverlayBannerShowing() +{ + return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); +} + +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h new file mode 100644 index 000000000..9c30b329c --- /dev/null +++ b/src/graphics/draw/NotificationRenderer.h @@ -0,0 +1,44 @@ +#pragma once + +#include "OLEDDisplay.h" +#include "OLEDDisplayUi.h" +#include "graphics/Screen.h" +#define MAX_LINES 5 + +namespace graphics +{ + +class NotificationRenderer +{ + public: + static InputEvent inEvent; + static char inKeypress; + static int8_t curSelected; + static char alertBannerMessage[256]; + static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static const char **optionsArrayPtr; + static const int *optionsEnumPtr; + static uint8_t alertBannerOptions; // last x lines are seelctable options + static std::function alertBannerCallback; + static uint32_t numDigits; + static uint32_t currentNumber; + + static bool pauseBanner; + + static void resetBanner(); + static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], + uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); + + static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static bool isOverlayBannerShowing(); + + static graphics::notificationTypeEnum current_notification_type; +}; + +} // namespace graphics diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp new file mode 100644 index 000000000..9be8b04f4 --- /dev/null +++ b/src/graphics/draw/UIRenderer.cpp @@ -0,0 +1,1231 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "CompassRenderer.h" +#include "GPSStatus.h" +#include "NodeDB.h" +#include "NodeListRenderer.h" +#include "UIRenderer.h" +#include "airtime.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include "target_specific.h" +#include +#include +#include + +// External variables +extern graphics::Screen *screen; + +namespace graphics +{ +NodeNum UIRenderer::currentFavoriteNodeNum = 0; + +#if !MESHTASTIC_EXCLUDE_GPS +// GeoCoord object for coordinate conversions +extern GeoCoord geoCoord; + +// Threshold values for the GPS lock accuracy bar display +extern uint32_t dopThresholds[5]; + +// Draw GPS status summary +void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + // Draw satellite image + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); + } + char textString[10]; + + if (config.position.fixed_position) { + // GPS coordinates are currently fixed + snprintf(textString, sizeof(textString), "Fixed"); + } + if (!gps->getIsConnected()) { + snprintf(textString, sizeof(textString), "No Lock"); + } + if (!gps->getHasLock()) { + // Draw "No sats" to the right of the icon with slightly more gap + snprintf(textString, sizeof(textString), "No Sats"); + } else { + snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); + } + if (isHighResolution) { + display->drawString(x + 18, y, textString); + } else { + display->drawString(x + 11, y, textString); + } +} + +// Draw status when GPS is disabled or not present +void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + const char *displayLine; + int pos; + if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + pos = display->getWidth() - display->getStringWidth(displayLine); + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" + : "GPS is disabled"; + pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; + } + display->drawString(x + pos, y, displayLine); +} + +void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + char displayLine[32]; + if (!gps->getIsConnected() && !config.position.fixed_position) { + // displayLine = "No GPS Module"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + // displayLine = "No GPS Lock"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + else + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } +} + +// Draw GPS status coordinates +void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + auto gpsFormat = config.display.gps_format; + char displayLine[32]; + + if (!gps->getIsConnected() && !config.position.fixed_position) { + strcpy(displayLine, "No GPS present"); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + strcpy(displayLine, "No GPS Lock"); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + + if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { + char coordinateLine[22]; + if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, + geoCoord.getLongitude() * 1e-7); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), + geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), + geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code + geoCoord.getOLCCode(coordinateLine); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region + snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); + else + snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + } + + // If fixed position, display text "Fixed GPS" alternating with the coordinates. + if (config.position.fixed_position) { + if ((millis() / 10000) % 2) { + display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, + coordinateLine); + } else { + display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); + } + } else { + display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); + } + } else { + char latLine[22]; + char lonLine[22]; + snprintf(latLine, sizeof(latLine), "%2iยฐ %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), + geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(lonLine, sizeof(lonLine), "%3iยฐ %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), + geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, + latLine); + display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine); + } + } +} +#endif // !MESHTASTIC_EXCLUDE_GPS + +// Draw nodes status +void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, + bool show_total, String additional_words) +{ + char usersString[20]; + int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; + + snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str()); + + if (show_total) { + int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; + snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str()); + } + +#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(DISPLAY_FORCE_SMALL_FONTS) + + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 3, 8, 8, imgUser); + } +#else + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 1, 8, 8, imgUser); + } +#endif + int string_offset = (isHighResolution) ? 9 : 0; + display->drawString(x + 10 + string_offset, y - 2, usersString); +} + +// ********************** +// * Favorite Node Info * +// ********************** +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; + + // --- Only display if index is valid --- + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; + + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; + + display->clear(); + currentFavoriteNodeNum = node->num; + // === Create the shortName and title string === + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + char titlestr[32] = {0}; + snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); + + // === Draw battery/time/mail header (common across screens) === + graphics::drawCommonHeader(display, x, y, titlestr); + + // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== + // 1. Each potential info row has a macro-defined Y position (not regular increments!). + // 2. Each row is only shown if it has valid data. + // 3. Each row "moves up" if previous are empty, so there are never any blank rows. + // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. + + // List of available macro Y positions in order, from top to bottom. + int line = 1; // which slot to use next + std::string usernameStr; + + // === 1. Long Name (always try to show first) === + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + if (username) { + usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case + // Print node's long name (e.g. "Backpack Node") + display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); + } + + // === 2. Signal and Hops (combined on one line, if available) === + // If both are present: "Sig: 97% [2hops]" + // If only one: show only that one + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + + // Always use "Sig" for the label + const char *signalLabel = " Sig"; + + // --- Build the Signal/Hops line --- + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + // Decide between "1 Hop" and "N Hops" + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, + (node->hops_away == 1 ? "Hop" : "Hops")); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], signalHopsStr); + } + + // === 3. Heard (last seen, skip if node never seen) === + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], seenStr); + } + + // === 4. Uptime (only show if metric is present) === + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + uint32_t uptime = node->device_metrics.uptime_seconds; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], uptimeStr); + } + + // === 5. Distance (only if both nodes have GPS position) === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; // Make buffer big enough for any string + bool haveDistance = false; + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: ยผmi"); + haveDistance = true; + } + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + haveDistance = true; + } + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + haveDistance = true; + } else if (meters >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: 1km"); + haveDistance = true; + } + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + haveDistance = true; + } + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], distStr); + } + + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = 0; + } else { + bearing -= myHeading; + } + + display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + } + // else show nothing + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) + : getTextPositions(display)[1]; + const int margin = 4; +// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- +#if defined(USE_EINK) + const int iconSize = (isHighResolution) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + // --------- END PATCH FOR EINK NAV BAR ----------- + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + const auto &op = ourNode->position; + float myHeading = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) + bearing -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing + } +} + +// **************************** +// * Device Focused Screen * +// **************************** +void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Header === + graphics::drawCommonHeader(display, x, y, ""); + + // === Content below header === + + // Determine if we need to show 4 or 5 rows on the screen + int rows = 4; + if (!config.bluetooth.enabled) { + rows = 5; + } + + // === First Row: Region / Channel Utilization and Uptime === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + // Display Region and Channel Utilization + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + + char uptimeStr[32] = ""; + uint32_t uptime = millis() / 1000; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); + + // === Second Row: Satellites and Voltage === + config.display.heading_bold = false; + +#if HAS_GPS + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + const char *displayLine; + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (isHighResolution) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); + } else { + UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); + } +#endif + + if (powerStatus->getHasBattery()) { + char batStr[20]; + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); + } else { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); + } + + config.display.heading_bold = origBold; + + // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (isHighResolution) ? 100 : 50; + if (!config.bluetooth.enabled) { +#if defined(USE_EINK) + chutil_bar_width = (isHighResolution) ? 50 : 30; +#else + chutil_bar_width = (isHighResolution) ? 80 : 40; +#endif + } + int chutil_bar_height = (isHighResolution) ? 12 : 7; + int extraoffset = (isHighResolution) ? 6 : 3; + if (!config.bluetooth.enabled) { + extraoffset = (isHighResolution) ? 6 : 1; + } + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + if (!config.bluetooth.enabled) { + starting_position = 0; + } + + display->drawString(starting_position, getTextPositions(display)[line], chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0โ€“25% + float weight2 = 0.35; // Weight for 25โ€“40% + float weight3 = 0.20; // Weight for 40โ€“100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], + chUtilPercentage); + + if (!config.bluetooth.enabled) { + display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); + } + + line += 1; + + // === Fourth & Fifth Rows: Node Identity === + int textWidth = 0; + int nameX = 0; + int yOffset = (isHighResolution) ? 0 : 5; + const char *longName = nullptr; + std::string longNameStr; + + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + longNameStr = sanitizeString(ourNode->user.long_name); + } + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + 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) { + size_t len = strlen(combinedName); + if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { + combinedName[len - 3] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(combinedName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString( + nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); + } else { + // === LongName Centered === + textWidth = display->getStringWidth(longName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); + + // === ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + } +} + +// Start Functions to write date/time to the screen +// Helper function to check if a year is a leap year +bool isLeapYear(int year) +{ + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} + +// Array of days in each month (non-leap year) +const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +// Fills the buffer with a formatted date/time string and returns pixel width +int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) +{ + int sec = rtc_sec % 60; + rtc_sec /= 60; + int min = rtc_sec % 60; + rtc_sec /= 60; + int hour = rtc_sec % 24; + rtc_sec /= 24; + + int year = 1970; + while (true) { + int daysInYear = isLeapYear(year) ? 366 : 365; + if (rtc_sec >= (uint32_t)daysInYear) { + rtc_sec -= daysInYear; + year++; + } else { + break; + } + } + + int month = 0; + while (month < 12) { + int dim = daysInMonth[month]; + if (month == 1 && isLeapYear(year)) + dim++; + if (rtc_sec >= (uint32_t)dim) { + rtc_sec -= dim; + month++; + } else { + break; + } + } + + int day = rtc_sec + 1; + + if (includeTime) { + snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + } else { + snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + } + + return display->getStringWidth(buf); +} + +// Check if the display can render a string (detect special chars; emoji) +bool UIRenderer::haveGlyphs(const char *str) +{ +#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) + // Don't want to make any assumptions about custom language support + return true; +#endif + + // Check each character with the lookup function for the OLED library + // We're not really meant to use this directly.. + bool have = true; + for (uint16_t i = 0; i < strlen(str); i++) { + uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); + // If font doesn't support a character, it is substituted for ยฟ + if (result == 191 && (uint8_t)str[i] != 191) { + have = false; + break; + } + } + + // LOG_DEBUG("haveGlyphs=%d", have); + return have; +} + +#ifdef USE_EINK +/// Used on eink displays while in deep sleep +void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + + // Next frame should use full-refresh, and block while running, else device will sleep before async callback + EINK_ADD_FRAMEFLAG(display, COSMETIC); + EINK_ADD_FRAMEFLAG(display, BLOCKING); + + LOG_DEBUG("Draw deep sleep screen"); + + // Display displayStr on the screen + graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); +} + +/// Used on eink displays when screen updates are paused +void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + LOG_DEBUG("Draw screensaver overlay"); + + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver + + // Config + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *pauseText = "Screen Paused"; + const char *idText = owner.short_name; + const bool useId = haveGlyphs(idText); + constexpr uint8_t padding = 2; + constexpr uint8_t dividerGap = 1; + + // Text widths + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); + const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); + + // Flush with bottom + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + const int16_t boxTop = display->height() - boxHeight; + const int16_t boxBottom = display->height() - 1; + const int16_t idTextLeft = boxLeft + padding; + const int16_t idTextTop = boxTop + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; + const int16_t pauseTextTop = boxTop + padding; + const int16_t dividerX = boxLeft + padding + idTextWidth + padding; + const int16_t dividerTop = boxTop + dividerGap; + const int16_t dividerBottom = boxBottom - dividerGap; + + // Draw: box + display->setColor(EINK_WHITE); + display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(EINK_BLACK); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + + // Draw: text + if (useId) + display->drawString(idTextLeft, idTextTop, idText); + display->drawString(pauseTextLeft, pauseTextTop, pauseText); + display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold + + // Draw: divider + if (useId) + display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); +} +#endif + +/** + * Draw the icon with extra info printed around the corners + */ +void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // draw an xbm image. + // Please note that everything that should be transitioned + // needs to be drawn relative to x and y + + // draw centered icon left to right and centered above the one line of app text + display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, + icon_width, icon_height, icon_bits); + + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = "meshtastic.org"; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + // Draw version and short name in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +// **************************** +// * My Position Screen * +// **************************** +void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = "Position"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === First Row: My Location === +#if HAS_GPS + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + const char *displayLine = ""; // Initialize to empty string by default + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (isHighResolution) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + } else { + UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); + } + + config.display.heading_bold = origBold; + + // === Update GeoCoord === + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude())); + + // === Determine Compass Heading === + float heading = 0; + bool validHeading = false; + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + validHeading = true; + } else { + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; + } else { + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); + } + } + + // If GPS is off, no need to display these parts + if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { + + // === Second Row: Date === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char datetimeStr[25]; + bool showTime = false; // set to true for full datetime + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); + display->drawString(0, getTextPositions(display)[line++], fullLine); + + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++], latStr); + + // === Fourth Row: Longitude === + char lonStr[32]; + snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++], lonStr); + + // === Fifth Row: Altitude === + char DisplayLineTwo[32] = {0}; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + } else { + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); + } + display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); + } + + // === Draw Compass if heading is valid === + if (validHeading) { + // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; + + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + // For E-Ink screens, account for navigation bar at the bottom! + int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; + const int margin = 4; + int availableHeight = +#if defined(USE_EINK) + SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink +#else + SCREEN_HEIGHT - yBelowContent - margin; +#endif + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } + } +#endif +} + +#ifdef USERPREFS_OEM_TEXT + +void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; + if (isHighResolution) { + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } else { + + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } + + switch (USERPREFS_OEM_FONT_SIZE) { + case 0: + display->setFont(FONT_SMALL); + break; + case 2: + display->setFont(FONT_LARGE); + break; + default: + display->setFont(FONT_MEDIUM); + break; + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = USERPREFS_OEM_TEXT; + if (isHighResolution) { + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + } + display->setFont(FONT_SMALL); + + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawOEMIconScreen(region, display, state, x, y); +} + +#endif + +// Navigation bar overlay implementation +static int8_t lastFrameIndex = -1; +static uint32_t lastFrameChangeTime = 0; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; + +void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + int currentFrame = state->currentFrame; + + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } + + const int iconSize = isHighResolution ? 16 : 8; + const int spacing = isHighResolution ? 8 : 4; + const int bigOffset = isHighResolution ? 1 : 0; + + const size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) + return; + + const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); + const size_t currentPage = currentFrame / iconsPerPage; + const size_t pageStart = currentPage * iconsPerPage; + const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); + + const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; + const int xStart = (SCREEN_WIDTH - totalWidth) / 2; + + // Only show bar briefly after switching frames + static uint32_t navBarLastShown = 0; + static bool cosmeticRefreshDone = false; + + bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; + int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; + +#if defined(USE_EINK) + static bool navBarPrevVisible = false; + + if (navBarVisible && !navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar + cosmeticRefreshDone = false; + navBarLastShown = millis(); + } + + if (!navBarVisible && navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar + navBarLastShown = millis(); // Mark when it disappeared + } + + if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { + if (millis() - navBarLastShown > 10000) { // 10s after hidden + EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup + cosmeticRefreshDone = true; + } + } + + navBarPrevVisible = navBarVisible; +#endif + + // Pre-calculate bounding rect + const int rectX = xStart - 2 - bigOffset; + const int rectWidth = totalWidth + 4 + (bigOffset * 2); + const int rectHeight = iconSize + 6; + + // Clear background and draw border + display->setColor(BLACK); + display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); + display->setColor(WHITE); + display->drawRect(rectX, y - 2, rectWidth, rectHeight); + + // Icon drawing loop for the current page + for (size_t i = pageStart; i < pageEnd; ++i) { + const uint8_t *icon = screen->indicatorIcons[i]; + const int x = xStart + (i - pageStart) * (iconSize + spacing); + const bool isActive = (i == static_cast(currentFrame)); + + if (isActive) { + display->setColor(WHITE); + display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); + display->setColor(BLACK); + } + + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } + + if (isActive) { + display->setColor(WHITE); + } + } + + // Knock the corners off the square + display->setColor(BLACK); + display->drawRect(rectX, y - 2, 1, 1); + display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->setColor(WHITE); +} + +void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) +{ + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, message); +} + +std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) +{ + std::string uptime; + + if (days > (HOURS_IN_MONTH * 6)) + uptime = "?"; + else if (days >= 2) + uptime = std::to_string(days) + "d"; + else if (hours >= 2) + uptime = std::to_string(hours) + "h"; + else if (minutes >= 1) + uptime = std::to_string(minutes) + "m"; + else + uptime = std::to_string(seconds) + "s"; + return uptime; +} + +} // namespace graphics + +#endif // HAS_SCREEN \ No newline at end of file diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h new file mode 100644 index 000000000..9e5e8c4b4 --- /dev/null +++ b/src/graphics/draw/UIRenderer.h @@ -0,0 +1,88 @@ +#pragma once + +#include "graphics/Screen.h" +#include "graphics/emotes.h" +#include +#include +#include + +#define HOURS_IN_MONTH 730 + +// Forward declarations for status types +namespace meshtastic +{ +class PowerStatus; +class NodeStatus; +class GPSStatus; +} // namespace meshtastic + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief UI utility drawing functions + * + * Contains utility functions for drawing common UI elements, overlays, + * battery indicators, and other shared graphical components. + */ +class UIRenderer +{ + public: + // Common UI elements + static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, + int node_offset = 0, bool show_total = true, String additional_words = ""); + + // GPS status functions + static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + + // Layout and utility functions + static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY); + + // Overlay and special screens + static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); + + // Navigation bar overlay + static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); + + static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); + + static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + // Icon and screen drawing functions + static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + // Compass and location screen + static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + static NodeNum currentFavoriteNodeNum; + +// OEM screens +#ifdef USERPREFS_OEM_TEXT + static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#endif + +#ifdef USE_EINK + /// Used on eink displays while in deep sleep + static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + /// Used on eink displays when screen updates are paused + static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); +#endif + + static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); + static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + + // Message filtering + static bool shouldDrawMessage(const meshtastic_MeshPacket *packet); + // Check if the display can render a string (detect special chars; emoji) + static bool haveGlyphs(const char *str); +}; // namespace UIRenderer + +} // namespace graphics diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp new file mode 100644 index 000000000..e1a105d20 --- /dev/null +++ b/src/graphics/emotes.cpp @@ -0,0 +1,277 @@ +#include "emotes.h" + +namespace graphics +{ + +// Always define Emote list and count +const Emote emotes[] = { +#ifndef EXCLUDE_EMOJI + // --- Thumbs --- + {"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // ๐Ÿ‘ Thumbs Up + {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // ๐Ÿ‘Ž Thumbs Down + + // --- Smileys (Multiple Unicode Aliases) --- + {"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // ๐Ÿ˜Š Smiling Eyes + {"\U0001F600", Grinning, Grinning_width, Grinning_height}, // ๐Ÿ˜€ Grinning Face + {"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // ๐Ÿ™‚ Slightly Smiling Face + {"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // ๐Ÿ˜‰ Winking Face + {"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // ๐Ÿ˜ Grinning Smiling Eyes + + // --- Question/Alert --- + {"\u2753", question, question_width, question_height}, // โ“ Question Mark + {"\u203C\uFE0F", bang, bang_width, bang_height}, // โ€ผ๏ธ Double Exclamation Mark + + // --- Laughing Faces --- + {"\U0001F602", haha, haha_width, haha_height}, // ๐Ÿ˜‚ Face with Tears of Joy + {"\U0001F923", ROFL, ROFL_width, ROFL_height}, // ๐Ÿคฃ Rolling on the Floor Laughing + {"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // ๐Ÿ˜† Smiling Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // ๐Ÿ˜… Smiling with Sweat + {"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width, + Grinning_SmilingEyes2_height}, // ๐Ÿ˜„ Grinning Face with Smiling Eyes + + // --- Gestures and People --- + {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // ๐Ÿ‘‹ Waving Hand + {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // ๐Ÿค  Cowboy Hat Face + {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // ๐ŸŽง Headphones + + // --- Weather --- + {"\u2600", sun, sun_width, sun_height}, // โ˜€ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // โ˜€๏ธ Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // ๐ŸŒง๏ธ Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // โ˜๏ธ Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // ๐ŸŒซ๏ธ Fog + + // --- Misc Faces --- + {"\U0001F608", devil, devil_width, devil_height}, // ๐Ÿ˜ˆ Smiling Face with Horns + + // --- Hearts (Multiple Unicode Aliases) --- + {"\u2764\uFE0F", heart, heart_width, heart_height}, // โค๏ธ Red Heart + {"\U0001F9E1", heart, heart_width, heart_height}, // ๐Ÿงก Orange Heart + {"\U00002763", heart, heart_width, heart_height}, // โฃ Heart Exclamation + {"\U00002764", heart, heart_width, heart_height}, // โค Red Heart (legacy) + {"\U0001F495", heart, heart_width, heart_height}, // ๐Ÿ’• Two Hearts + {"\U0001F496", heart, heart_width, heart_height}, // ๐Ÿ’– Sparkling Heart + {"\U0001F497", heart, heart_width, heart_height}, // ๐Ÿ’— Growing Heart + {"\U0001F498", heart, heart_width, heart_height}, // ๐Ÿ’˜ Heart with Arrow + + // --- Objects --- + {"\U0001F4A9", poo, poo_width, poo_height}, // ๐Ÿ’ฉ Pile of Poo + {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height} // ๐Ÿ”” Bell +#endif +}; + +const int numEmotes = sizeof(emotes) / sizeof(emotes[0]); + +#ifndef EXCLUDE_EMOJI +const unsigned char thumbup[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, + 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, + 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, + 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, + 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, +}; + +const unsigned char thumbdown[] PROGMEM = { + 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, + 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, + 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, + 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, + 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, +}; + +const unsigned char Smiling_Eyes[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1, + 0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf, + 0x7e, 0xf8, 0xc3, 0xdf, 0x3e, 0xf0, 0x81, 0xdf, 0xbf, 0xf7, 0xbd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0x3f, 0xff, + 0x6f, 0xff, 0xdf, 0xfe, 0x6f, 0xff, 0xdf, 0xfe, 0x9f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf, + 0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x07, 0xc0}; + +const unsigned char Grinning[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1, + 0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf9, 0xf3, 0xcf, 0xfc, 0xf0, 0xe1, 0xcf, + 0xfe, 0xf0, 0xe1, 0xdf, 0xfe, 0xf0, 0xe1, 0xdf, 0xff, 0xf0, 0xe1, 0xff, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x80, 0xff, 0xbe, 0xff, 0xbf, 0xdf, 0x7e, 0x00, 0xc0, 0xdf, + 0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0}; + +const unsigned char Slightly_Smiling[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1, + 0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf9, 0xf3, 0xcf, 0xfc, 0xf0, 0xe1, 0xcf, + 0xfe, 0xf0, 0xe1, 0xdf, 0xfe, 0xf0, 0xe1, 0xdf, 0xff, 0xf0, 0xe1, 0xff, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf, + 0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0}; + +const unsigned char Winking_Face[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1, + 0xf0, 0xf0, 0xff, 0xc3, 0x78, 0xef, 0xc3, 0xc7, 0xb8, 0xdf, 0xbd, 0xcf, 0xfc, 0xf9, 0x7f, 0xcf, 0xfc, 0xf0, 0xff, 0xcf, + 0xfe, 0xf0, 0xc3, 0xdf, 0xfe, 0xf0, 0x81, 0xdf, 0xff, 0xf0, 0xbf, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf, + 0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x07, 0xc0}; + +const unsigned char Grinning_Smiling_Eyes[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1, + 0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf8, 0xe3, 0xcf, 0x7c, 0xf7, 0xdd, 0xcf, + 0xbe, 0xef, 0xbe, 0xdf, 0xbe, 0xef, 0xbe, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0xff, 0x5e, 0x55, 0x55, 0xdf, 0x5e, 0x55, 0x55, 0xdf, + 0x3c, 0x00, 0x80, 0xcf, 0x7c, 0x55, 0xd5, 0xcf, 0xf8, 0x54, 0xe5, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0}; + +const unsigned char question[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, + 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char bang[] PROGMEM = { + 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, + 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, + 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, +}; + +const unsigned char haha[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xf9, 0xf3, 0xc0, + 0xf0, 0xfe, 0xef, 0xc1, 0x38, 0xff, 0x9f, 0xc3, 0xd8, 0xff, 0x7f, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xcf, + 0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xef, 0xff, 0xff, 0xde, 0xe7, 0xff, 0xff, 0xdc, 0xeb, 0xff, 0xff, 0xda, + 0xed, 0xff, 0xff, 0xd6, 0xee, 0xff, 0xff, 0xce, 0x36, 0x00, 0x80, 0xcd, 0xb8, 0xff, 0xbf, 0xc3, 0x7e, 0x00, 0xc0, 0xdf, + 0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0}; + +const unsigned char ROFL[] PROGMEM = { + 0x00, 0x00, 0x00, 0xc0, 0x00, 0xfc, 0x07, 0xc0, 0x00, 0xff, 0x1f, 0xc0, 0x80, 0xff, 0x7f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, + 0xe0, 0x9f, 0xff, 0xc1, 0xf0, 0x9f, 0xff, 0xc0, 0xf8, 0x9f, 0x7f, 0xcb, 0xf8, 0x9f, 0xbf, 0xcb, 0xfc, 0x9f, 0xdf, 0xdb, + 0xfc, 0x1f, 0x08, 0xdc, 0xfe, 0x1f, 0xf8, 0xfe, 0xfe, 0xff, 0xff, 0xfe, 0x1e, 0xf0, 0x7f, 0xfe, 0x1e, 0xf0, 0xbf, 0xfe, + 0xfe, 0xf3, 0xdf, 0xfe, 0xfe, 0xf3, 0x6f, 0xfe, 0xfe, 0xf3, 0x37, 0xfe, 0xfe, 0xeb, 0x1b, 0xfe, 0xfc, 0xef, 0x0d, 0xde, + 0xfc, 0xe7, 0x06, 0xcf, 0xf8, 0x6b, 0x83, 0xcf, 0xf8, 0x0d, 0xc0, 0xc7, 0xf0, 0xed, 0xff, 0xc7, 0xe0, 0xee, 0xff, 0xc3, + 0xc0, 0xee, 0xff, 0xc1, 0x80, 0xee, 0xff, 0xc0, 0x00, 0xe6, 0x3f, 0xc0, 0x00, 0xf0, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0xc0}; + +const unsigned char Smiling_Closed_Eyes[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1, + 0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0x7c, 0xfe, 0xcf, 0xcf, 0xfc, 0xfc, 0xe7, 0xcf, + 0xfe, 0xf9, 0xf3, 0xdf, 0xfe, 0xf3, 0xf9, 0xdf, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xfc, 0xe7, 0xff, 0x7f, 0xfe, 0xcf, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x80, 0xff, 0xbe, 0xff, 0xbf, 0xdf, 0x7e, 0x00, 0xc0, 0xdf, + 0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3, + 0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0}; + +const unsigned char Grinning_SmilingEyes2[] PROGMEM = { + 0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xff, 0xff, 0xc0, + 0xf0, 0xff, 0xff, 0xc1, 0xf8, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xc7, + 0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, + 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0x3f, 0x00, 0x80, 0xdf, 0xbe, 0xff, 0xbf, 0xcf, 0x7e, 0x00, 0xc0, 0xcf, + 0x7c, 0x00, 0xc0, 0xc7, 0xfc, 0x00, 0xe0, 0xc7, 0xf8, 0x01, 0xf0, 0xc3, 0xf8, 0x03, 0xf8, 0xc3, 0xf0, 0xff, 0xff, 0xc1, + 0xe0, 0xff, 0xff, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0}; + +const unsigned char wave_icon[] PROGMEM = { + 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0xc0, 0xc1, 0x00, 0x00, 0x00, 0xc7, + 0x00, 0x00, 0x1e, 0xcc, 0x00, 0x00, 0x30, 0xc8, 0x00, 0x00, 0x60, 0xd8, 0x00, 0x08, 0xc0, 0xd0, 0x00, 0x1a, 0x81, 0xd1, + 0x00, 0x36, 0x03, 0xd3, 0x80, 0x6d, 0x06, 0xd2, 0x00, 0xdb, 0x0c, 0xc2, 0x80, 0xb6, 0x1d, 0xc0, 0x80, 0x6d, 0x1f, 0xc0, + 0x00, 0xdb, 0x3f, 0xc0, 0x00, 0xf6, 0x7f, 0xc0, 0x00, 0xfc, 0x7f, 0xc0, 0x08, 0xf8, 0x7f, 0xc0, 0x48, 0xf0, 0x7f, 0xc0, + 0x48, 0xe0, 0x7f, 0xc0, 0xc8, 0xc0, 0x3f, 0xc0, 0x98, 0x81, 0x1f, 0xc0, 0x10, 0x03, 0x00, 0xc0, 0x30, 0x0e, 0x00, 0xc0, + 0x20, 0x38, 0x00, 0xc0, 0xe0, 0x00, 0x00, 0xc0, 0x80, 0x07, 0x00, 0xc0, 0x00, 0x1e, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0}; + +const unsigned char cowboy[] PROGMEM = { + 0x00, 0x0c, 0x0c, 0xc0, 0x00, 0x02, 0x10, 0xc0, 0x00, 0x01, 0x20, 0xc0, 0xbc, 0x00, 0x40, 0xcf, 0xc2, 0x01, 0xe0, 0xd0, + 0x01, 0x01, 0x20, 0xe0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, + 0xc1, 0x3f, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe1, 0xf2, 0xf3, 0xf3, 0xd3, 0xf4, 0xf1, 0xe3, 0xcb, 0xfc, 0xf1, 0xe3, 0xc7, + 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xfb, 0xf7, 0xc7, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xc7, + 0x70, 0xf8, 0x8f, 0xc3, 0x70, 0x03, 0xb0, 0xc3, 0x70, 0xfe, 0xbf, 0xc3, 0x60, 0x00, 0x80, 0xc1, 0xc0, 0x00, 0xc0, 0xc0, + 0x80, 0x01, 0x60, 0xc0, 0x00, 0x07, 0x38, 0xc0, 0x00, 0xfe, 0x1f, 0xc0, 0x00, 0xf0, 0x03, 0xc0, 0x00, 0x00, 0x00, 0xc0}; + +const unsigned char deadmau5[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, + 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, + 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, + 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, + 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char sun[] PROGMEM = { + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, + 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, + 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, + 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, + 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, +}; + +const unsigned char rain[] PROGMEM = { + 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, + 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, + 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, + 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, + 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, +}; + +const unsigned char cloud[] PROGMEM = { + 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, + 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, + 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, +}; + +const unsigned char fog[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, + 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, + 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char devil[] PROGMEM = { + 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xf0, 0x0f, 0xfc, 0x0f, 0xfc, + 0x3f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfc, 0xff, 0xff, 0xcf, + 0xfc, 0xff, 0xff, 0xcf, 0xf8, 0xff, 0xff, 0xc7, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xf1, 0xe3, 0xc3, + 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3, + 0xf0, 0xff, 0xff, 0xc3, 0xe0, 0xfd, 0xef, 0xc1, 0xe0, 0xf3, 0xf3, 0xc1, 0xc0, 0x07, 0xf8, 0xc0, 0x80, 0x1f, 0x7e, 0xc0, + 0x00, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0}; + +const unsigned char heart[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, + 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, + 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, + 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, +}; + +const unsigned char poo[] PROGMEM = { + 0x00, 0x1c, 0x00, 0xc0, 0x00, 0x7c, 0x00, 0xc0, 0x00, 0xfc, 0x00, 0xc0, 0x00, 0x7c, 0x03, 0xc0, 0x00, 0xbe, 0x03, 0xc0, + 0x00, 0xdf, 0x0f, 0xc0, 0x80, 0xcf, 0x0f, 0xc0, 0xc0, 0xf1, 0x0f, 0xc0, 0x60, 0xfc, 0x0f, 0xc0, 0x30, 0xff, 0x07, 0xc0, + 0x90, 0xff, 0x3b, 0xc0, 0xc0, 0xff, 0x7d, 0xc0, 0xf8, 0xff, 0xfc, 0xc0, 0xf8, 0x3f, 0xf0, 0xc0, 0x78, 0x88, 0xc0, 0xc0, + 0x20, 0xe3, 0x18, 0xc0, 0x98, 0xe7, 0xbc, 0xc1, 0x9c, 0x64, 0xa4, 0xc3, 0x9e, 0x64, 0xa4, 0xc7, 0xbe, 0xe4, 0xa4, 0xc7, + 0xbc, 0x27, 0xbc, 0xc7, 0x38, 0x03, 0xd9, 0xc3, 0x00, 0xf0, 0x63, 0xc0, 0xf8, 0xfc, 0x3f, 0xcf, 0xfc, 0xff, 0x87, 0xdf, + 0xfe, 0xff, 0xe0, 0xdf, 0xfc, 0x1f, 0xfe, 0xdf, 0xf8, 0x07, 0xf8, 0xcf, 0xf0, 0x03, 0xe0, 0xc7, 0x00, 0x00, 0x00, 0xc0}; + +const unsigned char bell_icon[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, + 0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000, + 0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000, + 0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011, + 0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100, + 0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000, + 0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; +#endif + +} // namespace graphics diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h new file mode 100644 index 000000000..30b164cbc --- /dev/null +++ b/src/graphics/emotes.h @@ -0,0 +1,114 @@ +#pragma once +#include + +namespace graphics +{ + +// === Emote List === +struct Emote { + const char *label; + const unsigned char *bitmap; + int width; + int height; +}; + +extern const Emote emotes[/* numEmotes */]; +extern const int numEmotes; + +#ifndef EXCLUDE_EMOJI +// === Emote Bitmaps === +#define thumbs_height 25 +#define thumbs_width 25 +extern const unsigned char thumbup[] PROGMEM; +extern const unsigned char thumbdown[] PROGMEM; + +#define Smiling_Eyes_height 30 +#define Smiling_Eyes_width 30 +extern const unsigned char Smiling_Eyes[] PROGMEM; + +#define Grinning_height 30 +#define Grinning_width 30 +extern const unsigned char Grinning[] PROGMEM; + +#define Slightly_Smiling_height 30 +#define Slightly_Smiling_width 30 +extern const unsigned char Slightly_Smiling[] PROGMEM; + +#define Winking_Face_height 30 +#define Winking_Face_width 30 +extern const unsigned char Winking_Face[] PROGMEM; + +#define Grinning_Smiling_Eyes_height 30 +#define Grinning_Smiling_Eyes_width 30 +extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM; + +#define question_height 25 +#define question_width 25 +extern const unsigned char question[] PROGMEM; + +#define bang_height 30 +#define bang_width 30 +extern const unsigned char bang[] PROGMEM; + +#define haha_height 30 +#define haha_width 30 +extern const unsigned char haha[] PROGMEM; + +#define ROFL_height 30 +#define ROFL_width 30 +extern const unsigned char ROFL[] PROGMEM; + +#define Smiling_Closed_Eyes_height 30 +#define Smiling_Closed_Eyes_width 30 +extern const unsigned char Smiling_Closed_Eyes[] PROGMEM; + +#define Grinning_SmilingEyes2_height 30 +#define Grinning_SmilingEyes2_width 30 +extern const unsigned char Grinning_SmilingEyes2[] PROGMEM; + +#define wave_icon_height 30 +#define wave_icon_width 30 +extern const unsigned char wave_icon[] PROGMEM; + +#define cowboy_height 30 +#define cowboy_width 30 +extern const unsigned char cowboy[] PROGMEM; + +#define deadmau5_height 30 +#define deadmau5_width 60 +extern const unsigned char deadmau5[] PROGMEM; + +#define sun_height 30 +#define sun_width 30 +extern const unsigned char sun[] PROGMEM; + +#define rain_height 30 +#define rain_width 30 +extern const unsigned char rain[] PROGMEM; + +#define cloud_height 30 +#define cloud_width 30 +extern const unsigned char cloud[] PROGMEM; + +#define fog_height 25 +#define fog_width 25 +extern const unsigned char fog[] PROGMEM; + +#define devil_height 30 +#define devil_width 30 +extern const unsigned char devil[] PROGMEM; + +#define heart_height 30 +#define heart_width 30 +extern const unsigned char heart[] PROGMEM; + +#define poo_height 30 +#define poo_width 30 +extern const unsigned char poo[] PROGMEM; + +#define bell_icon_width 30 +#define bell_icon_height 30 +extern const unsigned char bell_icon[] PROGMEM; +#endif // EXCLUDE_EMOJI + +} // namespace graphics diff --git a/src/graphics/images.h b/src/graphics/images.h index 069839a16..beef3a1b2 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -6,19 +6,25 @@ const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; -const uint8_t imgSatellite[] PROGMEM = {0x70, 0x71, 0x22, 0xFA, 0xFA, 0x22, 0x71, 0x70}; -const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C}; +#define imgSatellite_width 8 +#define imgSatellite_height 8 +const uint8_t imgSatellite[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, 0b11011011, 0b00011000, +}; + +const uint8_t imgUSB[] PROGMEM = {0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1, 0xfc, 0x00, 0xfc}; +const uint8_t imgUSB_HighResolution[] PROGMEM = {0x00, 0x3e, 0xf8, 0x80, 0x43, 0xf8, 0xc0, 0xc2, 0xff, 0x60, 0x42, 0xfc, + 0x3c, 0xc2, 0xff, 0x22, 0x42, 0xf8, 0x3d, 0x42, 0xf8, 0x22, 0xc2, 0xff, + 0x61, 0x42, 0xfc, 0xc0, 0xc2, 0xff, 0x80, 0x43, 0xf8, 0x00, 0x3e, 0xf8}; const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3C}; const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF}; const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF}; -#if defined(DISPLAY_CLOCK_FRAME) const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0xe3, 0x1f, 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; -#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) || ARCH_PORTDUINO) && \ @@ -37,181 +43,249 @@ const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1, const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, 0x15, 0x85, 0xf5}; #endif -#ifndef EXCLUDE_EMOJI -#define thumbs_height 25 -#define thumbs_width 25 -static unsigned char thumbup[] PROGMEM = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, - 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, - 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, - 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, - 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, +// === Horizontal battery === +// Basic battery design and all related pieces +const unsigned char batteryBitmap_h_bottom[] PROGMEM = { + 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, + 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; + +const unsigned char batteryBitmap_h_top[] PROGMEM = { + 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, + 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, + 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; + +// Lightning Bolt +const unsigned char lightning_bolt_h[] PROGMEM = { + 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, + 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000, + 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; + +// === Vertical battery === +// Basic battery design and all related pieces +const unsigned char batteryBitmap_v[] PROGMEM = {0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000, + 0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110}; +// This is the left and right bars for the fill in +const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010, 0b10000010}; +// Lightning Bolt +const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100}; + +#define mail_width 10 +#define mail_height 7 +static const unsigned char mail[] PROGMEM = { + 0b11111111, 0b00, // Top line + 0b10000001, 0b00, // Edges + 0b11000011, 0b00, // Diagonals start + 0b10100101, 0b00, // Inner M part + 0b10011001, 0b00, // Inner M part + 0b10000001, 0b00, // Edges + 0b11111111, 0b00 // Bottom line }; -static unsigned char thumbdown[] PROGMEM = { - 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, - 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, - 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, - 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, - 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, +// ๐Ÿ“ฌ Mail / Message +const uint8_t icon_mail[] PROGMEM = { + 0b11111111, // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ top border + 0b10000001, // โ–ˆ โ–ˆ sides + 0b11000011, // โ–ˆโ–ˆ โ–ˆโ–ˆ diagonal + 0b10100101, // โ–ˆ โ–ˆ โ–ˆ โ–ˆ inner M + 0b10011001, // โ–ˆ โ–ˆโ–ˆ โ–ˆ inner M + 0b10000001, // โ–ˆ โ–ˆ sides + 0b10000001, // โ–ˆ โ–ˆ sides + 0b11111111 // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ bottom }; -#define smiley_height 30 -#define smiley_width 30 -static unsigned char smiley[] PROGMEM = { - 0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, - 0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, - 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, - 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, - 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; - -#define question_height 25 -#define question_width 25 -static unsigned char question[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, - 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// ๐Ÿ“ GPS Screen / Location Pin +const unsigned char icon_compass[] PROGMEM = { + 0x3C, // Row 0: ..####.. + 0x52, // Row 1: .#..#.#. + 0x91, // Row 2: #...#..# + 0x91, // Row 3: #...#..# + 0x91, // Row 4: #...#..# + 0x81, // Row 5: #......# + 0x42, // Row 6: .#....#. + 0x3C // Row 7: ..####.. }; -#define bang_height 30 -#define bang_width 30 -static unsigned char bang[] PROGMEM = { - 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, - 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, - 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, - 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, - 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, +const uint8_t icon_radio[] PROGMEM = { + 0x0F, // Row 0: ####.... + 0x10, // Row 1: ....#... + 0x27, // Row 2: ###..#.. + 0x48, // Row 3: ...#..#. + 0x93, // Row 4: ##..#..# + 0xA4, // Row 5: ..#..#.# + 0xA8, // Row 6: ...#.#.# + 0xA9 // Row 7: #..#.#.# }; -#define haha_height 30 -#define haha_width 30 -static unsigned char haha[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, - 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, - 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, - 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, - 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// ๐Ÿช™ Memory Icon +const uint8_t icon_memory[] PROGMEM = { + 0x24, // Row 0: ..#..#.. + 0x3C, // Row 1: ..####.. + 0xC3, // Row 2: ##....## + 0x5A, // Row 3: .#.##.#. + 0x5A, // Row 4: .#.##.#. + 0xC3, // Row 5: ##....## + 0x3C, // Row 6: ..####.. + 0x24 // Row 7: ..#..#.. }; -#define wave_icon_height 30 -#define wave_icon_width 30 -static unsigned char wave_icon[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, - 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, - 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, - 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, - 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// ๐ŸŒ Wi-Fi +const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, + 0b11011011, 0b00011000, 0b00011000, 0b00000000}; + +const uint8_t icon_nodes[] PROGMEM = { + 0xF9, // Row 0 #..####### + 0x00, // Row 1 + 0xF9, // Row 2 #..####### + 0x00, // Row 3 + 0xF9, // Row 4 #..####### + 0x00, // Row 5 + 0xF9, // Row 6 #..####### + 0x00 // Row 7 }; -#define cowboy_height 30 -#define cowboy_width 30 -static unsigned char cowboy[] PROGMEM = { - 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, - 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, - 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, - 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, - 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, - 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, +// โžค Chevron Triangle Arrow Icon (8x8) +const uint8_t icon_list[] PROGMEM = { + 0x10, // Row 0: ...#.... + 0x10, // Row 1: ...#.... + 0x38, // Row 2: ..###... + 0x38, // Row 3: ..###... + 0x7C, // Row 4: .#####.. + 0x6C, // Row 5: .##.##.. + 0xC6, // Row 6: ##...##. + 0x82 // Row 7: #.....#. }; -#define deadmau5_height 30 -#define deadmau5_width 60 -static unsigned char deadmau5[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, - 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, - 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, - 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, - 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// ๐Ÿ“ถ Signal Bars Icon (left to right, small to large with spacing) +const uint8_t icon_signal[] PROGMEM = { + 0b00000000, // โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ + 0b10000000, // โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ + 0b10100000, // โ–‘โ–‘โ–‘โ–‘โ–ˆโ–‘โ–ˆ + 0b10100000, // โ–‘โ–‘โ–‘โ–‘โ–ˆโ–‘โ–ˆ + 0b10101000, // โ–‘โ–‘โ–ˆโ–‘โ–ˆโ–‘โ–ˆ + 0b10101000, // โ–‘โ–‘โ–ˆโ–‘โ–ˆโ–‘โ–ˆ + 0b10101010, // โ–ˆโ–‘โ–ˆโ–‘โ–ˆโ–‘โ–ˆ + 0b11111111 // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ }; -#define sun_width 30 -#define sun_height 30 -static unsigned char sun[] PROGMEM = { - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, - 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, - 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, - 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, - 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, - 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, +// โ†”๏ธ Distance / Measurement Icon (double-ended arrow) +const uint8_t icon_distance[] PROGMEM = { + 0b00000000, // โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ + 0b10000001, // โ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆ arrowheads + 0b01000010, // โ–‘โ–ˆโ–‘โ–‘โ–‘โ–ˆโ–‘ + 0b00100100, // โ–‘โ–‘โ–ˆโ–‘โ–ˆโ–‘โ–‘ + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘ center + 0b00100100, // โ–‘โ–‘โ–ˆโ–‘โ–ˆโ–‘โ–‘ + 0b01000010, // โ–‘โ–ˆโ–‘โ–‘โ–‘โ–ˆโ–‘ + 0b10000001 // โ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆ }; -#define rain_width 30 -#define rain_height 30 -static unsigned char rain[] PROGMEM = { - 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, - 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, - 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, - 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, - 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, +// โš ๏ธ Error / Fault +const uint8_t icon_error[] PROGMEM = { + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ + 0b00000000, // โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ + 0b00000000, // โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ + 0b00000000 // โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ }; -#define cloud_height 30 -#define cloud_width 30 -static unsigned char cloud[] PROGMEM = { - 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, - 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, - 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, - 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, - 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, +// ๐Ÿ  Optimized Home Icon (8x8) +const uint8_t icon_home[] PROGMEM = { + 0b00011000, // โ–ˆโ–ˆ + 0b00111100, // โ–ˆโ–ˆโ–ˆโ–ˆ + 0b01111110, // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ + 0b11111111, // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ + 0b11000011, // โ–ˆโ–ˆ โ–ˆโ–ˆ + 0b11011011, // โ–ˆโ–ˆ โ–ˆโ–ˆ โ–ˆโ–ˆ + 0b11011011, // โ–ˆโ–ˆ โ–ˆโ–ˆ โ–ˆโ–ˆ + 0b11111111 // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ }; -#define fog_height 25 -#define fog_width 25 -static unsigned char fog[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, - 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, - 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// ๐Ÿ”ง Generic module (gear-like shape) +const uint8_t icon_module[] PROGMEM = { + 0b00011000, // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ + 0b00111100, // โ–‘โ–‘โ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ + 0b01111110, // โ–‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘ + 0b11011011, // โ–ˆโ–ˆโ–‘โ–ˆโ–ˆโ–‘โ–ˆโ–ˆ + 0b11011011, // โ–ˆโ–ˆโ–‘โ–ˆโ–ˆโ–‘โ–ˆโ–ˆ + 0b01111110, // โ–‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘ + 0b00111100, // โ–‘โ–‘โ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ + 0b00011000 // โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–‘โ–‘โ–‘ }; -#define devil_height 30 -#define devil_width 30 -static unsigned char devil[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, - 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, - 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, - 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, - 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, - 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, +#define mute_symbol_width 8 +#define mute_symbol_height 8 +const uint8_t mute_symbol[] PROGMEM = { + 0b00011001, // โ–ˆ + 0b00100110, // โ–ˆ + 0b00100100, // โ–ˆโ–ˆโ–ˆโ–ˆ + 0b01001010, // โ–ˆ โ–ˆ โ–ˆ + 0b01010010, // โ–ˆ โ–ˆ โ–ˆ + 0b01100010, // โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ + 0b11111111, // โ–ˆ โ–ˆ + 0b10011000, // โ–ˆ }; -#define heart_height 30 -#define heart_width 30 -static unsigned char heart[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, - 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, - 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, - 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, - 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, +#define mute_symbol_big_width 16 +#define mute_symbol_big_height 16 +const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, + 0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, + 0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, + 0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010, + 0b10000000, 0b01000001, 0b00000000, 0b10000000}; + +// Bell icon for Alert Message +#define bell_alert_width 8 +#define bell_alert_height 8 +const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, + 0b01000010, 0b01000010, 0b11111111, 0b00011000}; + +#define key_symbol_width 8 +#define key_symbol_height 8 +const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, + 0b10101001, 0b10000110, 0b00000000, 0b00000000}; + +#define placeholder_width 8 +#define placeholder_height 8 +const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, 0b11111111}; + +#define icon_node_width 8 +#define icon_node_height 8 +static const uint8_t icon_node[] PROGMEM = { + 0x10, // # + 0x10, // # โ† antenna + 0x10, // # + 0xFE, // ####### โ† device top + 0x82, // # # + 0xAA, // # # # # โ† body with pattern + 0x92, // # # # + 0xFE // ####### โ† device base }; -#define poo_width 30 -#define poo_height 30 -static unsigned char poo[] PROGMEM = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, - 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, - 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, - 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, - 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, - 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, -}; -#endif +#define bluetoothdisabled_width 8 +#define bluetoothdisabled_height 8 +const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, + 0b01001100, 0b00000000, 0b00000000, 0b00000000}; + +#define smallbulletpoint_width 8 +#define smallbulletpoint_height 8 +const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000}; + +// Digital Clock +#define digital_icon_clock_width 8 +#define digital_icon_clock_height 8 +const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, + 0b10010001, 0b10000001, 0b01000010, 0b00111100}; +// Analog Clock +#define analog_icon_clock_width 8 +#define analog_icon_clock_height 8 +const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, + 0b00100100, 0b01000010, 0b01000010, 0b11111111}; #include "img/icon.xbm" +static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h index e1bb96450..3ce16e473 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: DKE - Size: 2.13 inch - Resolution: 122px x 250px - - Flex connector marking: FPC-7528B + - Flex connector marking (not a unique identifier): FPC-7528B Note: this is from an older generation of DKE panels, which still used Solomon Systech controller ICs. DKE's website suggests that the latest DEPG0213BN displays may use Fitipower controllers instead. diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h index 72062e0d6..257fed1a6 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: DKE - Size: 2.9 inch - Resolution: 128px x 296px - - Flex connector marking: FPC-7519 rev.b + - Flex connector marking (not a unique identifier): FPC-7519 rev.b */ diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.cpp b/src/graphics/niche/Drivers/EInk/E0213A367.cpp new file mode 100644 index 000000000..f19cb4ff7 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/E0213A367.cpp @@ -0,0 +1,84 @@ +#include "./E0213A367.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void E0213A367::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +void E0213A367::configWaveform() +{ + // This command (0x37) is poorly documented + // As of July 2025, the datasheet for this display's controller IC is unavailable + // The values are supplied by Heltec, who presumably have privileged access to information from the display manufacturer + // Datasheet for the similar SSD1680 IC hints at the function of this command: + + // "Spare VCOM OTP selection": + // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. + // Maybe value is redundant? No noticeable impact when set to 0x00. + // We'll leave it set to 0x40, following Heltec's lead, just in case. + + // "Display Mode" + // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential refresh) + + // Unusual that waveforms are programmed to OTP, but this meta information is not ..? + + sendCommand(0x37); // "Write Register for Display Option" ? + sendData(0x40); // "Spare VCOM OTP selection" ? + sendData(0x80); // "Display Mode for WS[7:0]" ? + sendData(0x03); // "Display Mode for WS[15:8]" ? + sendData(0x0E); // "Display Mode [23:16]" ? + + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } +} + +// Tell controller IC which operations to run +void E0213A367::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, Display mode 1 "full refresh" + 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 E0213A367::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh + } +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.h b/src/graphics/niche/Drivers/EInk/E0213A367.h new file mode 100644 index 000000000..a36fcb407 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/E0213A367.h @@ -0,0 +1,41 @@ +/* + +E-Ink display driver + - SSD1682 + - Manufacturer: SEEKINK + - Size: 2.13 inch + - Resolution: 122px x 255px + - Flex connector marking: HINK-E0213A162-A1 (hidden, printed on reverse) + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD1682.h" + +namespace NicheGraphics::Drivers +{ +class E0213A367 : public SSD1682 +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + E0213A367() : SSD1682(width, height, supported, 0) {} + + protected: + void configScanning() override; + void configWaveform() override; + 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/Drivers/EInk/GDEY0154D67.cpp b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp index 2cab179b9..9a06fa841 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp @@ -9,12 +9,9 @@ void GDEY0154D67::configScanning() { // "Driver output control" sendCommand(0x01); - sendData(0xC7); + sendData(0xC7); // Scan until gate 199 (200px vertical res.) sendData(0x00); sendData(0x00); - - // To-do: delete this method? - // Values set here might be redundant: C7, 00, 00 seems to be default } // Specify which information is used to control the sequence of voltages applied to move the pixels @@ -52,10 +49,10 @@ void GDEY0154D67::detachFromUpdate() { switch (updateType) { case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh + return beginPolling(50, 300); // At least 300ms for fast refresh case FULL: default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h index fc4d93d12..e391eea50 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: Goodisplay - Size: 1.54 inch - Resolution: 200px x 200px - - Flex connector marking: FPC-B001 + - Flex connector marking (not a unique identifier): FPC-B001 */ @@ -31,9 +31,9 @@ class GDEY0154D67 : public SSD16XX GDEY0154D67() : SSD16XX(width, height, supported) {} protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; void detachFromUpdate() override; }; diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp index a0ff63258..b3a585eb7 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp @@ -12,9 +12,6 @@ void GDEY0213B74::configScanning() sendData(0xF9); sendData(0x00); sendData(0x00); - - // To-do: delete this method? - // Values set here might be redundant: F9, 00, 00 seems to be default } // Specify which information is used to control the sequence of voltages applied to move the pixels diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h index 2212fe92a..1c36f295d 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h @@ -5,7 +5,9 @@ E-Ink display driver - Manufacturer: Goodisplay - Size: 2.13 inch - Resolution: 250px x 122px - - Flex connector marking: FPC-A002 + - Flex connector marking (not a unique identifier): + - FPC-A002 + - FPC-A005 20.06.15 TRX */ diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp new file mode 100644 index 000000000..0509b0502 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp @@ -0,0 +1,61 @@ +#include "./HINK_E0213A289.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void HINK_E0213A289::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); // Maximum gate # (249, bits 0-7) + sendData(0x00); // Maximum gate # (bit 8) + sendData(0x00); // (Do not invert scanning order) +} + +// 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 HINK_E0213A289::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +// Describes the sequence of events performed by the displays controller IC during a refresh +// Includes "power up", "load settings from memory", "update the pixels", etc +void HINK_E0213A289::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 HINK_E0213A289::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h new file mode 100644 index 000000000..eab0bf59d --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - HINK_E0213A289 + - Manufacturer: Holitech + - Size: 2.13 inch + - Resolution: 122px x 250px + - Flex connector label (not a unique identifier): FPC-7528B + + Note: as of Feb. 2025, these panels are used for "WeActStudio 2.13in B&W" display modules + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class HINK_E0213A289 : 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: + HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} + + protected: + void configScanning() override; + void configWaveform() override; + 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/Drivers/EInk/HINK_E042A87.h b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h index ac03b65ef..612072b50 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h +++ b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: Holitech - Size: 4.2 inch - Resolution: 400px x 300px - - Flex connector marking: HINK-E042A07-FPC-A1 + - Flex connector marking (not a unique identifier): HINK-E042A07-FPC-A1 - Silver sticker with QR code, marked: HE042A87 Note: as of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h index b78e3bcca..9fa6eaac9 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h @@ -5,7 +5,6 @@ E-Ink display driver - Manufacturer: WISEVAST - Size: 2.13 inch - Resolution: 122px x 255px - - Flex connector marking: Soldering connector, no connector is needed */ diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h index f9da202aa..499daef05 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: Wisevast - Size: 2.13 inch - Resolution: 122px x 250px - - Flex connector marking: HINK-E0213A162-FPC-A0 (Hidden, printed on back-side) + - Flex connector marking (not a unique identifier): HINK-E0213A162-FPC-A0 (Hidden, printed on back-side) Note: this display uses an uncommon controller IC, Fitipower JD79656. It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays. diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.cpp b/src/graphics/niche/Drivers/EInk/SSD1682.cpp new file mode 100644 index 000000000..c3d7f7786 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/SSD1682.cpp @@ -0,0 +1,41 @@ +#include "./SSD1682.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +SSD1682::SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX) + : SSD16XX(width, height, supported, bufferOffsetX) +{ +} + +// SSD1682 only accepts single-byte x and y values +// This causes an incompatibility with the default SSD16XX::configFullscreen +void SSD1682::configFullscreen() +{ + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint8_t sx = bufferOffsetX; // Notice the offset + static const uint8_t sy = 0; + static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint8_t ey = height; + + // Data entry mode - Left to Right, Top to Bottom + sendCommand(0x11); + sendData(0x03); + + // Select controller IC memory region to display a fullscreen image + sendCommand(0x44); // Memory X start - end + sendData(sx); + sendData(ex); + sendCommand(0x45); // Memory Y start - end + sendData(sy); + sendData(ey); + + // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 + sendCommand(0x4E); // Memory cursor X + sendData(sx); + sendCommand(0x4F); // Memory cursor y + sendData(sy); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.h b/src/graphics/niche/Drivers/EInk/SSD1682.h new file mode 100644 index 000000000..ba3008537 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/SSD1682.h @@ -0,0 +1,31 @@ +/* + +E-Ink base class for displays based on SSD1682 + +SSD1682 has a few quirks. We're implementing them here in a new base class, +to avoid re-implementing them every time we need to add a new SSD1682-based display. + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ + +class SSD1682 : public SSD16XX +{ + public: + SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void deepSleep() {} // Not usable (image memory not retained) +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp new file mode 100644 index 000000000..a8f43420f --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp @@ -0,0 +1,59 @@ +#include "./ZJY128296_029EAAMFGN.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void ZJY128296_029EAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 295 (vertical resolution 296px) + sendCommand(0x01); + sendData(0x27); // Number of gates (295, bits 0-7) + sendData(0x01); // Number of gates (295, bit 8) + sendData(0x00); // (Do not invert scanning order) +} + +// 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 ZJY128296_029EAAMFGN::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void ZJY128296_029EAAMFGN::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 ZJY128296_029EAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 300); // At least 300ms 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/ZJY128296_029EAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h new file mode 100644 index 000000000..27644e709 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - ZJY128296-029EAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 2.9 inch + - Resolution: 128px x 296px + - Flex connector label (not a unique identifier): FPC-A005 20.06.15 TRX + + Note: as of Feb. 2025, these panels are used for "WeActStudio 2.9in B&W" display modules + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class ZJY128296_029EAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 128; + static constexpr uint32_t height = 296; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} + + protected: + void configScanning() override; + void configWaveform() override; + 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/Drivers/EInk/ZJY200200_0154DAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h new file mode 100644 index 000000000..fb16bcf2f --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h @@ -0,0 +1,32 @@ +/* + +E-Ink display driver + - ZJY200200-0154DAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 1.54 inch + - Resolution: 200px x 200px + - Flex connector marking: FPC-B001 + + Note: as of Feb. 2025, these panels are used for "WeActStudio 1.54in B&W" display modules + + This *is* a distinct panel, however the driver is currently identical to GDEY0154D67 + We recognize it as separate now, to avoid breaking any custom builds if the drivers do need to diverge in future. + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./GDEY0154D67.h" + +namespace NicheGraphics::Drivers +{ + +typedef GDEY0154D67 ZJY200200_0154DAAMFGN; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Fonts/FreeSans12pt_Win1250.h b/src/graphics/niche/Fonts/FreeSans12pt_Win1250.h new file mode 100644 index 000000000..66edcc6ad --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans12pt_Win1250.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans12pt_Win1250 +*/ +const uint8_t FreeSans12pt_Win1250Bitmaps[] PROGMEM = { +/* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, +/* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, +/* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, +/* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, +/* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, +/* 0x0A */ +/* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, +/* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, +/* 0x0D */ +/* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, +/* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, +/* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, +/* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, +/* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, +/* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, +/* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, +/* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, +/* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, +/* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, +/* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, +/* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, +/* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, +/* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, +/* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, +/* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, +/* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, +/* ''' 0x27 */ 0xFF, 0xA0, +/* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, +/* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, +/* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, +/* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, +/* ',' 0x2C */ 0xF5, 0x60, +/* '-' 0x2D */ 0xFF, 0xF0, +/* '.' 0x2E */ 0xF0, +/* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, +/* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, +/* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, +/* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, +/* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, +/* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, +/* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, +/* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, +/* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, +/* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, +/* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, +/* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, +/* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, +/* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, +/* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, +/* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, +/* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, +/* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, +/* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, +/* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, +/* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, +/* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, +/* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, +/* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, +/* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, +/* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, +/* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, +/* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, +/* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, +/* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, +/* '_' 0x5F */ 0xFF, 0xFE, +/* '`' 0x60 */ 0xE3, 0x8C, 0x30, +/* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, +/* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, +/* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, +/* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, +/* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, +/* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, +/* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, +/* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, +/* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, +/* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, +/* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, +/* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, +/* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, +/* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, +/* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, +/* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, +/* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, +/* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, +/* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, +/* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, +/* 0x7F */ +/* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, +/* 0x81 */ +/* 0x82 */ 0xF5, 0x80, +/* 0x83 */ +/* 0x84 */ 0xCF, 0x34, 0x51, 0x88, +/* 0x85 */ 0xC6, 0x3C, 0x63, +/* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0x88 */ +/* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, +/* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, +/* 0x8B */ 0x2F, 0x49, 0x99, +/* 0x8C */ 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x00, 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 0x8D */ 0x0C, 0xC0, 0xF8, 0x07, 0x0F, 0xFF, 0xFF, 0xF0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, +/* 0x8F */ 0x01, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x06, 0x00, 0x70, 0x07, 0x00, 0x70, 0x03, 0x00, 0x38, 0x03, 0x80, 0x38, 0x01, 0x80, 0x1C, 0x01, 0xC0, 0x0F, 0xFF, 0xFF, 0xFC, +/* 0x90 */ +/* 0x91 */ 0x6A, 0xF0, +/* 0x92 */ 0xF5, 0x60, +/* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, +/* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, +/* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, +/* 0x96 */ 0xFF, 0xFF, 0xF0, +/* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 0x98 */ +/* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, +/* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, +/* 0x9B */ 0x99, 0x92, 0xF4, +/* 0x9C */ 0x07, 0x03, 0x80, 0xC0, 0x60, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, +/* 0x9D */ 0x03, 0x06, 0x66, 0x64, 0x60, 0xF8, 0xF8, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x78, 0x38, +/* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, +/* 0x9F */ 0x07, 0x01, 0x80, 0xC0, 0x20, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, +/* 0xA0 */ +/* 0xA1 */ 0xC6, 0xD9, 0xB1, 0xC0, +/* 0xA2 */ 0x83, 0x8D, 0xF1, 0xC0, +/* 0xA3 */ 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x80, 0xCC, 0x06, 0xC0, 0x3C, 0x01, 0xC0, 0x3C, 0x01, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x3F, 0xF9, 0xFF, 0xC0, +/* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, +/* 0xA5 */ 0x03, 0x80, 0x03, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x04, 0xE0, 0x0C, 0xE0, 0x0C, 0xE0, 0x08, 0x70, 0x18, 0x70, 0x18, 0x70, 0x10, 0x38, 0x3F, 0xF8, 0x3F, 0xF8, 0x30, 0x1C, 0x70, 0x0C, 0x60, 0x0C, 0x60, 0x0E, 0xE0, 0x06, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x0F, +/* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, +/* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, +/* 0xA8 */ 0xCF, 0x30, +/* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAA */ 0x0F, 0xC0, 0xFF, 0xC3, 0x03, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDC, 0x0E, 0x3F, 0xF0, 0x3F, 0x00, 0x20, 0x01, 0xE0, 0x01, 0x80, 0x06, 0x00, 0xF0, 0x00, +/* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, +/* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, +/* 0xAD */ 0xFF, 0xF0, +/* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAF */ 0x03, 0x00, 0x18, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, +/* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, +/* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xB2 */ 0x76, 0x31, 0x87, 0x80, +/* 0xB3 */ 0x66, 0x66, 0x66, 0x67, 0x7E, 0xE6, 0x66, 0x66, 0x66, +/* 0xB4 */ 0x3B, 0x99, 0x80, +/* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, +/* 0xB7 */ 0xF0, +/* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, +/* 0xB9 */ 0x1F, 0x01, 0xFC, 0x1C, 0x70, 0xC1, 0x80, 0x0C, 0x00, 0xE0, 0xFF, 0x1F, 0x18, 0xC0, 0xC6, 0x06, 0x38, 0x70, 0xFF, 0xE3, 0xC7, 0x00, 0x30, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x03, 0xC0, +/* 0xBA */ 0x3F, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3E, 0x01, 0xF0, 0x3C, 0x0D, 0xDE, 0x7F, 0x02, 0x01, 0xE0, 0x18, 0x46, 0x0F, 0x00, +/* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, +/* 0xBC */ 0xC3, 0x31, 0x8C, 0x63, 0x10, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 0xBD */ 0x77, 0x66, 0xCC, 0xC8, +/* 0xBE */ 0xC7, 0x9B, 0x36, 0xCC, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x80, +/* 0xBF */ 0x0C, 0x03, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xC0 */ 0x03, 0x80, 0x18, 0x00, 0x40, 0x00, 0x00, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, +/* 0xC1 */ 0x01, 0xC0, 0x0C, 0x00, 0x20, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC2 */ 0x07, 0x00, 0x3E, 0x01, 0x8C, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC3 */ 0x10, 0x40, 0x63, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC4 */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xFC, 0x03, 0x30, 0x0C, 0xC0, 0x73, 0x81, 0x86, 0x06, 0x18, 0x38, 0x70, 0xC0, 0xC3, 0xFF, 0x1F, 0xFE, 0x60, 0x19, 0x80, 0x6E, 0x01, 0xF0, 0x03, 0xC0, 0x0C, +/* 0xC5 */ 0x18, 0x0C, 0x06, 0x00, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 0xC6 */ 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x7E, 0x03, 0xFF, 0x0E, 0x07, 0x38, 0x07, 0x60, 0x06, 0xC0, 0x03, 0x00, 0x06, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, 0x60, 0x00, 0xE0, 0x06, 0xC0, 0x0D, 0xC0, 0x31, 0xE0, 0xE1, 0xFF, 0x80, 0xFC, 0x00, +/* 0xC7 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x66, 0x00, 0x7C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x01, 0xDC, 0x03, 0x1C, 0x1E, 0x1F, 0xF8, 0x0F, 0xC0, 0x08, 0x00, 0x1E, 0x00, 0x0C, 0x01, 0x18, 0x01, 0xE0, 0x00, +/* 0xC8 */ 0x06, 0x30, 0x07, 0xC0, 0x07, 0x00, 0x3F, 0x01, 0xFF, 0x87, 0x03, 0x9C, 0x03, 0xB0, 0x03, 0x60, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, 0x70, 0x03, 0x60, 0x06, 0xE0, 0x18, 0xF0, 0x70, 0xFF, 0xC0, 0x7E, 0x00, +/* 0xC9 */ 0x07, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 0xCA */ 0xFF, 0xE7, 0xFF, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xFF, 0xE7, 0xFF, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xFF, 0xF7, 0xFF, 0x80, 0x18, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x01, 0xE0, +/* 0xCB */ 0x19, 0x81, 0x98, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xCC */ 0x08, 0xC0, 0xF8, 0x07, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xCD */ 0x36, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, +/* 0xCE */ 0x39, 0xFC, 0x40, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xCF */ 0x18, 0xC0, 0x3E, 0x00, 0x70, 0x3F, 0xF0, 0xFF, 0xE3, 0x01, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x07, 0xC0, 0x1B, 0x00, 0xEC, 0x07, 0x3F, 0xF8, 0xFF, 0x80, +/* 0xD0 */ 0x7F, 0xE0, 0xFF, 0xE1, 0x80, 0xE3, 0x00, 0xE6, 0x00, 0xCC, 0x01, 0xD8, 0x01, 0xB0, 0x03, 0xFE, 0x07, 0xFC, 0x0D, 0x80, 0x1B, 0x00, 0x36, 0x00, 0x6C, 0x01, 0x98, 0x07, 0x30, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, +/* 0xD1 */ 0x01, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0E, 0x01, 0xF0, 0x0F, 0xC0, 0x7E, 0x03, 0xD8, 0x1E, 0xE0, 0xF3, 0x07, 0x9C, 0x3C, 0x61, 0xE1, 0x8F, 0x0E, 0x78, 0x33, 0xC1, 0xDE, 0x06, 0xF0, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1C, +/* 0xD2 */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xC0, 0x3E, 0x01, 0xF8, 0x0F, 0xC0, 0x7B, 0x03, 0xDC, 0x1E, 0x60, 0xF3, 0x87, 0x8C, 0x3C, 0x31, 0xE1, 0xCF, 0x06, 0x78, 0x3B, 0xC0, 0xDE, 0x03, 0xF0, 0x1F, 0x80, 0x7C, 0x03, 0x80, +/* 0xD3 */ 0x00, 0xE0, 0x00, 0x60, 0x00, 0x40, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD4 */ 0x03, 0xC0, 0x01, 0xE0, 0x01, 0x98, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD5 */ 0x03, 0xB8, 0x01, 0x98, 0x00, 0x98, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD6 */ 0x06, 0x30, 0x03, 0x18, 0x00, 0x00, 0x00, 0xFE, 0x01, 0xFF, 0xC1, 0xE0, 0xF0, 0xC0, 0x18, 0xC0, 0x06, 0x60, 0x03, 0x60, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x06, 0xC0, 0x06, 0x60, 0x03, 0x18, 0x03, 0x0F, 0x07, 0x83, 0xFF, 0x80, 0x7F, 0x00, +/* 0xD7 */ 0x81, 0xC3, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0xC3, 0x81, +/* 0xD8 */ 0x0C, 0xC0, 0x1E, 0x00, 0x78, 0x3F, 0xF8, 0xFF, 0xF3, 0x00, 0xEC, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x03, 0x3F, 0xF8, 0xFF, 0xF3, 0x00, 0xEC, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1C, +/* 0xD9 */ 0x03, 0x00, 0x7C, 0x03, 0x70, 0x19, 0x80, 0xF8, 0x03, 0xC3, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3E, 0x03, 0xB8, 0x38, 0xFF, 0x83, 0xF0, +/* 0xDA */ 0x03, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, +/* 0xDB */ 0x0E, 0xE0, 0x66, 0x03, 0x60, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, +/* 0xDC */ 0x0C, 0xC0, 0x66, 0x00, 0x01, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1F, 0x01, 0xDC, 0x1C, 0x7F, 0xC1, 0xF8, 0x00, +/* 0xDD */ 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x00, 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 0xDE */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x04, 0x00, 0x78, 0x01, 0x81, 0x18, 0x0F, 0x00, +/* 0xDF */ 0x1F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x66, 0x38, 0xC7, 0xD8, 0x1B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x3E, 0x0E, 0xCF, 0x99, 0xE0, +/* 0xE0 */ 0x0C, 0x61, 0x8C, 0x03, 0x3D, 0xFC, 0xE3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, +/* 0xE1 */ 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE2 */ 0x0C, 0x01, 0xE0, 0x1B, 0x03, 0x30, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE3 */ 0x20, 0x82, 0x10, 0x3F, 0x01, 0xE0, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE4 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0x00, 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 0xE5 */ 0x3B, 0x30, 0x06, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x30, +/* 0xE6 */ 0x07, 0x01, 0x80, 0xC0, 0x20, 0x00, 0x07, 0xC3, 0xF9, 0x87, 0xE0, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0E, 0x0D, 0xC7, 0x3F, 0x87, 0xC0, +/* 0xE7 */ 0x1F, 0x0F, 0xE7, 0x1D, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x02, 0x00, 0xE0, 0x0C, 0x23, 0x07, 0x80, +/* 0xE8 */ 0x21, 0x0C, 0xC1, 0xE0, 0x78, 0x00, 0x07, 0xC3, 0xF9, 0x87, 0xE0, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0E, 0x0D, 0xC7, 0x3F, 0x87, 0xC0, +/* 0xE9 */ 0x03, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, +/* 0xEA */ 0x1F, 0x07, 0xF1, 0x87, 0x60, 0x6C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x1D, 0x87, 0x1F, 0xC1, 0xF8, 0x06, 0x01, 0x80, 0x30, 0x06, 0x00, 0x78, +/* 0xEB */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x03, 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0C, 0x01, 0xC0, 0xDC, 0x31, 0xFC, 0x1F, 0x00, +/* 0xEC */ 0x31, 0x82, 0x60, 0x6C, 0x07, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, +/* 0xED */ 0x39, 0x99, 0x80, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x80, +/* 0xEE */ 0x71, 0xED, 0xA3, 0x00, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xEF */ 0x00, 0x67, 0x00, 0x66, 0x00, 0x64, 0x00, 0x6C, 0x00, 0x60, 0x1E, 0x60, 0x3F, 0xE0, 0x71, 0xE0, 0xE0, 0xE0, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0x60, 0xE0, 0xE0, 0x71, 0xE0, 0x3F, 0x60, 0x1E, 0x60, +/* 0xF0 */ 0x00, 0x60, 0x06, 0x03, 0xF0, 0x06, 0x00, 0x61, 0xE6, 0x3F, 0xE7, 0x1E, 0xE0, 0xEC, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xE0, 0xE7, 0x1E, 0x3F, 0xE1, 0xE6, +/* 0xF1 */ 0x03, 0x81, 0xC0, 0x60, 0x30, 0x00, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 0xF2 */ 0x31, 0x84, 0xC1, 0xA0, 0x38, 0x00, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 0xF3 */ 0x07, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF4 */ 0x0C, 0x03, 0xC0, 0xD8, 0x19, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF5 */ 0x0C, 0xC3, 0xB8, 0x66, 0x0D, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF6 */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* 0xF7 */ 0x06, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, +/* 0xF8 */ 0xC7, 0x37, 0x8E, 0x03, 0x3D, 0xFC, 0xE3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, +/* 0xF9 */ 0x0E, 0x07, 0xC1, 0xB8, 0x6C, 0x1F, 0x03, 0x8C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x07, 0xE3, 0xDF, 0xB3, 0xCC, +/* 0xFA */ 0x03, 0x01, 0x80, 0x60, 0x30, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, +/* 0xFB */ 0x1D, 0xC6, 0x61, 0xB0, 0xCC, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, +/* 0xFC */ 0x31, 0x8C, 0x60, 0x00, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 0xFD */ 0x03, 0x80, 0x60, 0x18, 0x06, 0x00, 0x01, 0xC0, 0xD8, 0x1B, 0x06, 0x70, 0xC6, 0x18, 0xC6, 0x18, 0xC1, 0x98, 0x36, 0x06, 0xC0, 0x78, 0x0E, 0x01, 0xC0, 0x30, 0x06, 0x01, 0xC0, 0xF0, 0x1C, 0x00, +/* 0xFE */ 0x61, 0x86, 0x3E, 0xF9, 0x86, 0x18, 0x61, 0x86, 0x18, 0x61, 0x87, 0x8E, 0x10, 0x83, 0xC3, 0x78, +/* 0xFF */ 0xF0, +}; + +const GFXglyph FreeSans12pt_Win1250Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 19, 20, 21, 1, -17 }, +/* 0x02 */ { 48, 19, 20, 21, 1, -17 }, +/* 0x03 */ { 96, 21, 20, 23, 1, -17 }, +/* 0x04 */ { 149, 21, 20, 23, 1, -17 }, +/* 0x05 */ { 202, 20, 20, 22, 1, -17 }, +/* 0x06 */ { 252, 20, 20, 22, 1, -17 }, +/* 0x07 */ { 302, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 302, 23, 20, 25, 1, -17 }, +/* 0x09 */ { 360, 23, 16, 25, 1, -16 }, +/* 0x0A */ { 406, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 406, 21, 20, 23, 1, -17 }, +/* 0x0C */ { 459, 19, 18, 21, 1, -15 }, +/* 0x0D */ { 502, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 502, 20, 20, 22, 1, -17 }, +/* 0x0F */ { 552, 21, 21, 23, 1, -18 }, +/* 0x10 */ { 608, 19, 20, 21, 1, -17 }, +/* 0x11 */ { 656, 21, 20, 23, 1, -17 }, +/* 0x12 */ { 709, 20, 20, 22, 1, -17 }, +/* 0x13 */ { 759, 21, 20, 23, 1, -17 }, +/* 0x14 */ { 812, 21, 20, 23, 1, -17 }, +/* 0x15 */ { 865, 22, 20, 24, 1, -17 }, +/* 0x16 */ { 920, 16, 20, 18, 1, -17 }, +/* 0x17 */ { 960, 21, 20, 23, 1, -17 }, +/* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, +/* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, +/* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, +/* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, +/* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, +/* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, +/* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, +/* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, +/* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, +/* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, +/* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, +/* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, +/* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, +/* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, +/* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, +/* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, +/* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, +/* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, +/* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, +/* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, +/* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, +/* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, +/* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, +/* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, +/* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, +/* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, +/* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, +/* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, +/* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, +/* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, +/* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, +/* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, +/* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, +/* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, +/* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, +/* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, +/* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, +/* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, +/* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, +/* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, +/* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, +/* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, +/* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, +/* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, +/* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, +/* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, +/* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, +/* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, +/* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, +/* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, +/* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, +/* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, +/* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, +/* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, +/* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, +/* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, +/* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, +/* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, +/* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, +/* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, +/* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, +/* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, +/* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, +/* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, +/* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, +/* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, +/* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, +/* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, +/* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, +/* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, +/* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, +/* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, +/* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, +/* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, +/* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, +/* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, +/* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, +/* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, +/* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, +/* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, +/* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, +/* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, +/* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, +/* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, +/* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, +/* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, +/* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, +/* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, +/* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, +/* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, +/* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, +/* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, +/* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, +/* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, +/* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, +/* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, +/* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, +/* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, +/* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, +/* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, +/* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, +/* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, +/* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, +/* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 3368, 14, 17, 16, 1, -17 }, +/* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 3398, 2, 5, 6, 2, -2 }, +/* 0x83 */ { 3400, 0, 0, 8, 0, 0 }, +/* 0x84 */ { 3400, 6, 5, 10, 2, -2 }, +/* 0x85 */ { 3404, 12, 2, 16, 2, -2 }, +/* 0x86 */ { 3407, 10, 21, 13, 2, -17 }, +/* 0x87 */ { 3434, 10, 20, 13, 2, -17 }, +/* 0x88 */ { 3459, 0, 0, 8, 0, 0 }, +/* 0x89 */ { 3459, 23, 18, 24, 0, -18 }, +/* 0x8A */ { 3511, 14, 21, 16, 1, -21 }, +/* 0x8B */ { 3548, 3, 8, 6, 1, -11 }, +/* 0x8C */ { 3551, 14, 22, 16, 1, -22 }, +/* 0x8D */ { 3590, 12, 21, 15, 1, -21 }, +/* 0x8E */ { 3622, 13, 21, 15, 1, -21 }, +/* 0x8F */ { 3657, 13, 22, 15, 1, -22 }, +/* 0x90 */ { 3693, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 3693, 2, 6, 6, 2, -18 }, +/* 0x92 */ { 3695, 2, 6, 6, 2, -18 }, +/* 0x93 */ { 3697, 6, 6, 10, 2, -18 }, +/* 0x94 */ { 3702, 6, 6, 10, 2, -18 }, +/* 0x95 */ { 3707, 6, 6, 10, 2, -11 }, +/* 0x96 */ { 3712, 10, 2, 12, 1, -8 }, +/* 0x97 */ { 3715, 22, 2, 24, 1, -8 }, +/* 0x98 */ { 3721, 0, 0, 8, 0, 0 }, +/* 0x99 */ { 3721, 22, 13, 24, 2, -18 }, +/* 0x9A */ { 3757, 10, 18, 12, 1, -18 }, +/* 0x9B */ { 3780, 3, 8, 6, 2, -10 }, +/* 0x9C */ { 3783, 10, 18, 12, 1, -18 }, +/* 0x9D */ { 3806, 8, 18, 11, 1, -18 }, +/* 0x9E */ { 3824, 10, 18, 12, 1, -18 }, +/* 0x9F */ { 3847, 10, 18, 12, 1, -18 }, +/* 0xA0 */ { 3870, 0, 0, 7, 0, 0 }, +/* 0xA1 */ { 3870, 7, 4, 8, 0, -18 }, +/* 0xA2 */ { 3874, 7, 4, 8, 0, -18 }, +/* 0xA3 */ { 3878, 13, 18, 15, 1, -18 }, +/* 0xA4 */ { 3908, 9, 9, 13, 2, -13 }, +/* 0xA5 */ { 3919, 16, 23, 16, 1, -18 }, +/* 0xA6 */ { 3965, 2, 23, 6, 2, -18 }, +/* 0xA7 */ { 3971, 11, 23, 13, 1, -18 }, +/* 0xA8 */ { 4003, 6, 2, 8, 1, -17 }, +/* 0xA9 */ { 4005, 18, 17, 19, 1, -17 }, +/* 0xAA */ { 4044, 14, 23, 16, 1, -18 }, +/* 0xAB */ { 4085, 8, 8, 12, 2, -11 }, +/* 0xAC */ { 4093, 12, 6, 14, 1, -9 }, +/* 0xAD */ { 4102, 6, 2, 8, 1, -8 }, +/* 0xAE */ { 4104, 18, 17, 19, 1, -17 }, +/* 0xAF */ { 4143, 13, 21, 15, 1, -21 }, +/* 0xB0 */ { 4178, 7, 8, 15, 4, -17 }, +/* 0xB1 */ { 4185, 12, 15, 14, 1, -15 }, +/* 0xB2 */ { 4208, 5, 5, 8, 1, 0 }, +/* 0xB3 */ { 4212, 4, 18, 6, 1, -18 }, +/* 0xB4 */ { 4221, 5, 4, 8, 2, -18 }, +/* 0xB5 */ { 4224, 12, 17, 13, 2, -13 }, +/* 0xB6 */ { 4250, 11, 21, 13, 2, -18 }, +/* 0xB7 */ { 4279, 2, 2, 6, 2, -8 }, +/* 0xB8 */ { 4280, 6, 5, 8, 1, 0 }, +/* 0xB9 */ { 4284, 13, 18, 13, 1, -13 }, +/* 0xBA */ { 4314, 10, 18, 12, 1, -13 }, +/* 0xBB */ { 4337, 8, 8, 12, 2, -10 }, +/* 0xBC */ { 4345, 10, 18, 14, 2, -18 }, +/* 0xBD */ { 4368, 8, 4, 8, 0, -18 }, +/* 0xBE */ { 4372, 7, 18, 9, 1, -18 }, +/* 0xBF */ { 4388, 10, 17, 12, 1, -17 }, +/* 0xC0 */ { 4410, 14, 22, 17, 2, -22 }, +/* 0xC1 */ { 4449, 14, 22, 16, 1, -22 }, +/* 0xC2 */ { 4488, 14, 22, 16, 1, -22 }, +/* 0xC3 */ { 4527, 14, 22, 16, 1, -22 }, +/* 0xC4 */ { 4566, 14, 21, 16, 1, -21 }, +/* 0xC5 */ { 4603, 10, 22, 14, 2, -22 }, +/* 0xC6 */ { 4631, 15, 22, 17, 1, -22 }, +/* 0xC7 */ { 4673, 15, 23, 17, 1, -18 }, +/* 0xC8 */ { 4717, 15, 21, 17, 1, -21 }, +/* 0xC9 */ { 4757, 12, 22, 15, 2, -22 }, +/* 0xCA */ { 4790, 13, 23, 15, 2, -18 }, +/* 0xCB */ { 4828, 12, 21, 15, 2, -21 }, +/* 0xCC */ { 4860, 12, 21, 15, 2, -21 }, +/* 0xCD */ { 4892, 4, 22, 7, 1, -22 }, +/* 0xCE */ { 4903, 6, 22, 7, 0, -22 }, +/* 0xCF */ { 4920, 14, 21, 17, 2, -21 }, +/* 0xD0 */ { 4957, 15, 18, 17, 1, -18 }, +/* 0xD1 */ { 4991, 13, 22, 18, 2, -22 }, +/* 0xD2 */ { 5027, 13, 21, 18, 2, -21 }, +/* 0xD3 */ { 5062, 17, 22, 19, 1, -22 }, +/* 0xD4 */ { 5109, 17, 22, 19, 1, -22 }, +/* 0xD5 */ { 5156, 17, 22, 19, 1, -22 }, +/* 0xD6 */ { 5203, 17, 21, 19, 1, -21 }, +/* 0xD7 */ { 5248, 8, 9, 14, 3, -10 }, +/* 0xD8 */ { 5257, 14, 21, 17, 2, -21 }, +/* 0xD9 */ { 5294, 13, 24, 17, 2, -24 }, +/* 0xDA */ { 5333, 13, 22, 17, 2, -22 }, +/* 0xDB */ { 5369, 13, 22, 17, 2, -22 }, +/* 0xDC */ { 5405, 13, 21, 17, 2, -21 }, +/* 0xDD */ { 5440, 14, 22, 16, 1, -22 }, +/* 0xDE */ { 5479, 12, 23, 15, 1, -18 }, +/* 0xDF */ { 5514, 11, 18, 14, 2, -18 }, +/* 0xE0 */ { 5539, 6, 18, 8, 1, -18 }, +/* 0xE1 */ { 5553, 12, 18, 13, 1, -18 }, +/* 0xE2 */ { 5580, 12, 18, 13, 1, -18 }, +/* 0xE3 */ { 5607, 12, 18, 13, 1, -18 }, +/* 0xE4 */ { 5634, 12, 17, 13, 1, -17 }, +/* 0xE5 */ { 5660, 5, 22, 6, 0, -22 }, +/* 0xE6 */ { 5674, 10, 18, 12, 1, -18 }, +/* 0xE7 */ { 5697, 10, 18, 12, 1, -13 }, +/* 0xE8 */ { 5720, 10, 18, 12, 1, -18 }, +/* 0xE9 */ { 5743, 11, 18, 13, 1, -18 }, +/* 0xEA */ { 5768, 11, 18, 13, 1, -13 }, +/* 0xEB */ { 5793, 11, 17, 13, 1, -17 }, +/* 0xEC */ { 5817, 11, 18, 13, 1, -18 }, +/* 0xED */ { 5842, 5, 18, 5, 0, -18 }, +/* 0xEE */ { 5854, 6, 18, 6, 0, -18 }, +/* 0xEF */ { 5868, 16, 18, 18, 1, -18 }, +/* 0xF0 */ { 5904, 12, 18, 14, 1, -18 }, +/* 0xF1 */ { 5931, 10, 18, 13, 1, -18 }, +/* 0xF2 */ { 5954, 10, 18, 13, 1, -18 }, +/* 0xF3 */ { 5977, 11, 18, 13, 1, -18 }, +/* 0xF4 */ { 6002, 11, 18, 13, 1, -18 }, +/* 0xF5 */ { 6027, 11, 18, 13, 1, -18 }, +/* 0xF6 */ { 6052, 11, 17, 13, 1, -17 }, +/* 0xF7 */ { 6076, 12, 11, 14, 1, -11 }, +/* 0xF8 */ { 6093, 6, 18, 8, 1, -18 }, +/* 0xF9 */ { 6107, 10, 19, 13, 1, -19 }, +/* 0xFA */ { 6131, 10, 18, 13, 1, -18 }, +/* 0xFB */ { 6154, 10, 18, 13, 1, -18 }, +/* 0xFC */ { 6177, 10, 17, 13, 1, -17 }, +/* 0xFD */ { 6199, 11, 23, 11, 0, -18 }, +/* 0xFE */ { 6231, 6, 21, 7, 1, -16 }, +/* 0xFF */ { 6247, 2, 2, 8, 3, -17 }, +}; + +const GFXfont FreeSans12pt_Win1250 PROGMEM = { +(uint8_t*)FreeSans12pt_Win1250Bitmaps, +(GFXglyph*)FreeSans12pt_Win1250Glyphs, +0x01, 0xFF, 19 +}; diff --git a/src/graphics/niche/Fonts/FreeSans12pt_Win1251.h b/src/graphics/niche/Fonts/FreeSans12pt_Win1251.h new file mode 100644 index 000000000..d2972f836 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans12pt_Win1251.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans12pt_Win1251 +*/ +const uint8_t FreeSans12pt_Win1251Bitmaps[] PROGMEM = { +/* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, +/* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, +/* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, +/* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, +/* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, +/* 0x0A */ +/* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, +/* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, +/* 0x0D */ +/* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, +/* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, +/* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, +/* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, +/* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, +/* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, +/* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, +/* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, +/* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, +/* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, +/* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, +/* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, +/* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, +/* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, +/* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, +/* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, +/* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, +/* ''' 0x27 */ 0xFF, 0xA0, +/* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, +/* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, +/* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, +/* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, +/* ',' 0x2C */ 0xF5, 0x60, +/* '-' 0x2D */ 0xFF, 0xF0, +/* '.' 0x2E */ 0xF0, +/* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, +/* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, +/* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, +/* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, +/* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, +/* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, +/* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, +/* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, +/* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, +/* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, +/* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, +/* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, +/* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, +/* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, +/* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, +/* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, +/* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, +/* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, +/* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, +/* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, +/* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, +/* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, +/* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, +/* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, +/* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, +/* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, +/* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, +/* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, +/* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, +/* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, +/* '_' 0x5F */ 0xFF, 0xFE, +/* '`' 0x60 */ 0xE3, 0x8C, 0x30, +/* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, +/* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, +/* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, +/* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, +/* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, +/* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, +/* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, +/* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, +/* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, +/* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, +/* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, +/* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, +/* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, +/* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, +/* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, +/* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, +/* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, +/* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, +/* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, +/* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, +/* 0x7F */ +/* 0x80 */ 0xFF, 0xE0, 0xFF, 0xE0, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x07, 0xFC, 0x07, 0xFE, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x1C, +/* 0x81 */ 0x07, 0x01, 0xC0, 0x20, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, +/* 0x82 */ 0xF5, 0x80, +/* 0x83 */ 0x0C, 0x38, 0x61, 0x80, 0x1F, 0xFF, 0xE0, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x80, +/* 0x84 */ 0xCF, 0x34, 0x51, 0x88, +/* 0x85 */ 0xC6, 0x3C, 0x63, +/* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0x88 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, +/* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, +/* 0x8A */ 0x1F, 0xF8, 0x00, 0x3F, 0xF0, 0x00, 0x60, 0x60, 0x00, 0xC0, 0xC0, 0x01, 0x81, 0x80, 0x03, 0x03, 0x00, 0x06, 0x06, 0x00, 0x0C, 0x0C, 0x00, 0x18, 0x1F, 0xF0, 0x30, 0x3F, 0xF0, 0x60, 0x60, 0x30, 0xC0, 0xC0, 0x31, 0x81, 0x80, 0x66, 0x03, 0x00, 0xCC, 0x06, 0x01, 0xB8, 0x0C, 0x06, 0xE0, 0x1F, 0xFD, 0x80, 0x3F, 0xE0, +/* 0x8B */ 0x2F, 0x49, 0x99, +/* 0x8C */ 0xC0, 0x60, 0x06, 0x03, 0x00, 0x30, 0x18, 0x01, 0x80, 0xC0, 0x0C, 0x06, 0x00, 0x60, 0x30, 0x03, 0x01, 0x80, 0x18, 0x0C, 0x00, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0x30, 0x18, 0x1D, 0x80, 0xC0, 0x3C, 0x06, 0x01, 0xE0, 0x30, 0x0F, 0x01, 0x80, 0x78, 0x0C, 0x06, 0xC0, 0x7F, 0xF6, 0x03, 0xFE, 0x00, +/* 0x8D */ 0x03, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xC0, 0x7C, 0x0E, 0xC1, 0xCC, 0x38, 0xC7, 0x0C, 0xE0, 0xDC, 0x0F, 0x80, 0xF0, 0x0F, 0x80, 0xDC, 0x0C, 0xE0, 0xC7, 0x0C, 0x30, 0xC3, 0x8C, 0x1C, 0xC0, 0xEC, 0x07, +/* 0x8E */ 0xFF, 0xE0, 0xFF, 0xE0, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x07, 0xFC, 0x07, 0xFE, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, +/* 0x8F */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xC0, 0xC0, 0x06, 0x00, 0x30, 0x00, +/* 0x90 */ 0x30, 0x0F, 0xF0, 0xFF, 0x03, 0x00, 0x30, 0x03, 0x3C, 0x37, 0xE3, 0xC7, 0x38, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x33, 0x03, 0x00, 0x60, 0x06, 0x01, 0x80, 0x30, +/* 0x91 */ 0x6A, 0xF0, +/* 0x92 */ 0xF5, 0x60, +/* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, +/* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, +/* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, +/* 0x96 */ 0xFF, 0xFF, 0xF0, +/* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 0x98 */ +/* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, +/* 0x9A */ 0x7F, 0x80, 0x3F, 0xC0, 0x18, 0x60, 0x0C, 0x30, 0x06, 0x18, 0x03, 0x0F, 0xF1, 0x87, 0xFC, 0xC3, 0x07, 0x61, 0x81, 0xB0, 0xC0, 0xD0, 0x60, 0xF8, 0x3F, 0xEC, 0x1F, 0xE0, +/* 0x9B */ 0x99, 0x92, 0xF4, +/* 0x9C */ 0xC0, 0xC0, 0x30, 0x30, 0x0C, 0x0C, 0x03, 0x03, 0x00, 0xC0, 0xC0, 0x3F, 0xFF, 0xCF, 0xFF, 0xFB, 0x03, 0x07, 0xC0, 0xC0, 0xF0, 0x30, 0x3C, 0x0C, 0x1F, 0x03, 0xFE, 0xC0, 0xFF, 0x00, +/* 0x9D */ 0x07, 0x07, 0x03, 0x03, 0x00, 0x06, 0x0F, 0x0D, 0x8C, 0xCC, 0x6C, 0x3C, 0x1E, 0x0F, 0x86, 0xE3, 0x39, 0x8E, 0xC3, 0xE0, 0xC0, +/* 0x9E */ 0x30, 0x1F, 0xE3, 0xFC, 0x18, 0x03, 0x00, 0x67, 0x0D, 0xF9, 0xC7, 0x38, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, +/* 0x9F */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xFF, 0xFF, 0xC1, 0x80, 0x60, +/* 0xA0 */ +/* 0xA1 */ 0x10, 0x40, 0xC4, 0x07, 0xE0, 0x1E, 0x0C, 0x01, 0xF0, 0x1D, 0x80, 0xCE, 0x0E, 0x30, 0x61, 0xC7, 0x06, 0x30, 0x3B, 0x81, 0xD8, 0x07, 0xC0, 0x3C, 0x00, 0xE0, 0x06, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0x00, +/* 0xA2 */ 0x21, 0x86, 0x20, 0xFC, 0x07, 0x0E, 0x06, 0xC0, 0xD8, 0x33, 0x86, 0x30, 0xC6, 0x30, 0xC6, 0x0C, 0xC1, 0xB0, 0x36, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0x30, 0x0E, 0x07, 0x80, 0xE0, 0x00, +/* 0xA3 */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, +/* 0xA5 */ 0x00, 0x60, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x00, +/* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, +/* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, +/* 0xA8 */ 0x19, 0x81, 0x98, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAA */ 0x07, 0xE0, 0x3F, 0xF0, 0xF0, 0x71, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1C, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, +/* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, +/* 0xAD */ 0xFF, 0xF0, +/* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAF */ 0xC7, 0x8C, 0x01, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x00, +/* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, +/* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xB2 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 0xB3 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, +/* 0xB4 */ 0x03, 0x03, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, +/* 0xB7 */ 0xF0, +/* 0xB8 */ 0x19, 0x83, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x03, 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0C, 0x01, 0xC0, 0xDC, 0x31, 0xFC, 0x1F, 0x00, +/* 0xB9 */ 0xC0, 0xC0, 0x18, 0x18, 0x03, 0x83, 0x00, 0x70, 0x60, 0x0B, 0x0C, 0x01, 0x61, 0x8F, 0xA6, 0x33, 0x1C, 0xC6, 0x41, 0x88, 0xC8, 0x31, 0x99, 0x06, 0x13, 0x20, 0xC3, 0x66, 0x38, 0x6C, 0xEF, 0x07, 0x8F, 0xA0, 0xF0, 0x04, 0x0E, 0x00, 0x81, 0xC7, 0xF0, 0x18, 0xFC, +/* 0xBA */ 0x1F, 0x87, 0xF9, 0xC3, 0x30, 0x3E, 0x01, 0xFE, 0x3F, 0xC6, 0x00, 0xE0, 0x0C, 0x0D, 0xC3, 0x9F, 0xE1, 0xF8, +/* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, +/* 0xBC */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, +/* 0xBD */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 0xBE */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x00, 0xF0, 0x3E, 0x1D, 0xFE, 0x3E, 0x00, +/* 0xBF */ 0xCF, 0x30, 0x00, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xC0 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC1 */ 0xFF, 0xE7, 0xFF, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xF8, 0xFF, 0xF6, 0x01, 0xB0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 0xC2 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 0xC3 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 0xC4 */ 0x07, 0xFE, 0x01, 0xFF, 0x80, 0x60, 0x60, 0x18, 0x18, 0x06, 0x06, 0x01, 0x81, 0x80, 0x60, 0x60, 0x18, 0x18, 0x06, 0x06, 0x01, 0x81, 0x80, 0x60, 0x60, 0x18, 0x18, 0x0E, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x70, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xC0, +/* 0xC5 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 0xC6 */ 0x70, 0x60, 0xE3, 0x06, 0x0C, 0x38, 0x61, 0xC1, 0xC6, 0x38, 0x0E, 0x67, 0x00, 0x66, 0x60, 0x03, 0x6C, 0x00, 0x3F, 0xC0, 0x01, 0xF8, 0x00, 0x1F, 0x80, 0x03, 0xFC, 0x00, 0x76, 0xE0, 0x0E, 0x67, 0x01, 0xC6, 0x38, 0x38, 0x61, 0xC3, 0x06, 0x0C, 0x60, 0x60, 0xEE, 0x06, 0x07, +/* 0xC7 */ 0x0F, 0x81, 0xFF, 0x0C, 0x18, 0xC0, 0x66, 0x03, 0x00, 0x18, 0x01, 0xC0, 0x1C, 0x07, 0xC0, 0x3F, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xE0, 0x0F, 0x80, 0x6E, 0x0E, 0x3F, 0xE0, 0x7E, 0x00, +/* 0xC8 */ 0xC0, 0x3E, 0x01, 0xF0, 0x1F, 0x80, 0xFC, 0x0D, 0xE0, 0xEF, 0x06, 0x78, 0x73, 0xC3, 0x1E, 0x30, 0xF1, 0x87, 0x98, 0x3D, 0xC1, 0xEC, 0x0F, 0xC0, 0x7E, 0x03, 0xE0, 0x1F, 0x00, 0xC0, +/* 0xC9 */ 0x10, 0x40, 0xC4, 0x07, 0xE0, 0x1E, 0x0C, 0x03, 0xE0, 0x1F, 0x01, 0xF8, 0x0F, 0xC0, 0xDE, 0x0E, 0xF0, 0x67, 0x87, 0x3C, 0x31, 0xE3, 0x0F, 0x18, 0x79, 0x83, 0xDC, 0x1E, 0xC0, 0xFC, 0x07, 0xE0, 0x3E, 0x01, 0xF0, 0x0C, +/* 0xCA */ 0xC0, 0x7C, 0x0E, 0xC1, 0xCC, 0x38, 0xC7, 0x0C, 0xE0, 0xDC, 0x0F, 0x80, 0xF0, 0x0F, 0x80, 0xDC, 0x0C, 0xE0, 0xC7, 0x0C, 0x30, 0xC3, 0x8C, 0x1C, 0xC0, 0xEC, 0x07, +/* 0xCB */ 0x1F, 0xFC, 0x7F, 0xF1, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0x60, 0x31, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0x60, 0x31, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0xC0, 0x33, 0x00, 0xDC, 0x03, 0xE0, 0x0F, 0x00, 0x30, +/* 0xCC */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, +/* 0xCD */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 0xCE */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, +/* 0xCF */ 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 0xD0 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 0xD1 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 0xD2 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xD3 */ 0xC0, 0x1F, 0x01, 0xD8, 0x0C, 0xE0, 0xE3, 0x06, 0x1C, 0x70, 0x63, 0x03, 0xB8, 0x1D, 0x80, 0x7C, 0x03, 0xC0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x00, +/* 0xD4 */ 0x00, 0xC0, 0x00, 0x30, 0x00, 0xFF, 0xC0, 0xFF, 0xFC, 0x78, 0xC7, 0x98, 0x30, 0x6E, 0x0C, 0x1F, 0x03, 0x03, 0xC0, 0xC0, 0xF0, 0x30, 0x3C, 0x0C, 0x0F, 0x83, 0x07, 0x60, 0xC1, 0x9E, 0x31, 0xE3, 0xFF, 0xF0, 0x3F, 0xF0, 0x00, 0xC0, 0x00, 0x30, 0x00, +/* 0xD5 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, +/* 0xD6 */ 0xC0, 0x19, 0x80, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x01, 0x98, 0x03, 0x30, 0x06, 0x60, 0x0C, 0xC0, 0x19, 0x80, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x01, 0x98, 0x03, 0x30, 0x06, 0x60, 0x0C, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x18, 0x00, 0x30, 0x00, 0x60, +/* 0xD7 */ 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x37, 0xFF, 0x3F, 0xF0, 0x03, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, +/* 0xD8 */ 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, +/* 0xD9 */ 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0C, 0x00, 0x03, 0x00, 0x00, 0xC0, +/* 0xDA */ 0xFE, 0x00, 0x3F, 0x80, 0x00, 0x60, 0x00, 0x18, 0x00, 0x06, 0x00, 0x01, 0x80, 0x00, 0x60, 0x00, 0x1F, 0xF8, 0x07, 0xFF, 0x01, 0x80, 0xE0, 0x60, 0x1C, 0x18, 0x03, 0x06, 0x00, 0xC1, 0x80, 0x30, 0x60, 0x1C, 0x18, 0x0E, 0x07, 0xFF, 0x01, 0xFF, 0x80, +/* 0xDB */ 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0xFF, 0x83, 0xFF, 0xE1, 0xE0, 0x38, 0xF0, 0x0E, 0x78, 0x03, 0x3C, 0x01, 0x9E, 0x00, 0xCF, 0x00, 0xE7, 0x80, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, 0xC0, +/* 0xDC */ 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xF8, 0xFF, 0xE6, 0x03, 0xB0, 0x0F, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0xF8, 0x0E, 0xFF, 0xE7, 0xFE, 0x00, +/* 0xDD */ 0x0F, 0xC0, 0x7F, 0xE1, 0xC1, 0xE7, 0x01, 0xCC, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x1F, 0xFE, 0x3F, 0xFC, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xD8, 0x03, 0x98, 0x0E, 0x38, 0x3C, 0x3F, 0xF0, 0x1F, 0x80, +/* 0xDE */ 0xC0, 0x3F, 0x06, 0x07, 0xFE, 0x30, 0x70, 0x39, 0x87, 0x00, 0xEC, 0x30, 0x03, 0x61, 0x80, 0x1B, 0x18, 0x00, 0x78, 0xC0, 0x03, 0xFE, 0x00, 0x1F, 0xF0, 0x00, 0xF1, 0x80, 0x07, 0x8C, 0x00, 0x3C, 0x70, 0x03, 0xE1, 0x80, 0x1B, 0x0E, 0x01, 0xD8, 0x38, 0x1C, 0xC0, 0xFF, 0xC6, 0x01, 0xF8, 0x00, +/* 0xDF */ 0x0F, 0xFC, 0xFF, 0xF3, 0x00, 0xD8, 0x03, 0x60, 0x0D, 0x80, 0x36, 0x00, 0xCC, 0x03, 0x3F, 0xFC, 0x3F, 0xF0, 0x38, 0xC1, 0xC3, 0x0E, 0x0C, 0x70, 0x31, 0x80, 0xCE, 0x03, 0x70, 0x0F, 0x80, 0x30, +/* 0xE0 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 0xE1 */ 0x00, 0xC0, 0x38, 0x3F, 0x1F, 0x87, 0x00, 0xC0, 0x17, 0xC7, 0xFC, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1F, 0x07, 0x71, 0xC7, 0xF0, 0x7C, 0x00, +/* 0xE2 */ 0xFE, 0x3F, 0xEC, 0x3B, 0x06, 0xC1, 0xB0, 0xEF, 0xF3, 0x0E, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, +/* 0xE3 */ 0xFF, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x00, +/* 0xE4 */ 0x0F, 0xF0, 0x3F, 0xC0, 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x30, 0x60, 0xC1, 0x83, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0xC0, +/* 0xE5 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, +/* 0xE6 */ 0xE1, 0x87, 0x71, 0x8E, 0x39, 0x9C, 0x1D, 0xB8, 0x0F, 0xF0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xF0, 0x1D, 0xB8, 0x39, 0x9C, 0x71, 0x8E, 0xE1, 0x87, 0xC1, 0x83, +/* 0xE7 */ 0x3E, 0x7F, 0xB0, 0xE0, 0x30, 0x18, 0x78, 0x3C, 0x07, 0x01, 0xE0, 0xF8, 0xEF, 0xE3, 0xE0, +/* 0xE8 */ 0xC0, 0xF8, 0x3F, 0x07, 0xE1, 0xFC, 0x37, 0x8C, 0xF3, 0x9E, 0x63, 0xD8, 0x7F, 0x0F, 0xC1, 0xF8, 0x3E, 0x06, +/* 0xE9 */ 0x21, 0x86, 0x20, 0xFC, 0x0F, 0x0C, 0x0F, 0x83, 0xF0, 0x7E, 0x1F, 0xC3, 0x78, 0xCF, 0x39, 0xE6, 0x3D, 0x87, 0xF0, 0xFC, 0x1F, 0x83, 0xE0, 0x60, +/* 0xEA */ 0xC1, 0xE1, 0xB1, 0x99, 0x8D, 0x87, 0x83, 0xC1, 0xF0, 0xDC, 0x67, 0x31, 0xD8, 0x7C, 0x18, +/* 0xEB */ 0x3F, 0xCF, 0xF3, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xDC, 0x36, 0x0F, 0x83, 0xC0, 0xC0, +/* 0xEC */ 0xE0, 0x7E, 0x07, 0xF0, 0xFF, 0x0F, 0xF0, 0xFD, 0x9B, 0xD9, 0xBD, 0xFB, 0xCF, 0x3C, 0xF3, 0xC6, 0x3C, 0x63, 0xC0, 0x30, +/* 0xED */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 0xEE */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 0xEF */ 0xFF, 0xFF, 0xFC, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 0xF0 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xF1 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, +/* 0xF2 */ 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xF3 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, +/* 0xF4 */ 0x00, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x0F, 0x67, 0x87, 0xFD, 0xF8, 0xC3, 0xE3, 0xB8, 0x78, 0x3E, 0x06, 0x03, 0xC0, 0xC0, 0x78, 0x18, 0x0F, 0x03, 0x01, 0xE0, 0x60, 0x3E, 0x1E, 0x0E, 0xC3, 0xE3, 0x9F, 0xFF, 0xE1, 0xF6, 0x78, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x00, 0x60, 0x00, +/* 0xF5 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, +/* 0xF6 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCF, 0xFF, 0xFF, 0xF0, 0x03, 0x00, 0x30, +/* 0xF7 */ 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0xFE, 0xFF, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xF8 */ 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xFF, 0xFF, 0xFF, 0xFC, +/* 0xF9 */ 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0x00, 0x03, +/* 0xFA */ 0xFC, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0xF0, 0x3F, 0xE0, 0xC1, 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC1, 0xC3, 0xFE, 0x0F, 0xF0, +/* 0xFB */ 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xFF, 0x0F, 0xFE, 0x3C, 0x1C, 0xF0, 0x33, 0xC0, 0xCF, 0x03, 0x3C, 0x1C, 0xFF, 0xE3, 0xFF, 0x0C, +/* 0xFC */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0x3F, 0xEC, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, +/* 0xFD */ 0x3F, 0x0F, 0xF1, 0x87, 0x60, 0x60, 0x0E, 0x3F, 0xC7, 0xF8, 0x03, 0x00, 0xD8, 0x1B, 0x87, 0x3F, 0xC3, 0xF0, +/* 0xFE */ 0xC0, 0xF8, 0xC1, 0xFC, 0xC3, 0x8E, 0xC7, 0x07, 0xC6, 0x03, 0xFE, 0x03, 0xFE, 0x03, 0xC6, 0x03, 0xC6, 0x03, 0xC7, 0x06, 0xC3, 0x8E, 0xC1, 0xFC, 0xC0, 0xF8, +/* 0xFF */ 0x1F, 0xCF, 0xF7, 0x0D, 0x83, 0x60, 0xD8, 0x33, 0xFC, 0x7F, 0x0C, 0xC6, 0x33, 0x0D, 0x83, 0xC0, 0xC0, +}; + +const GFXglyph FreeSans12pt_Win1251Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 19, 20, 21, 1, -17 }, +/* 0x02 */ { 48, 19, 20, 21, 1, -17 }, +/* 0x03 */ { 96, 21, 20, 23, 1, -17 }, +/* 0x04 */ { 149, 21, 20, 23, 1, -17 }, +/* 0x05 */ { 202, 20, 20, 22, 1, -17 }, +/* 0x06 */ { 252, 20, 20, 22, 1, -17 }, +/* 0x07 */ { 302, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 302, 23, 20, 25, 1, -17 }, +/* 0x09 */ { 360, 23, 16, 25, 1, -16 }, +/* 0x0A */ { 406, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 406, 21, 20, 23, 1, -17 }, +/* 0x0C */ { 459, 19, 18, 21, 1, -15 }, +/* 0x0D */ { 502, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 502, 20, 20, 22, 1, -17 }, +/* 0x0F */ { 552, 21, 21, 23, 1, -18 }, +/* 0x10 */ { 608, 19, 20, 21, 1, -17 }, +/* 0x11 */ { 656, 21, 20, 23, 1, -17 }, +/* 0x12 */ { 709, 20, 20, 22, 1, -17 }, +/* 0x13 */ { 759, 21, 20, 23, 1, -17 }, +/* 0x14 */ { 812, 21, 20, 23, 1, -17 }, +/* 0x15 */ { 865, 22, 20, 24, 1, -17 }, +/* 0x16 */ { 920, 16, 20, 18, 1, -17 }, +/* 0x17 */ { 960, 21, 20, 23, 1, -17 }, +/* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, +/* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, +/* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, +/* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, +/* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, +/* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, +/* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, +/* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, +/* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, +/* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, +/* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, +/* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, +/* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, +/* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, +/* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, +/* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, +/* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, +/* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, +/* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, +/* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, +/* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, +/* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, +/* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, +/* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, +/* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, +/* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, +/* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, +/* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, +/* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, +/* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, +/* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, +/* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, +/* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, +/* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, +/* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, +/* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, +/* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, +/* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, +/* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, +/* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, +/* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, +/* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, +/* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, +/* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, +/* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, +/* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, +/* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, +/* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, +/* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, +/* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, +/* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, +/* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, +/* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, +/* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, +/* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, +/* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, +/* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, +/* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, +/* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, +/* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, +/* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, +/* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, +/* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, +/* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, +/* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, +/* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, +/* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, +/* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, +/* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, +/* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, +/* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, +/* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, +/* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, +/* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, +/* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, +/* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, +/* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, +/* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, +/* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, +/* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, +/* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, +/* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, +/* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, +/* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, +/* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, +/* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, +/* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, +/* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, +/* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, +/* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, +/* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, +/* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, +/* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, +/* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, +/* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, +/* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, +/* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, +/* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, +/* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, +/* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, +/* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, +/* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, +/* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, +/* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 3368, 16, 22, 18, 1, -18 }, +/* 0x81 */ { 3412, 11, 22, 14, 2, -22 }, +/* 0x82 */ { 3443, 2, 5, 6, 2, -2 }, +/* 0x83 */ { 3445, 7, 18, 9, 1, -18 }, +/* 0x84 */ { 3461, 6, 5, 10, 2, -2 }, +/* 0x85 */ { 3465, 12, 2, 16, 2, -2 }, +/* 0x86 */ { 3468, 10, 21, 13, 2, -17 }, +/* 0x87 */ { 3495, 10, 20, 13, 2, -17 }, +/* 0x88 */ { 3520, 14, 17, 16, 1, -17 }, +/* 0x89 */ { 3550, 23, 18, 24, 0, -18 }, +/* 0x8A */ { 3602, 23, 18, 24, 0, -18 }, +/* 0x8B */ { 3654, 3, 8, 6, 1, -11 }, +/* 0x8C */ { 3657, 21, 18, 24, 2, -18 }, +/* 0x8D */ { 3705, 12, 22, 15, 2, -22 }, +/* 0x8E */ { 3738, 16, 18, 18, 1, -18 }, +/* 0x8F */ { 3774, 13, 21, 17, 2, -18 }, +/* 0x90 */ { 3809, 12, 22, 14, 0, -18 }, +/* 0x91 */ { 3842, 2, 6, 6, 2, -18 }, +/* 0x92 */ { 3844, 2, 6, 6, 2, -18 }, +/* 0x93 */ { 3846, 6, 6, 10, 2, -18 }, +/* 0x94 */ { 3851, 6, 6, 10, 2, -18 }, +/* 0x95 */ { 3856, 6, 6, 10, 2, -11 }, +/* 0x96 */ { 3861, 10, 2, 12, 1, -8 }, +/* 0x97 */ { 3864, 22, 2, 24, 1, -8 }, +/* 0x98 */ { 3870, 0, 0, 8, 0, 0 }, +/* 0x99 */ { 3870, 22, 13, 24, 2, -18 }, +/* 0x9A */ { 3906, 17, 13, 19, 1, -13 }, +/* 0x9B */ { 3934, 3, 8, 6, 2, -10 }, +/* 0x9C */ { 3937, 18, 13, 20, 1, -13 }, +/* 0x9D */ { 3967, 9, 18, 12, 1, -18 }, +/* 0x9E */ { 3988, 11, 18, 14, 1, -18 }, +/* 0x9F */ { 4013, 10, 15, 13, 1, -13 }, +/* 0xA0 */ { 4032, 0, 0, 7, 0, 0 }, +/* 0xA1 */ { 4032, 13, 22, 15, 1, -22 }, +/* 0xA2 */ { 4068, 11, 22, 11, 0, -17 }, +/* 0xA3 */ { 4099, 9, 18, 13, 1, -18 }, +/* 0xA4 */ { 4120, 9, 9, 13, 2, -13 }, +/* 0xA5 */ { 4131, 11, 20, 14, 2, -20 }, +/* 0xA6 */ { 4159, 2, 23, 6, 2, -18 }, +/* 0xA7 */ { 4165, 11, 23, 13, 1, -18 }, +/* 0xA8 */ { 4197, 12, 21, 15, 2, -21 }, +/* 0xA9 */ { 4229, 18, 17, 19, 1, -17 }, +/* 0xAA */ { 4268, 15, 18, 17, 1, -18 }, +/* 0xAB */ { 4302, 8, 8, 12, 2, -11 }, +/* 0xAC */ { 4310, 12, 6, 14, 1, -9 }, +/* 0xAD */ { 4319, 6, 2, 8, 1, -8 }, +/* 0xAE */ { 4321, 18, 17, 19, 1, -17 }, +/* 0xAF */ { 4360, 7, 21, 7, 0, -21 }, +/* 0xB0 */ { 4379, 7, 8, 15, 4, -17 }, +/* 0xB1 */ { 4386, 12, 15, 14, 1, -15 }, +/* 0xB2 */ { 4409, 2, 18, 7, 2, -18 }, +/* 0xB3 */ { 4414, 2, 18, 5, 2, -18 }, +/* 0xB4 */ { 4419, 8, 15, 9, 1, -15 }, +/* 0xB5 */ { 4434, 12, 17, 13, 2, -13 }, +/* 0xB6 */ { 4460, 11, 21, 13, 2, -18 }, +/* 0xB7 */ { 4489, 2, 2, 6, 2, -8 }, +/* 0xB8 */ { 4490, 11, 17, 13, 1, -17 }, +/* 0xB9 */ { 4514, 19, 18, 22, 2, -18 }, +/* 0xBA */ { 4557, 11, 13, 12, 0, -13 }, +/* 0xBB */ { 4575, 8, 8, 12, 2, -10 }, +/* 0xBC */ { 4583, 4, 23, 6, 0, -18 }, +/* 0xBD */ { 4595, 14, 18, 16, 1, -18 }, +/* 0xBE */ { 4627, 10, 13, 12, 1, -13 }, +/* 0xBF */ { 4644, 6, 17, 6, 0, -17 }, +/* 0xC0 */ { 4657, 14, 18, 16, 1, -18 }, +/* 0xC1 */ { 4689, 13, 18, 16, 2, -18 }, +/* 0xC2 */ { 4719, 13, 18, 16, 2, -18 }, +/* 0xC3 */ { 4749, 11, 18, 14, 2, -18 }, +/* 0xC4 */ { 4774, 18, 21, 19, 1, -18 }, +/* 0xC5 */ { 4822, 12, 18, 15, 2, -18 }, +/* 0xC6 */ { 4849, 20, 18, 22, 1, -18 }, +/* 0xC7 */ { 4894, 13, 18, 16, 1, -18 }, +/* 0xC8 */ { 4924, 13, 18, 18, 2, -18 }, +/* 0xC9 */ { 4954, 13, 22, 18, 2, -22 }, +/* 0xCA */ { 4990, 12, 18, 15, 2, -18 }, +/* 0xCB */ { 5017, 14, 18, 16, 0, -18 }, +/* 0xCC */ { 5049, 16, 18, 20, 2, -18 }, +/* 0xCD */ { 5085, 13, 18, 17, 2, -18 }, +/* 0xCE */ { 5115, 17, 18, 19, 1, -18 }, +/* 0xCF */ { 5154, 13, 18, 17, 2, -18 }, +/* 0xD0 */ { 5184, 12, 18, 16, 2, -18 }, +/* 0xD1 */ { 5211, 15, 18, 17, 1, -18 }, +/* 0xD2 */ { 5245, 12, 18, 15, 1, -18 }, +/* 0xD3 */ { 5272, 13, 18, 15, 1, -18 }, +/* 0xD4 */ { 5302, 18, 18, 20, 1, -18 }, +/* 0xD5 */ { 5343, 14, 18, 16, 1, -18 }, +/* 0xD6 */ { 5375, 15, 21, 18, 2, -18 }, +/* 0xD7 */ { 5415, 12, 18, 15, 1, -18 }, +/* 0xD8 */ { 5442, 16, 18, 20, 2, -18 }, +/* 0xD9 */ { 5478, 18, 21, 20, 2, -18 }, +/* 0xDA */ { 5526, 18, 18, 20, 1, -18 }, +/* 0xDB */ { 5567, 17, 18, 21, 2, -18 }, +/* 0xDC */ { 5606, 13, 18, 16, 2, -18 }, +/* 0xDD */ { 5636, 15, 18, 17, 1, -18 }, +/* 0xDE */ { 5670, 21, 18, 24, 2, -18 }, +/* 0xDF */ { 5718, 14, 18, 16, 0, -18 }, +/* 0xE0 */ { 5750, 12, 13, 13, 1, -13 }, +/* 0xE1 */ { 5770, 11, 19, 13, 1, -19 }, +/* 0xE2 */ { 5797, 10, 13, 12, 1, -13 }, +/* 0xE3 */ { 5814, 7, 13, 9, 1, -13 }, +/* 0xE4 */ { 5826, 14, 15, 14, 0, -13 }, +/* 0xE5 */ { 5853, 11, 13, 13, 1, -13 }, +/* 0xE6 */ { 5871, 16, 13, 18, 1, -13 }, +/* 0xE7 */ { 5897, 9, 13, 12, 1, -13 }, +/* 0xE8 */ { 5912, 11, 13, 13, 1, -13 }, +/* 0xE9 */ { 5930, 11, 17, 13, 1, -17 }, +/* 0xEA */ { 5954, 9, 13, 12, 1, -13 }, +/* 0xEB */ { 5969, 10, 13, 12, 0, -13 }, +/* 0xEC */ { 5986, 12, 13, 14, 1, -13 }, +/* 0xED */ { 6006, 10, 13, 13, 1, -13 }, +/* 0xEE */ { 6023, 11, 13, 13, 1, -13 }, +/* 0xEF */ { 6041, 10, 13, 13, 1, -13 }, +/* 0xF0 */ { 6058, 12, 17, 13, 1, -13 }, +/* 0xF1 */ { 6084, 10, 13, 12, 1, -13 }, +/* 0xF2 */ { 6101, 8, 13, 10, 1, -13 }, +/* 0xF3 */ { 6114, 11, 18, 11, 0, -13 }, +/* 0xF4 */ { 6139, 19, 20, 20, 1, -16 }, +/* 0xF5 */ { 6187, 10, 13, 11, 1, -13 }, +/* 0xF6 */ { 6204, 12, 15, 13, 1, -13 }, +/* 0xF7 */ { 6227, 9, 13, 12, 1, -13 }, +/* 0xF8 */ { 6242, 14, 13, 16, 1, -13 }, +/* 0xF9 */ { 6265, 16, 15, 17, 1, -13 }, +/* 0xFA */ { 6295, 14, 13, 15, 1, -13 }, +/* 0xFB */ { 6318, 14, 13, 16, 1, -13 }, +/* 0xFC */ { 6341, 10, 13, 12, 1, -13 }, +/* 0xFD */ { 6358, 11, 13, 12, 1, -13 }, +/* 0xFE */ { 6376, 16, 13, 18, 1, -13 }, +/* 0xFF */ { 6402, 10, 13, 13, 1, -13 }, +}; + +const GFXfont FreeSans12pt_Win1251 PROGMEM = { +(uint8_t*)FreeSans12pt_Win1251Bitmaps, +(GFXglyph*)FreeSans12pt_Win1251Glyphs, +0x01, 0xFF, 19 +}; diff --git a/src/graphics/niche/Fonts/FreeSans12pt_Win1252.h b/src/graphics/niche/Fonts/FreeSans12pt_Win1252.h new file mode 100644 index 000000000..752925d6d --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans12pt_Win1252.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans12pt_Win1252 +*/ +const uint8_t FreeSans12pt_Win1252Bitmaps[] PROGMEM = { +/* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, +/* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, +/* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, +/* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, +/* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, +/* 0x0A */ +/* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, +/* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, +/* 0x0D */ +/* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, +/* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, +/* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, +/* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, +/* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, +/* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, +/* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, +/* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, +/* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, +/* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, +/* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, +/* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, +/* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, +/* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, +/* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, +/* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, +/* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, +/* ''' 0x27 */ 0xFF, 0xA0, +/* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, +/* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, +/* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, +/* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, +/* ',' 0x2C */ 0xF5, 0x60, +/* '-' 0x2D */ 0xFF, 0xF0, +/* '.' 0x2E */ 0xF0, +/* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, +/* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, +/* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, +/* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, +/* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, +/* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, +/* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, +/* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, +/* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, +/* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, +/* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, +/* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, +/* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, +/* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, +/* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, +/* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, +/* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, +/* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, +/* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, +/* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, +/* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, +/* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, +/* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, +/* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, +/* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, +/* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, +/* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, +/* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, +/* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, +/* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, +/* '_' 0x5F */ 0xFF, 0xFE, +/* '`' 0x60 */ 0xE3, 0x8C, 0x30, +/* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, +/* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, +/* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, +/* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, +/* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, +/* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, +/* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, +/* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, +/* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, +/* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, +/* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, +/* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, +/* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, +/* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, +/* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, +/* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, +/* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, +/* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, +/* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, +/* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, +/* 0x7F */ +/* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, +/* 0x81 */ +/* 0x82 */ 0xF5, 0x80, +/* 0x83 */ 0x1C, 0xF3, 0x0C, 0x31, 0xF7, 0xCC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0xCE, 0x00, +/* 0x84 */ 0xCF, 0x34, 0x51, 0x88, +/* 0x85 */ 0xC6, 0x3C, 0x63, +/* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0x88 */ 0x38, 0xD9, 0xB6, 0x30, +/* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, +/* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, +/* 0x8B */ 0x2F, 0x49, 0x99, +/* 0x8C */ 0x07, 0xCF, 0xFC, 0x7F, 0xFF, 0xF3, 0x83, 0xC0, 0x18, 0x07, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x30, 0x0C, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0F, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0x1F, 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, +/* 0x8D */ +/* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0x6A, 0xF0, +/* 0x92 */ 0xF5, 0x60, +/* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, +/* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, +/* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, +/* 0x96 */ 0xFF, 0xFF, 0xF0, +/* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 0x98 */ 0x63, 0xFE, 0x70, +/* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, +/* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, +/* 0x9B */ 0x99, 0x92, 0xF4, +/* 0x9C */ 0x1F, 0x0F, 0x83, 0xF9, 0xFC, 0x71, 0xF8, 0x6E, 0x0F, 0x03, 0xC0, 0x60, 0x3C, 0x07, 0xFF, 0xC0, 0x7F, 0xFC, 0x06, 0x00, 0xC0, 0x60, 0x0E, 0x0F, 0x03, 0x71, 0xF8, 0x63, 0xF9, 0xFC, 0x1F, 0x0F, 0x80, +/* 0x9D */ +/* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, +/* 0x9F */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x30, 0x03, 0xE0, 0x1D, 0x80, 0x67, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x80, 0xCC, 0x03, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, +/* 0xA0 */ +/* 0xA1 */ 0xF0, 0xBF, 0xFF, 0xFF, 0xF0, +/* 0xA2 */ 0x04, 0x00, 0x80, 0x7C, 0x1F, 0xE7, 0x4C, 0xC8, 0xF1, 0x1E, 0x20, 0xC4, 0x18, 0x83, 0x10, 0x72, 0x37, 0x4E, 0x7F, 0x87, 0xC0, 0x20, 0x04, 0x00, +/* 0xA3 */ 0x0F, 0xC1, 0xFE, 0x38, 0x76, 0x03, 0x60, 0x36, 0x00, 0x70, 0x03, 0x80, 0xFF, 0x0F, 0xF0, 0x1C, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x10, 0x02, 0xF1, 0x7F, 0xF6, 0x1F, +/* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, +/* 0xA5 */ 0xC0, 0x3E, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x19, 0x80, 0xF0, 0x0F, 0x07, 0xFE, 0x06, 0x00, 0x60, 0x7F, 0xE0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, +/* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, +/* 0xA8 */ 0xCF, 0x30, +/* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAA */ 0x79, 0x08, 0x11, 0xEE, 0x50, 0xA3, 0x3B, 0x00, 0x03, 0xF8, +/* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, +/* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, +/* 0xAD */ 0xFF, 0xF0, +/* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAF */ 0xFF, 0xF0, +/* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, +/* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xB2 */ 0x7D, 0x8F, 0x18, 0x30, 0xC6, 0x18, 0x60, 0xFF, 0xFC, +/* 0xB3 */ 0x7D, 0x8F, 0x18, 0x31, 0x80, 0xC1, 0xE3, 0xC6, 0xF8, +/* 0xB4 */ 0x3B, 0x99, 0x80, +/* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, +/* 0xB7 */ 0xF0, +/* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, +/* 0xB9 */ 0x2F, 0xB6, 0xDB, 0x6C, +/* 0xBA */ 0x79, 0x38, 0x61, 0x86, 0x1C, 0xDE, 0x00, 0x0F, 0xC0, +/* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, +/* 0xBC */ 0x20, 0x08, 0x30, 0x0C, 0x38, 0x04, 0x0C, 0x06, 0x06, 0x02, 0x03, 0x02, 0x01, 0x81, 0x00, 0xC1, 0x06, 0x61, 0x87, 0x30, 0x83, 0x80, 0xC2, 0xC0, 0x42, 0x60, 0x43, 0x30, 0x21, 0xFC, 0x20, 0x0C, 0x30, 0x06, 0x10, 0x03, 0x00, +/* 0xBD */ 0x20, 0x00, 0x08, 0x02, 0x06, 0x01, 0x83, 0x80, 0x40, 0x60, 0x20, 0x18, 0x18, 0x06, 0x04, 0x01, 0x83, 0x00, 0x61, 0x9F, 0x98, 0x4E, 0x76, 0x33, 0x0C, 0x08, 0x03, 0x04, 0x03, 0x83, 0x01, 0x80, 0x81, 0x80, 0x60, 0xC0, 0x30, 0x3F, 0xC8, 0x0F, 0xF0, +/* 0xBE */ 0x7C, 0x00, 0x18, 0xC0, 0x43, 0x18, 0x18, 0x03, 0x02, 0x00, 0x60, 0xC0, 0x30, 0x10, 0x01, 0x84, 0x00, 0x31, 0x80, 0xC6, 0x20, 0xD8, 0xC8, 0x39, 0xF1, 0x07, 0x00, 0x41, 0x60, 0x18, 0x4C, 0x02, 0x11, 0x80, 0x83, 0xF8, 0x10, 0x06, 0x04, 0x00, 0xC1, 0x00, 0x18, +/* 0xBF */ 0x0C, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x38, 0x38, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 0xC0 */ 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC1 */ 0x01, 0xC0, 0x0C, 0x00, 0x20, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC2 */ 0x07, 0x00, 0x3E, 0x01, 0x8C, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC3 */ 0x0E, 0x40, 0x7F, 0x01, 0x98, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 0xC4 */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xFC, 0x03, 0x30, 0x0C, 0xC0, 0x73, 0x81, 0x86, 0x06, 0x18, 0x38, 0x70, 0xC0, 0xC3, 0xFF, 0x1F, 0xFE, 0x60, 0x19, 0x80, 0x6E, 0x01, 0xF0, 0x03, 0xC0, 0x0C, +/* 0xC5 */ 0x03, 0x00, 0x1E, 0x00, 0xEC, 0x03, 0x30, 0x0F, 0xC0, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x03, 0x30, 0x1C, 0xE0, 0x61, 0x81, 0x86, 0x0E, 0x1C, 0x30, 0x30, 0xFF, 0xC7, 0xFF, 0x98, 0x06, 0x60, 0x1B, 0x80, 0x7C, 0x00, 0xF0, 0x03, +/* 0xC6 */ 0x01, 0xFF, 0xFC, 0x07, 0xFF, 0xF0, 0x31, 0x80, 0x00, 0xC6, 0x00, 0x07, 0x18, 0x00, 0x18, 0x60, 0x00, 0x61, 0x80, 0x03, 0x86, 0x00, 0x0C, 0x1F, 0xF8, 0x70, 0x7F, 0xE1, 0x81, 0x80, 0x07, 0xFE, 0x00, 0x3F, 0xF8, 0x00, 0xC0, 0x60, 0x07, 0x01, 0x80, 0x1C, 0x06, 0x00, 0x60, 0x1F, 0xFF, 0x80, 0x7F, 0xF0, +/* 0xC7 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x66, 0x00, 0x7C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x01, 0xDC, 0x03, 0x1C, 0x1E, 0x1F, 0xF8, 0x0F, 0xC0, 0x08, 0x00, 0x1E, 0x00, 0x0C, 0x01, 0x18, 0x01, 0xE0, 0x00, +/* 0xC8 */ 0x1C, 0x00, 0xC0, 0x02, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 0xC9 */ 0x07, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 0xCA */ 0x0E, 0x01, 0xF0, 0x31, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 0xCB */ 0x19, 0x81, 0x98, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xCC */ 0xE7, 0x10, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, +/* 0xCD */ 0x36, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, +/* 0xCE */ 0x39, 0xFC, 0x40, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xCF */ 0xC7, 0x8C, 0x01, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x00, +/* 0xD0 */ 0x7F, 0xE0, 0xFF, 0xE1, 0x80, 0xE3, 0x00, 0xE6, 0x00, 0xCC, 0x01, 0xD8, 0x01, 0xB0, 0x03, 0xFE, 0x07, 0xFC, 0x0D, 0x80, 0x1B, 0x00, 0x36, 0x00, 0x6C, 0x01, 0x98, 0x07, 0x30, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, +/* 0xD1 */ 0x08, 0xC0, 0xFE, 0x05, 0xE0, 0x00, 0x0E, 0x01, 0xF0, 0x0F, 0xC0, 0x7E, 0x03, 0xD8, 0x1E, 0xE0, 0xF3, 0x07, 0x9C, 0x3C, 0x61, 0xE1, 0x8F, 0x0E, 0x78, 0x33, 0xC1, 0xDE, 0x06, 0xF0, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1C, +/* 0xD2 */ 0x07, 0x00, 0x01, 0x80, 0x00, 0x60, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD3 */ 0x00, 0xE0, 0x00, 0x60, 0x00, 0x40, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD4 */ 0x03, 0xC0, 0x01, 0xE0, 0x01, 0x98, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD5 */ 0x07, 0x20, 0x03, 0xF0, 0x01, 0x38, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, +/* 0xD6 */ 0x06, 0x30, 0x03, 0x18, 0x00, 0x00, 0x00, 0xFE, 0x01, 0xFF, 0xC1, 0xE0, 0xF0, 0xC0, 0x18, 0xC0, 0x06, 0x60, 0x03, 0x60, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x06, 0xC0, 0x06, 0x60, 0x03, 0x18, 0x03, 0x0F, 0x07, 0x83, 0xFF, 0x80, 0x7F, 0x00, +/* 0xD7 */ 0x81, 0xC3, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0xC3, 0x81, +/* 0xD8 */ 0x07, 0xF0, 0x8F, 0xFE, 0x8F, 0x07, 0xC6, 0x00, 0xE6, 0x00, 0xF3, 0x00, 0xDF, 0x00, 0xC7, 0x80, 0xC3, 0xC0, 0xC1, 0xE0, 0xC0, 0xF0, 0xC0, 0x78, 0xC0, 0x3E, 0xC0, 0x33, 0xC0, 0x19, 0xC0, 0x1C, 0xF8, 0x3C, 0xDF, 0xF8, 0x43, 0xF8, 0x00, +/* 0xD9 */ 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, +/* 0xDA */ 0x03, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, +/* 0xDB */ 0x07, 0x00, 0x7C, 0x06, 0x20, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, +/* 0xDC */ 0x0C, 0xC0, 0x66, 0x00, 0x01, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1F, 0x01, 0xDC, 0x1C, 0x7F, 0xC1, 0xF8, 0x00, +/* 0xDD */ 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x00, 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 0xDE */ 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xF8, 0xFF, 0xEC, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x6F, 0xFE, 0xFF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 0xDF */ 0x1F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x66, 0x38, 0xC7, 0xD8, 0x1B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x3E, 0x0E, 0xCF, 0x99, 0xE0, +/* 0xE0 */ 0x1C, 0x00, 0xC0, 0x06, 0x00, 0x20, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE1 */ 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE2 */ 0x0C, 0x01, 0xE0, 0x1B, 0x03, 0x30, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE3 */ 0x19, 0x83, 0xF0, 0x27, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, +/* 0xE4 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0x00, 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 0xE5 */ 0x0E, 0x01, 0xF0, 0x1B, 0x81, 0xB8, 0x1F, 0x00, 0xE0, 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 0xE6 */ 0x3F, 0x1F, 0x0F, 0xF7, 0xF3, 0x87, 0xC3, 0x60, 0x70, 0x30, 0x0C, 0x06, 0x3F, 0xFF, 0xDF, 0xFF, 0xFF, 0x06, 0x00, 0xC0, 0xC0, 0x18, 0x3C, 0x0F, 0x8F, 0xC7, 0x3F, 0x9F, 0xE3, 0xC1, 0xF0, +/* 0xE7 */ 0x1F, 0x0F, 0xE7, 0x1D, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x02, 0x00, 0xE0, 0x0C, 0x23, 0x07, 0x80, +/* 0xE8 */ 0x1C, 0x01, 0x80, 0x18, 0x01, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, +/* 0xE9 */ 0x03, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, +/* 0xEA */ 0x0C, 0x03, 0xC0, 0x6C, 0x18, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, +/* 0xEB */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x03, 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0C, 0x01, 0xC0, 0xDC, 0x31, 0xFC, 0x1F, 0x00, +/* 0xEC */ 0xC6, 0x31, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, +/* 0xED */ 0x39, 0x99, 0x80, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x80, +/* 0xEE */ 0x71, 0xED, 0xA3, 0x00, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xEF */ 0xCF, 0x30, 0x00, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xF0 */ 0x20, 0x07, 0xE0, 0x70, 0x3B, 0x00, 0x30, 0x3F, 0x0F, 0xF3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF1 */ 0x19, 0x8F, 0xE2, 0x70, 0x00, 0x00, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 0xF2 */ 0x1C, 0x01, 0x80, 0x18, 0x01, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF3 */ 0x07, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF4 */ 0x0C, 0x03, 0xC0, 0xD8, 0x19, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF5 */ 0x19, 0x87, 0xE0, 0x9C, 0x00, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, +/* 0xF6 */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* 0xF7 */ 0x06, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, +/* 0xF8 */ 0x1F, 0x27, 0xF5, 0xC7, 0x70, 0x7C, 0x17, 0x84, 0xF1, 0x1E, 0x43, 0xD0, 0x7C, 0x1D, 0xC7, 0x3F, 0xC9, 0xF0, +/* 0xF9 */ 0x18, 0x03, 0x00, 0x60, 0x08, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, +/* 0xFA */ 0x03, 0x01, 0x80, 0x60, 0x30, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, +/* 0xFB */ 0x0C, 0x07, 0x81, 0x20, 0xCC, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, +/* 0xFC */ 0x31, 0x8C, 0x60, 0x00, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 0xFD */ 0x03, 0x80, 0x60, 0x18, 0x06, 0x00, 0x01, 0xC0, 0xD8, 0x1B, 0x06, 0x70, 0xC6, 0x18, 0xC6, 0x18, 0xC1, 0x98, 0x36, 0x06, 0xC0, 0x78, 0x0E, 0x01, 0xC0, 0x30, 0x06, 0x01, 0xC0, 0xF0, 0x1C, 0x00, +/* 0xFE */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xCF, 0x8F, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xFF */ 0x19, 0x83, 0x30, 0x00, 0x00, 0x0E, 0x06, 0xC0, 0xD8, 0x33, 0x86, 0x30, 0xC6, 0x30, 0xC6, 0x0C, 0xC1, 0xB0, 0x36, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0x30, 0x0E, 0x07, 0x80, 0xE0, 0x00, +}; + +const GFXglyph FreeSans12pt_Win1252Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 19, 20, 21, 1, -17 }, +/* 0x02 */ { 48, 19, 20, 21, 1, -17 }, +/* 0x03 */ { 96, 21, 20, 23, 1, -17 }, +/* 0x04 */ { 149, 21, 20, 23, 1, -17 }, +/* 0x05 */ { 202, 20, 20, 22, 1, -17 }, +/* 0x06 */ { 252, 20, 20, 22, 1, -17 }, +/* 0x07 */ { 302, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 302, 23, 20, 25, 1, -17 }, +/* 0x09 */ { 360, 23, 16, 25, 1, -16 }, +/* 0x0A */ { 406, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 406, 21, 20, 23, 1, -17 }, +/* 0x0C */ { 459, 19, 18, 21, 1, -15 }, +/* 0x0D */ { 502, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 502, 20, 20, 22, 1, -17 }, +/* 0x0F */ { 552, 21, 21, 23, 1, -18 }, +/* 0x10 */ { 608, 19, 20, 21, 1, -17 }, +/* 0x11 */ { 656, 21, 20, 23, 1, -17 }, +/* 0x12 */ { 709, 20, 20, 22, 1, -17 }, +/* 0x13 */ { 759, 21, 20, 23, 1, -17 }, +/* 0x14 */ { 812, 21, 20, 23, 1, -17 }, +/* 0x15 */ { 865, 22, 20, 24, 1, -17 }, +/* 0x16 */ { 920, 16, 20, 18, 1, -17 }, +/* 0x17 */ { 960, 21, 20, 23, 1, -17 }, +/* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, +/* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, +/* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, +/* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, +/* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, +/* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, +/* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, +/* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, +/* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, +/* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, +/* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, +/* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, +/* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, +/* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, +/* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, +/* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, +/* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, +/* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, +/* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, +/* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, +/* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, +/* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, +/* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, +/* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, +/* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, +/* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, +/* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, +/* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, +/* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, +/* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, +/* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, +/* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, +/* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, +/* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, +/* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, +/* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, +/* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, +/* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, +/* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, +/* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, +/* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, +/* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, +/* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, +/* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, +/* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, +/* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, +/* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, +/* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, +/* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, +/* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, +/* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, +/* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, +/* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, +/* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, +/* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, +/* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, +/* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, +/* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, +/* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, +/* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, +/* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, +/* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, +/* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, +/* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, +/* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, +/* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, +/* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, +/* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, +/* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, +/* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, +/* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, +/* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, +/* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, +/* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, +/* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, +/* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, +/* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, +/* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, +/* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, +/* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, +/* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, +/* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, +/* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, +/* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, +/* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, +/* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, +/* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, +/* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, +/* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, +/* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, +/* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, +/* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, +/* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, +/* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, +/* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, +/* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, +/* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, +/* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, +/* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, +/* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, +/* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, +/* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, +/* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, +/* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 3368, 14, 17, 16, 1, -15 }, +/* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 3398, 2, 5, 6, 2, 0 }, +/* 0x83 */ { 3400, 6, 23, 7, 0, -16 }, +/* 0x84 */ { 3418, 6, 5, 10, 2, 0 }, +/* 0x85 */ { 3422, 12, 2, 16, 2, 0 }, +/* 0x86 */ { 3425, 10, 21, 13, 2, -15 }, +/* 0x87 */ { 3452, 10, 20, 13, 2, -15 }, +/* 0x88 */ { 3477, 7, 4, 8, 0, -16 }, +/* 0x89 */ { 3481, 23, 18, 24, 0, -16 }, +/* 0x8A */ { 3533, 14, 21, 16, 1, -19 }, +/* 0x8B */ { 3570, 3, 8, 6, 1, -9 }, +/* 0x8C */ { 3573, 22, 18, 24, 1, -16 }, +/* 0x8D */ { 3623, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 3623, 13, 21, 15, 1, -19 }, +/* 0x8F */ { 3658, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 3658, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 3658, 2, 6, 6, 2, -16 }, +/* 0x92 */ { 3660, 2, 6, 6, 2, -16 }, +/* 0x93 */ { 3662, 6, 6, 10, 2, -16 }, +/* 0x94 */ { 3667, 6, 6, 10, 2, -16 }, +/* 0x95 */ { 3672, 6, 6, 10, 2, -9 }, +/* 0x96 */ { 3677, 10, 2, 12, 1, -6 }, +/* 0x97 */ { 3680, 22, 2, 24, 1, -6 }, +/* 0x98 */ { 3686, 7, 3, 8, 0, -16 }, +/* 0x99 */ { 3689, 22, 13, 24, 2, -16 }, +/* 0x9A */ { 3725, 10, 18, 12, 1, -16 }, +/* 0x9B */ { 3748, 3, 8, 6, 2, -8 }, +/* 0x9C */ { 3751, 20, 13, 22, 1, -11 }, +/* 0x9D */ { 3784, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 3784, 10, 18, 12, 1, -16 }, +/* 0x9F */ { 3807, 14, 21, 16, 1, -19 }, +/* 0xA0 */ { 3844, 0, 0, 7, 0, 0 }, +/* 0xA1 */ { 3844, 2, 18, 8, 3, -11 }, +/* 0xA2 */ { 3849, 11, 17, 13, 1, -13 }, +/* 0xA3 */ { 3873, 12, 18, 13, 0, -16 }, +/* 0xA4 */ { 3900, 9, 9, 13, 2, -11 }, +/* 0xA5 */ { 3911, 12, 17, 13, 1, -15 }, +/* 0xA6 */ { 3937, 2, 23, 6, 2, -16 }, +/* 0xA7 */ { 3943, 11, 23, 13, 1, -16 }, +/* 0xA8 */ { 3975, 6, 2, 8, 1, -15 }, +/* 0xA9 */ { 3977, 18, 17, 19, 1, -15 }, +/* 0xAA */ { 4016, 7, 11, 9, 1, -16 }, +/* 0xAB */ { 4026, 8, 8, 12, 2, -9 }, +/* 0xAC */ { 4034, 12, 6, 14, 1, -7 }, +/* 0xAD */ { 4043, 6, 2, 8, 1, -6 }, +/* 0xAE */ { 4045, 18, 17, 19, 1, -15 }, +/* 0xAF */ { 4084, 6, 2, 8, 1, -15 }, +/* 0xB0 */ { 4086, 7, 8, 15, 4, -15 }, +/* 0xB1 */ { 4093, 12, 15, 14, 1, -13 }, +/* 0xB2 */ { 4116, 7, 10, 8, 1, -17 }, +/* 0xB3 */ { 4125, 7, 10, 8, 1, -17 }, +/* 0xB4 */ { 4134, 5, 4, 8, 2, -16 }, +/* 0xB5 */ { 4137, 12, 17, 13, 2, -11 }, +/* 0xB6 */ { 4163, 11, 21, 13, 2, -16 }, +/* 0xB7 */ { 4192, 2, 2, 6, 2, -6 }, +/* 0xB8 */ { 4193, 6, 5, 8, 1, 2 }, +/* 0xB9 */ { 4197, 3, 10, 8, 3, -18 }, +/* 0xBA */ { 4201, 6, 11, 9, 1, -16 }, +/* 0xBB */ { 4210, 8, 8, 12, 2, -8 }, +/* 0xBC */ { 4218, 17, 17, 21, 3, -15 }, +/* 0xBD */ { 4255, 18, 18, 21, 3, -16 }, +/* 0xBE */ { 4296, 19, 18, 21, 1, -16 }, +/* 0xBF */ { 4339, 9, 18, 13, 3, -11 }, +/* 0xC0 */ { 4360, 14, 22, 16, 1, -20 }, +/* 0xC1 */ { 4399, 14, 22, 16, 1, -20 }, +/* 0xC2 */ { 4438, 14, 22, 16, 1, -20 }, +/* 0xC3 */ { 4477, 14, 22, 16, 1, -20 }, +/* 0xC4 */ { 4516, 14, 21, 16, 1, -19 }, +/* 0xC5 */ { 4553, 14, 24, 16, 1, -22 }, +/* 0xC6 */ { 4595, 22, 18, 24, 0, -16 }, +/* 0xC7 */ { 4645, 15, 23, 17, 1, -16 }, +/* 0xC8 */ { 4689, 12, 22, 15, 2, -20 }, +/* 0xC9 */ { 4722, 12, 22, 15, 2, -20 }, +/* 0xCA */ { 4755, 12, 22, 15, 2, -20 }, +/* 0xCB */ { 4788, 12, 21, 15, 2, -19 }, +/* 0xCC */ { 4820, 4, 22, 7, 0, -20 }, +/* 0xCD */ { 4831, 4, 22, 7, 1, -20 }, +/* 0xCE */ { 4842, 6, 22, 7, 0, -20 }, +/* 0xCF */ { 4859, 7, 21, 7, 0, -19 }, +/* 0xD0 */ { 4878, 15, 18, 17, 1, -16 }, +/* 0xD1 */ { 4912, 13, 22, 18, 2, -20 }, +/* 0xD2 */ { 4948, 17, 22, 19, 1, -20 }, +/* 0xD3 */ { 4995, 17, 22, 19, 1, -20 }, +/* 0xD4 */ { 5042, 17, 22, 19, 1, -20 }, +/* 0xD5 */ { 5089, 17, 22, 19, 1, -20 }, +/* 0xD6 */ { 5136, 17, 21, 19, 1, -19 }, +/* 0xD7 */ { 5181, 8, 9, 14, 3, -8 }, +/* 0xD8 */ { 5190, 17, 18, 19, 1, -16 }, +/* 0xD9 */ { 5229, 13, 22, 17, 2, -20 }, +/* 0xDA */ { 5265, 13, 22, 17, 2, -20 }, +/* 0xDB */ { 5301, 13, 22, 17, 2, -20 }, +/* 0xDC */ { 5337, 13, 21, 17, 2, -19 }, +/* 0xDD */ { 5372, 14, 22, 16, 1, -20 }, +/* 0xDE */ { 5411, 12, 18, 15, 2, -16 }, +/* 0xDF */ { 5438, 11, 18, 14, 2, -16 }, +/* 0xE0 */ { 5463, 12, 18, 13, 1, -16 }, +/* 0xE1 */ { 5490, 12, 18, 13, 1, -16 }, +/* 0xE2 */ { 5517, 12, 18, 13, 1, -16 }, +/* 0xE3 */ { 5544, 12, 18, 13, 1, -16 }, +/* 0xE4 */ { 5571, 12, 17, 13, 1, -15 }, +/* 0xE5 */ { 5597, 12, 19, 13, 1, -17 }, +/* 0xE6 */ { 5626, 19, 13, 21, 1, -11 }, +/* 0xE7 */ { 5657, 10, 18, 12, 1, -11 }, +/* 0xE8 */ { 5680, 11, 18, 13, 1, -16 }, +/* 0xE9 */ { 5705, 11, 18, 13, 1, -16 }, +/* 0xEA */ { 5730, 11, 18, 13, 1, -16 }, +/* 0xEB */ { 5755, 11, 17, 13, 1, -15 }, +/* 0xEC */ { 5779, 4, 18, 5, 1, -16 }, +/* 0xED */ { 5788, 5, 18, 5, 0, -16 }, +/* 0xEE */ { 5800, 6, 18, 6, 0, -16 }, +/* 0xEF */ { 5814, 6, 17, 6, 0, -15 }, +/* 0xF0 */ { 5827, 11, 18, 13, 1, -16 }, +/* 0xF1 */ { 5852, 10, 18, 13, 1, -16 }, +/* 0xF2 */ { 5875, 11, 18, 13, 1, -16 }, +/* 0xF3 */ { 5900, 11, 18, 13, 1, -16 }, +/* 0xF4 */ { 5925, 11, 18, 13, 1, -16 }, +/* 0xF5 */ { 5950, 11, 18, 13, 1, -16 }, +/* 0xF6 */ { 5975, 11, 17, 13, 1, -15 }, +/* 0xF7 */ { 5999, 12, 11, 14, 1, -9 }, +/* 0xF8 */ { 6016, 11, 13, 13, 1, -11 }, +/* 0xF9 */ { 6034, 10, 18, 13, 1, -16 }, +/* 0xFA */ { 6057, 10, 18, 13, 1, -16 }, +/* 0xFB */ { 6080, 10, 18, 13, 1, -16 }, +/* 0xFC */ { 6103, 10, 17, 13, 1, -15 }, +/* 0xFD */ { 6125, 11, 23, 11, 0, -16 }, +/* 0xFE */ { 6157, 12, 21, 13, 1, -15 }, +/* 0xFF */ { 6189, 11, 22, 11, 0, -15 }, +}; + +const GFXfont FreeSans12pt_Win1252 PROGMEM = { +(uint8_t*)FreeSans12pt_Win1252Bitmaps, +(GFXglyph*)FreeSans12pt_Win1252Glyphs, +0x01, 0xFF, 19 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h index aee777783..4042bd835 100644 --- a/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h @@ -1,457 +1,527 @@ +// trunk-ignore-all(clang-format) #pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1250 +*/ const uint8_t FreeSans6pt_Win1250Bitmaps[] PROGMEM = { - /* ' ' 0x20 */ - 0xFC, 0x80, /* '!' 0x21 */ - 0xB6, 0x80, /* '"' 0x22 */ - 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '#' 0x23 */ - 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '$' 0x24 */ - 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '%' 0x25 */ - 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* '&' 0x26 */ - 0xE0, /* ''' 0x27 */ - 0x5A, 0xAA, 0x94, /* '(' 0x28 */ - 0x89, 0x12, 0x49, 0x29, 0x00, /* ')' 0x29 */ - 0x5E, 0x80, /* '*' 0x2A */ - 0x21, 0x3E, 0x42, 0x00, /* '+' 0x2B */ - 0xE0, /* ',' 0x2C */ - 0xC0, /* '-' 0x2D */ - 0x80, /* '.' 0x2E */ - 0x24, 0xA4, 0xA4, 0x80, /* '/' 0x2F */ - 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '0' 0x30 */ - 0x27, 0x92, 0x49, 0x20, /* '1' 0x31 */ - 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '2' 0x32 */ - 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '3' 0x33 */ - 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '4' 0x34 */ - 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '5' 0x35 */ - 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '6' 0x36 */ - 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '7' 0x37 */ - 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '8' 0x38 */ - 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* '9' 0x39 */ - 0x82, /* ':' 0x3A */ - 0x87, /* ';' 0x3B */ - 0x3E, 0x30, 0x60, 0x80, /* '<' 0x3C */ - 0xF8, 0x3E, /* '=' 0x3D */ - 0xE0, 0xC6, 0xC8, 0x00, /* '>' 0x3E */ - 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '?' 0x3F */ - 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* '@' 0x40 */ - 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'A' 0x41 */ - 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'B' 0x42 */ - 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'C' 0x43 */ - 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'D' 0x44 */ - 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'E' 0x45 */ - 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'F' 0x46 */ - 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'G' 0x47 */ - 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'H' 0x48 */ - 0xFF, 0x80, /* 'I' 0x49 */ - 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'J' 0x4A */ - 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'K' 0x4B */ - 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'L' 0x4C */ - 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'M' 0x4D */ - 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'N' 0x4E */ - 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'O' 0x4F */ - 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'P' 0x50 */ - 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'Q' 0x51 */ - 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'R' 0x52 */ - 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'S' 0x53 */ - 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'T' 0x54 */ - 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'U' 0x55 */ - 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'V' 0x56 */ - 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'W' 0x57 */ - 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'X' 0x58 */ - 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Y' 0x59 */ - 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* 'Z' 0x5A */ - 0xEA, 0xAA, 0xAB, /* '[' 0x5B */ - 0x92, 0x24, 0x89, 0x20, /* '\' 0x5C */ - 0xD5, 0x55, 0x57, /* ']' 0x5D */ - 0x46, 0xA9, /* '^' 0x5E */ - 0xFE, /* '_' 0x5F */ - 0x80, /* '`' 0x60 */ - 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'a' 0x61 */ - 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'b' 0x62 */ - 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'c' 0x63 */ - 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'd' 0x64 */ - 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'e' 0x65 */ - 0x6B, 0xA4, 0x92, 0x40, /* 'f' 0x66 */ - 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'g' 0x67 */ - 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'h' 0x68 */ - 0xBF, 0x80, /* 'i' 0x69 */ - 0x45, 0x55, 0x57, /* 'j' 0x6A */ - 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'k' 0x6B */ - 0xFF, 0x80, /* 'l' 0x6C */ - 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'm' 0x6D */ - 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'n' 0x6E */ - 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'o' 0x6F */ - 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'p' 0x70 */ - 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'q' 0x71 */ - 0xF2, 0x49, 0x20, /* 'r' 0x72 */ - 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 's' 0x73 */ - 0x5D, 0x24, 0x93, /* 't' 0x74 */ - 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'u' 0x75 */ - 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'v' 0x76 */ - 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'w' 0x77 */ - 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'x' 0x78 */ - 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'y' 0x79 */ - 0x78, 0x44, 0x46, 0x23, 0xE0, /* 'z' 0x7A */ - 0x6A, 0xAA, 0xA9, /* '{' 0x7B */ - 0xFF, 0xE0, /* '|' 0x7C */ - 0x95, 0x55, 0x56, /* '}' 0x7D */ - 0x66, 0x60, /* '~' 0x7E */ - 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x7F */ - 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x80 */ - /* 0x81 */ - 0xE0, /* 0x82 */ - /* 0x83 */ - 0xB6, 0x80, /* 0x84 */ - 0xA8, /* 0x85 */ - 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x86 */ - 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x87 */ - /* 0x88 */ - 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x89 */ - 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8A */ - 0x64, /* 0x8B */ - 0x10, 0x87, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8C */ - 0x28, 0x4F, 0xC4, 0x10, 0x41, 0x04, 0x10, 0x40, /* 0x8D */ - 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x8E */ - 0x08, 0x21, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x8F */ - /* 0x90 */ - 0xE0, /* 0x91 */ - 0xE0, /* 0x92 */ - 0xB6, 0x80, /* 0x93 */ - 0xB6, 0x80, /* 0x94 */ - 0xFF, 0x80, /* 0x95 */ - 0xFC, /* 0x96 */ - 0xFF, 0xF0, /* 0x97 */ - /* 0x98 */ - 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x99 */ - 0x52, 0x69, 0x8E, 0x19, 0x60, /* 0x9A */ - 0x98, /* 0x9B */ - 0x24, 0x06, 0x98, 0xE1, 0x96, /* 0x9C */ - 0x15, 0xE4, 0x44, 0x44, 0x60, /* 0x9D */ - 0x51, 0x00, 0xF0, 0x88, 0x8C, 0x47, 0xC0, /* 0x9E */ - 0x11, 0x00, 0xF0, 0x88, 0x8C, 0x47, 0xC0, /* 0x9F */ - /* 0xA0 */ - 0xA8, /* 0xA1 */ - 0x96, /* 0xA2 */ - 0x41, 0x05, 0x18, 0x43, 0x04, 0x10, 0x7C, /* 0xA3 */ - 0xFC, 0x63, 0xF0, /* 0xA4 */ - 0x30, 0x38, 0x28, 0x48, 0x4C, 0x7C, 0x84, 0x86, 0x82, 0x04, 0x07, /* 0xA5 */ - 0xF9, 0xF0, /* 0xA6 */ - 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA7 */ - 0xA0, /* 0xA8 */ - 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xA9 */ - 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xC1, 0x0C, /* 0xAA */ - 0x5A, 0xA5, /* 0xAB */ - 0xFC, 0x10, 0x40, /* 0xAC */ - /* 0xAD */ - 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAE */ - 0x18, 0x31, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0xAF */ - 0x69, 0x96, /* 0xB0 */ - 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB1 */ - 0x9C, /* 0xB2 */ - 0x49, 0x35, 0x92, 0x40, /* 0xB3 */ - 0x80, /* 0xB4 */ - 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB5 */ - 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB6 */ - 0x80, /* 0xB7 */ - 0x67, 0x80, /* 0xB8 */ - 0x78, 0x84, 0x04, 0x3C, 0xC4, 0x8C, 0x76, 0x04, 0x07, /* 0xB9 */ - 0x69, 0x8E, 0x19, 0x66, 0x26, /* 0xBA */ - 0xA5, 0x5A, /* 0xBB */ - 0xA5, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 0xBC */ - 0xA0, /* 0xBD */ - 0xBA, 0x49, 0x24, 0x90, /* 0xBE */ - 0x31, 0x9E, 0x11, 0x11, 0x88, 0xF8, /* 0xBF */ - 0x10, 0x43, 0xE4, 0x28, 0x50, 0xBE, 0x42, 0x85, 0x0C, /* 0xC0 */ - 0x08, 0x10, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC1 */ - 0x18, 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC2 */ - 0x24, 0x18, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC3 */ - 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 0xC4 */ - 0x11, 0x21, 0x08, 0x42, 0x10, 0x87, 0xC0, /* 0xC5 */ - 0x08, 0x20, 0x01, 0xE4, 0x30, 0x20, 0x40, 0x82, 0x8C, 0xF0, /* 0xC6 */ - 0x3E, 0x61, 0xC0, 0x80, 0x80, 0x80, 0xC1, 0x63, 0x3E, 0x0C, 0x04, 0x1C, /* 0xC7 */ - 0x28, 0x20, 0x01, 0xE4, 0x30, 0x20, 0x40, 0x82, 0x8C, 0xF0, /* 0xC8 */ - 0x08, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xC9 */ - 0xFD, 0x02, 0x04, 0x0F, 0xD0, 0x20, 0x40, 0xFC, 0x10, 0x38, /* 0xCA */ - 0x28, 0x0F, 0xE0, 0x83, 0xE8, 0x20, 0x83, 0xF0, /* 0xCB */ - 0x28, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x82, 0x0F, 0xC0, /* 0xCC */ - 0x62, 0xAA, 0xA0, /* 0xCD */ - 0x54, 0x24, 0x92, 0x48, /* 0xCE */ - 0x50, 0x43, 0xE4, 0x28, 0x30, 0x60, 0xC1, 0x85, 0xF0, /* 0xCF */ - 0x7C, 0x42, 0x41, 0x41, 0xF1, 0x41, 0x41, 0x42, 0x7C, /* 0xD0 */ - 0x08, 0x23, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, /* 0xD1 */ - 0x28, 0x23, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, /* 0xD2 */ - 0x04, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, /* 0xD3 */ - 0x08, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD4 */ - 0x0A, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD5 */ - 0x14, 0x00, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD6 */ - 0x8A, 0x88, 0xA8, 0x80, /* 0xD7 */ - 0x50, 0x43, 0xE4, 0x28, 0x50, 0xBE, 0x42, 0x85, 0x0C, /* 0xD8 */ - 0x10, 0x52, 0x4C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xD9 */ - 0x08, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDA */ - 0x14, 0x52, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDB */ - 0x29, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDC */ - 0x09, 0x25, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0xDD */ - 0xFC, 0x41, 0x04, 0x10, 0x41, 0x04, 0x10, 0x60, 0x8E, /* 0xDE */ - 0x7A, 0x18, 0x61, 0x8A, 0x18, 0x61, 0xB8, /* 0xDF */ - 0x42, 0xE9, 0x24, 0x80, /* 0xE0 */ - 0x10, 0x40, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE1 */ - 0x10, 0x50, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE2 */ - 0x48, 0x60, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE3 */ - 0x28, 0x01, 0xE4, 0x20, 0x47, 0xB1, 0x46, 0x76, /* 0xE4 */ - 0x62, 0xAA, 0xA0, /* 0xE5 */ - 0x10, 0x80, 0x1E, 0xC6, 0x08, 0x20, 0xC5, 0xE0, /* 0xE6 */ - 0x7B, 0x18, 0x20, 0x83, 0x17, 0x8C, 0x11, 0xC0, /* 0xE7 */ - 0x28, 0x40, 0x1E, 0xC6, 0x08, 0x20, 0xC5, 0xE0, /* 0xE8 */ - 0x10, 0x80, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xE9 */ - 0x7B, 0x38, 0x7F, 0x83, 0x37, 0x84, 0x1C, /* 0xEA */ - 0x28, 0x07, 0xB3, 0x87, 0xF8, 0x31, 0x78, /* 0xEB */ - 0x28, 0x40, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xEC */ - 0x62, 0xAA, 0xA0, /* 0xED */ - 0x54, 0x24, 0x92, 0x48, /* 0xEE */ - 0x02, 0x0C, 0x13, 0xEC, 0xD0, 0xA1, 0x42, 0xCC, 0xE8, /* 0xEF */ - 0x04, 0x1D, 0xD6, 0x68, 0x50, 0xA1, 0x66, 0x74, /* 0xF0 */ - 0x11, 0x01, 0x6C, 0xC6, 0x31, 0x8C, 0x40, /* 0xF1 */ - 0x20, 0x81, 0x6C, 0xC6, 0x31, 0x8C, 0x40, /* 0xF2 */ - 0x10, 0x80, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF3 */ - 0x10, 0xA0, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF4 */ - 0x29, 0x40, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF5 */ - 0x28, 0x07, 0xB3, 0x86, 0x18, 0x73, 0x78, /* 0xF6 */ - 0x20, 0x3E, 0x02, 0x00, /* 0xF7 */ - 0xA8, 0x5D, 0x24, 0x90, /* 0xF8 */ - 0x22, 0x89, 0x18, 0xC6, 0x31, 0x9B, 0x40, /* 0xF9 */ - 0x11, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFA */ - 0x2A, 0x81, 0x18, 0xC6, 0x31, 0x9B, 0x40, /* 0xFB */ - 0x50, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFC */ - 0x10, 0x88, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, /* 0xFD */ - 0x4E, 0x44, 0x44, 0x46, 0x31, 0x70, /* 0xFE */ - 0x80, /* 0xFF */ +/* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, +/* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, +/* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, +/* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, +/* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, +/* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, +/* 0x07 */ +/* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, +/* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, +/* 0x0A */ +/* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, +/* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, +/* 0x0D */ +/* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, +/* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, +/* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, +/* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, +/* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, +/* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, +/* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, +/* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, +/* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, +/* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, +/* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, +/* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, +/* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, +/* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, +/* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, +/* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, +/* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, +/* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFC, 0x80, +/* '"' 0x22 */ 0xB6, 0x80, +/* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, +/* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, +/* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, +/* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, +/* ''' 0x27 */ 0xE0, +/* '(' 0x28 */ 0x5A, 0xAA, 0x94, +/* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, +/* '*' 0x2A */ 0x5E, 0x80, +/* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, +/* ',' 0x2C */ 0xE0, +/* '-' 0x2D */ 0xC0, +/* '.' 0x2E */ 0x80, +/* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, +/* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, +/* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, +/* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, +/* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, +/* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, +/* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, +/* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, +/* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, +/* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, +/* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, +/* ':' 0x3A */ 0x82, +/* ';' 0x3B */ 0x87, +/* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, +/* '=' 0x3D */ 0xF8, 0x3E, +/* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, +/* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, +/* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, +/* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, +/* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, +/* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, +/* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, +/* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, +/* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 'I' 0x49 */ 0xFF, 0x80, +/* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, +/* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, +/* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, +/* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, +/* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, +/* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, +/* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, +/* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, +/* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, +/* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, +/* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, +/* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, +/* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, +/* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, +/* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, +/* '[' 0x5B */ 0xEA, 0xAA, 0xAB, +/* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, +/* ']' 0x5D */ 0xD5, 0x55, 0x57, +/* '^' 0x5E */ 0x46, 0xA9, +/* '_' 0x5F */ 0xFE, +/* '`' 0x60 */ 0x80, +/* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, +/* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, +/* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, +/* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, +/* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, +/* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, +/* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, +/* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, +/* 'i' 0x69 */ 0xBF, 0x80, +/* 'j' 0x6A */ 0x45, 0x55, 0x57, +/* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, +/* 'l' 0x6C */ 0xFF, 0x80, +/* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, +/* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, +/* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, +/* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, +/* 'r' 0x72 */ 0xF2, 0x49, 0x20, +/* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, +/* 't' 0x74 */ 0x5D, 0x24, 0x93, +/* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, +/* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, +/* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, +/* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, +/* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, +/* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, +/* '{' 0x7B */ 0x6A, 0xAA, 0xA9, +/* '|' 0x7C */ 0xFF, 0xE0, +/* '}' 0x7D */ 0x95, 0x55, 0x56, +/* '~' 0x7E */ 0x66, 0x60, +/* 0x7F */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, +/* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, +/* 0x81 */ +/* 0x82 */ 0xE0, +/* 0x83 */ +/* 0x84 */ 0xB6, 0x80, +/* 0x85 */ 0xA8, +/* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, +/* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, +/* 0x88 */ +/* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, +/* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, +/* 0x8B */ 0x64, +/* 0x8C */ 0x10, 0x87, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, +/* 0x8D */ 0x28, 0x4F, 0xC4, 0x10, 0x41, 0x04, 0x10, 0x40, +/* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, +/* 0x8F */ 0x08, 0x21, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, +/* 0x90 */ +/* 0x91 */ 0xE0, +/* 0x92 */ 0xE0, +/* 0x93 */ 0xB6, 0x80, +/* 0x94 */ 0xB6, 0x80, +/* 0x95 */ 0xFF, 0x80, +/* 0x96 */ 0xFC, +/* 0x97 */ 0xFF, 0xF0, +/* 0x98 */ +/* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, +/* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, +/* 0x9B */ 0x98, +/* 0x9C */ 0x24, 0x06, 0x98, 0xE1, 0x96, +/* 0x9D */ 0x15, 0xE4, 0x44, 0x44, 0x60, +/* 0x9E */ 0x51, 0x00, 0xF0, 0x88, 0x8C, 0x47, 0xC0, +/* 0x9F */ 0x11, 0x00, 0xF0, 0x88, 0x8C, 0x47, 0xC0, +/* 0xA0 */ +/* 0xA1 */ 0xA8, +/* 0xA2 */ 0x96, +/* 0xA3 */ 0x41, 0x05, 0x18, 0x43, 0x04, 0x10, 0x7C, +/* 0xA4 */ 0xFC, 0x63, 0xF0, +/* 0xA5 */ 0x30, 0x38, 0x28, 0x48, 0x4C, 0x7C, 0x84, 0x86, 0x82, 0x04, 0x07, +/* 0xA6 */ 0xF9, 0xF0, +/* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, +/* 0xA8 */ 0xA0, +/* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, +/* 0xAA */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xC1, 0x0C, +/* 0xAB */ 0x5A, 0xA5, +/* 0xAC */ 0xFC, 0x10, 0x40, +/* 0xAD */ +/* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, +/* 0xAF */ 0x18, 0x31, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, +/* 0xB0 */ 0x69, 0x96, +/* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, +/* 0xB2 */ 0x9C, +/* 0xB3 */ 0x49, 0x35, 0x92, 0x40, +/* 0xB4 */ 0x80, +/* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, +/* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, +/* 0xB7 */ 0x80, +/* 0xB8 */ 0x67, 0x80, +/* 0xB9 */ 0x78, 0x84, 0x04, 0x3C, 0xC4, 0x8C, 0x76, 0x04, 0x07, +/* 0xBA */ 0x69, 0x8E, 0x19, 0x66, 0x26, +/* 0xBB */ 0xA5, 0x5A, +/* 0xBC */ 0xA5, 0x21, 0x08, 0x42, 0x10, 0xF8, +/* 0xBD */ 0xA0, +/* 0xBE */ 0xBA, 0x49, 0x24, 0x90, +/* 0xBF */ 0x31, 0x9E, 0x11, 0x11, 0x88, 0xF8, +/* 0xC0 */ 0x10, 0x43, 0xE4, 0x28, 0x50, 0xBE, 0x42, 0x85, 0x0C, +/* 0xC1 */ 0x08, 0x10, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC2 */ 0x18, 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC3 */ 0x24, 0x18, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC4 */ 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 0xC5 */ 0x11, 0x21, 0x08, 0x42, 0x10, 0x87, 0xC0, +/* 0xC6 */ 0x08, 0x20, 0x01, 0xE4, 0x30, 0x20, 0x40, 0x82, 0x8C, 0xF0, +/* 0xC7 */ 0x3E, 0x61, 0xC0, 0x80, 0x80, 0x80, 0xC1, 0x63, 0x3E, 0x0C, 0x04, 0x1C, +/* 0xC8 */ 0x28, 0x20, 0x01, 0xE4, 0x30, 0x20, 0x40, 0x82, 0x8C, 0xF0, +/* 0xC9 */ 0x08, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, +/* 0xCA */ 0xFD, 0x02, 0x04, 0x0F, 0xD0, 0x20, 0x40, 0xFC, 0x10, 0x38, +/* 0xCB */ 0x28, 0x0F, 0xE0, 0x83, 0xE8, 0x20, 0x83, 0xF0, +/* 0xCC */ 0x28, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x82, 0x0F, 0xC0, +/* 0xCD */ 0x62, 0xAA, 0xA0, +/* 0xCE */ 0x54, 0x24, 0x92, 0x48, +/* 0xCF */ 0x50, 0x43, 0xE4, 0x28, 0x30, 0x60, 0xC1, 0x85, 0xF0, +/* 0xD0 */ 0x7C, 0x42, 0x41, 0x41, 0xF1, 0x41, 0x41, 0x42, 0x7C, +/* 0xD1 */ 0x08, 0x23, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, +/* 0xD2 */ 0x28, 0x23, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, +/* 0xD3 */ 0x04, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, +/* 0xD4 */ 0x08, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, +/* 0xD5 */ 0x0A, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, +/* 0xD6 */ 0x14, 0x00, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, +/* 0xD7 */ 0x8A, 0x88, 0xA8, 0x80, +/* 0xD8 */ 0x50, 0x43, 0xE4, 0x28, 0x50, 0xBE, 0x42, 0x85, 0x0C, +/* 0xD9 */ 0x10, 0x52, 0x4C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDA */ 0x08, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDB */ 0x14, 0x52, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDC */ 0x29, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDD */ 0x09, 0x25, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, +/* 0xDE */ 0xFC, 0x41, 0x04, 0x10, 0x41, 0x04, 0x10, 0x60, 0x8E, +/* 0xDF */ 0x7A, 0x18, 0x61, 0x8A, 0x18, 0x61, 0xB8, +/* 0xE0 */ 0x42, 0xE9, 0x24, 0x80, +/* 0xE1 */ 0x10, 0x40, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE2 */ 0x10, 0x50, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE3 */ 0x48, 0x60, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE4 */ 0x28, 0x01, 0xE4, 0x20, 0x47, 0xB1, 0x46, 0x76, +/* 0xE5 */ 0x62, 0xAA, 0xA0, +/* 0xE6 */ 0x10, 0x80, 0x1E, 0xC6, 0x08, 0x20, 0xC5, 0xE0, +/* 0xE7 */ 0x7B, 0x18, 0x20, 0x83, 0x17, 0x8C, 0x11, 0xC0, +/* 0xE8 */ 0x28, 0x40, 0x1E, 0xC6, 0x08, 0x20, 0xC5, 0xE0, +/* 0xE9 */ 0x10, 0x80, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, +/* 0xEA */ 0x7B, 0x38, 0x7F, 0x83, 0x37, 0x84, 0x1C, +/* 0xEB */ 0x28, 0x07, 0xB3, 0x87, 0xF8, 0x31, 0x78, +/* 0xEC */ 0x28, 0x40, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, +/* 0xED */ 0x62, 0xAA, 0xA0, +/* 0xEE */ 0x54, 0x24, 0x92, 0x48, +/* 0xEF */ 0x02, 0x0C, 0x13, 0xEC, 0xD0, 0xA1, 0x42, 0xCC, 0xE8, +/* 0xF0 */ 0x04, 0x1D, 0xD6, 0x68, 0x50, 0xA1, 0x66, 0x74, +/* 0xF1 */ 0x11, 0x01, 0x6C, 0xC6, 0x31, 0x8C, 0x40, +/* 0xF2 */ 0x20, 0x81, 0x6C, 0xC6, 0x31, 0x8C, 0x40, +/* 0xF3 */ 0x10, 0x80, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF4 */ 0x10, 0xA0, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF5 */ 0x29, 0x40, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF6 */ 0x28, 0x07, 0xB3, 0x86, 0x18, 0x73, 0x78, +/* 0xF7 */ 0x20, 0x3E, 0x02, 0x00, +/* 0xF8 */ 0xA8, 0x5D, 0x24, 0x90, +/* 0xF9 */ 0x22, 0x89, 0x18, 0xC6, 0x31, 0x9B, 0x40, +/* 0xFA */ 0x11, 0x23, 0x18, 0xC6, 0x33, 0x68, +/* 0xFB */ 0x2A, 0x81, 0x18, 0xC6, 0x31, 0x9B, 0x40, +/* 0xFC */ 0x50, 0x23, 0x18, 0xC6, 0x33, 0x68, +/* 0xFD */ 0x10, 0x88, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, +/* 0xFE */ 0x4E, 0x44, 0x44, 0x46, 0x31, 0x70, +/* 0xFF */ 0x80, }; const GFXglyph FreeSans6pt_Win1250Glyphs[] PROGMEM = { - /* ' ' 0x20 */ {0, 0, 0, 3, 0, 0}, - /* '!' 0x21 */ {0, 1, 9, 4, 2, -8}, - /* '"' 0x22 */ {2, 3, 3, 4, 0, -8}, - /* '#' 0x23 */ {4, 7, 8, 7, 0, -7}, - /* '$' 0x24 */ {11, 6, 11, 7, 0, -9}, - /* '%' 0x25 */ {20, 10, 9, 11, 0, -8}, - /* '&' 0x26 */ {32, 6, 9, 8, 1, -8}, - /* ''' 0x27 */ {39, 1, 3, 2, 1, -8}, - /* '(' 0x28 */ {40, 2, 11, 4, 1, -8}, - /* ')' 0x29 */ {43, 3, 11, 4, 0, -8}, - /* '*' 0x2A */ {48, 3, 3, 5, 1, -8}, - /* '+' 0x2B */ {50, 5, 5, 7, 1, -4}, - /* ',' 0x2C */ {54, 1, 3, 3, 1, 0}, - /* '-' 0x2D */ {55, 2, 1, 4, 1, -3}, - /* '.' 0x2E */ {56, 1, 1, 3, 1, 0}, - /* '/' 0x2F */ {57, 3, 9, 3, 0, -8}, - /* '0' 0x30 */ {61, 5, 9, 7, 1, -8}, - /* '1' 0x31 */ {67, 3, 9, 7, 1, -8}, - /* '2' 0x32 */ {71, 6, 9, 7, 0, -8}, - /* '3' 0x33 */ {78, 6, 9, 7, 0, -8}, - /* '4' 0x34 */ {85, 6, 9, 7, 0, -8}, - /* '5' 0x35 */ {92, 5, 9, 7, 1, -8}, - /* '6' 0x36 */ {98, 5, 9, 7, 1, -8}, - /* '7' 0x37 */ {104, 5, 9, 7, 1, -8}, - /* '8' 0x38 */ {110, 6, 9, 7, 0, -8}, - /* '9' 0x39 */ {117, 6, 9, 7, 0, -8}, - /* ':' 0x3A */ {124, 1, 7, 3, 1, -6}, - /* ';' 0x3B */ {125, 1, 8, 3, 1, -5}, - /* '<' 0x3C */ {126, 5, 5, 7, 1, -4}, - /* '=' 0x3D */ {130, 5, 3, 7, 1, -3}, - /* '>' 0x3E */ {132, 5, 5, 7, 1, -4}, - /* '?' 0x3F */ {136, 5, 9, 7, 1, -8}, - /* '@' 0x40 */ {142, 11, 11, 12, 0, -8}, - /* 'A' 0x41 */ {158, 8, 9, 8, 0, -8}, - /* 'B' 0x42 */ {167, 6, 9, 8, 1, -8}, - /* 'C' 0x43 */ {174, 8, 9, 9, 0, -8}, - /* 'D' 0x44 */ {183, 7, 9, 8, 1, -8}, - /* 'E' 0x45 */ {191, 6, 9, 8, 1, -8}, - /* 'F' 0x46 */ {198, 6, 9, 7, 1, -8}, - /* 'G' 0x47 */ {205, 8, 9, 9, 0, -8}, - /* 'H' 0x48 */ {214, 7, 9, 9, 1, -8}, - /* 'I' 0x49 */ {222, 1, 9, 3, 1, -8}, - /* 'J' 0x4A */ {224, 5, 9, 6, 0, -8}, - /* 'K' 0x4B */ {230, 7, 9, 8, 1, -8}, - /* 'L' 0x4C */ {238, 5, 9, 7, 1, -8}, - /* 'M' 0x4D */ {244, 8, 9, 10, 1, -8}, - /* 'N' 0x4E */ {253, 7, 9, 9, 1, -8}, - /* 'O' 0x4F */ {261, 9, 9, 9, 0, -8}, - /* 'P' 0x50 */ {272, 6, 9, 8, 1, -8}, - /* 'Q' 0x51 */ {279, 9, 10, 9, 0, -8}, - /* 'R' 0x52 */ {291, 7, 9, 9, 1, -8}, - /* 'S' 0x53 */ {299, 6, 9, 8, 1, -8}, - /* 'T' 0x54 */ {306, 7, 9, 8, 0, -8}, - /* 'U' 0x55 */ {314, 7, 9, 9, 1, -8}, - /* 'V' 0x56 */ {322, 7, 9, 8, 0, -8}, - /* 'W' 0x57 */ {330, 11, 9, 11, 0, -8}, - /* 'X' 0x58 */ {343, 6, 9, 8, 1, -8}, - /* 'Y' 0x59 */ {350, 8, 9, 8, 0, -8}, - /* 'Z' 0x5A */ {359, 7, 9, 7, 0, -8}, - /* '[' 0x5B */ {367, 2, 12, 3, 1, -8}, - /* '\' 0x5C */ {370, 3, 9, 3, 0, -8}, - /* ']' 0x5D */ {374, 2, 12, 3, 0, -8}, - /* '^' 0x5E */ {377, 4, 4, 6, 1, -8}, - /* '_' 0x5F */ {379, 7, 1, 7, 0, 2}, - /* '`' 0x60 */ {380, 1, 1, 3, 1, -8}, - /* 'a' 0x61 */ {381, 6, 7, 7, 0, -6}, - /* 'b' 0x62 */ {387, 5, 9, 7, 1, -8}, - /* 'c' 0x63 */ {393, 6, 7, 6, 0, -6}, - /* 'd' 0x64 */ {399, 6, 9, 7, 0, -8}, - /* 'e' 0x65 */ {406, 6, 7, 6, 0, -6}, - /* 'f' 0x66 */ {412, 3, 9, 3, 0, -8}, - /* 'g' 0x67 */ {416, 6, 10, 7, 0, -6}, - /* 'h' 0x68 */ {424, 5, 9, 6, 1, -8}, - /* 'i' 0x69 */ {430, 1, 9, 3, 1, -8}, - /* 'j' 0x6A */ {432, 2, 12, 3, 0, -8}, - /* 'k' 0x6B */ {435, 5, 9, 6, 1, -8}, - /* 'l' 0x6C */ {441, 1, 9, 3, 1, -8}, - /* 'm' 0x6D */ {443, 8, 7, 10, 1, -6}, - /* 'n' 0x6E */ {450, 5, 7, 6, 1, -6}, - /* 'o' 0x6F */ {455, 6, 7, 6, 0, -6}, - /* 'p' 0x70 */ {461, 5, 9, 7, 1, -6}, - /* 'q' 0x71 */ {467, 6, 9, 7, 0, -6}, - /* 'r' 0x72 */ {474, 3, 7, 4, 1, -6}, - /* 's' 0x73 */ {477, 5, 7, 6, 0, -6}, - /* 't' 0x74 */ {482, 3, 8, 3, 0, -7}, - /* 'u' 0x75 */ {485, 5, 7, 6, 1, -6}, - /* 'v' 0x76 */ {490, 6, 7, 6, 0, -6}, - /* 'w' 0x77 */ {496, 8, 7, 9, 0, -6}, - /* 'x' 0x78 */ {503, 5, 7, 6, 0, -6}, - /* 'y' 0x79 */ {508, 5, 10, 6, 0, -6}, - /* 'z' 0x7A */ {515, 5, 7, 6, 0, -6}, - /* '{' 0x7B */ {520, 2, 12, 4, 1, -8}, - /* '|' 0x7C */ {523, 1, 11, 3, 1, -8}, - /* '}' 0x7D */ {525, 2, 12, 4, 1, -8}, - /* '~' 0x7E */ {528, 6, 2, 6, 0, -4}, - /* 0x7F */ {530, 9, 10, 11, 1, -8}, - /* 0x80 */ {542, 7, 9, 8, 0, -8}, - /* 0x81 */ {550, 0, 0, 8, 0, 0}, - /* 0x82 */ {550, 1, 3, 3, 1, 0}, - /* 0x83 */ {551, 0, 0, 8, 0, 0}, - /* 0x84 */ {551, 3, 3, 5, 1, 0}, - /* 0x85 */ {553, 5, 1, 7, 1, 0}, - /* 0x86 */ {554, 5, 11, 7, 1, -8}, - /* 0x87 */ {561, 5, 11, 7, 1, -8}, - /* 0x88 */ {568, 0, 0, 8, 0, 0}, - /* 0x89 */ {568, 12, 9, 12, 0, -8}, - /* 0x8A */ {582, 6, 11, 8, 1, -9}, - /* 0x8B */ {591, 2, 3, 4, 1, -4}, - /* 0x8C */ {592, 6, 11, 8, 1, -10}, - /* 0x8D */ {601, 6, 10, 8, 0, -9}, - /* 0x8E */ {609, 7, 10, 7, 0, -9}, - /* 0x8F */ {618, 7, 10, 7, 0, -9}, - /* 0x90 */ {627, 0, 0, 8, 0, 0}, - /* 0x91 */ {627, 1, 3, 3, 1, -8}, - /* 0x92 */ {628, 1, 3, 2, 1, -8}, - /* 0x93 */ {629, 3, 3, 5, 1, -8}, - /* 0x94 */ {631, 3, 3, 5, 1, -8}, - /* 0x95 */ {633, 3, 3, 5, 1, -5}, - /* 0x96 */ {635, 6, 1, 6, 0, -3}, - /* 0x97 */ {636, 12, 1, 12, 0, -3}, - /* 0x98 */ {638, 0, 0, 8, 0, 0}, - /* 0x99 */ {638, 11, 7, 12, 1, -8}, - /* 0x9A */ {648, 4, 9, 6, 1, -8}, - /* 0x9B */ {653, 2, 3, 3, 1, -4}, - /* 0x9C */ {654, 4, 10, 6, 1, -9}, - /* 0x9D */ {659, 4, 9, 5, 0, -8}, - /* 0x9E */ {664, 5, 10, 6, 0, -9}, - /* 0x9F */ {671, 5, 10, 6, 0, -9}, - /* 0xA0 */ {678, 0, 0, 3, 0, 0}, - /* 0xA1 */ {678, 3, 2, 4, 0, -8}, - /* 0xA2 */ {679, 4, 2, 4, 0, -8}, - /* 0xA3 */ {680, 6, 9, 7, 0, -8}, - /* 0xA4 */ {687, 5, 4, 7, 1, -5}, - /* 0xA5 */ {690, 8, 11, 8, 1, -8}, - /* 0xA6 */ {701, 1, 12, 3, 1, -8}, - /* 0xA7 */ {703, 5, 12, 7, 1, -8}, - /* 0xA8 */ {711, 3, 1, 4, 0, -7}, - /* 0xA9 */ {712, 9, 9, 10, 0, -8}, - /* 0xAA */ {723, 6, 12, 8, 1, -8}, - /* 0xAB */ {732, 4, 4, 6, 1, -4}, - /* 0xAC */ {734, 6, 3, 7, 1, -4}, - /* 0xAD */ {737, 0, 0, 0, 0, 0}, - /* 0xAE */ {737, 9, 9, 10, 0, -8}, - /* 0xAF */ {748, 7, 10, 7, 0, -9}, - /* 0xB0 */ {757, 4, 4, 7, 2, -8}, - /* 0xB1 */ {759, 5, 7, 7, 1, -6}, - /* 0xB2 */ {764, 3, 2, 4, 1, 1}, - /* 0xB3 */ {765, 3, 9, 3, 0, -8}, - /* 0xB4 */ {769, 1, 1, 4, 1, -8}, - /* 0xB5 */ {770, 6, 9, 7, 1, -6}, - /* 0xB6 */ {777, 6, 10, 6, 1, -8}, - /* 0xB7 */ {785, 1, 1, 3, 1, -2}, - /* 0xB8 */ {786, 3, 3, 4, 1, 1}, - /* 0xB9 */ {788, 8, 9, 7, 0, -6}, - /* 0xBA */ {797, 4, 10, 6, 1, -6}, - /* 0xBB */ {802, 4, 4, 6, 1, -5}, - /* 0xBC */ {804, 5, 9, 7, 1, -8}, - /* 0xBD */ {810, 3, 1, 4, 0, -8}, - /* 0xBE */ {811, 3, 10, 3, 1, -9}, - /* 0xBF */ {815, 5, 9, 6, 0, -8}, - /* 0xC0 */ {821, 7, 10, 9, 1, -9}, - /* 0xC1 */ {830, 8, 10, 8, 0, -9}, - /* 0xC2 */ {840, 8, 10, 8, 0, -9}, - /* 0xC3 */ {850, 8, 10, 8, 0, -9}, - /* 0xC4 */ {860, 8, 10, 8, 0, -9}, - /* 0xC5 */ {870, 5, 10, 7, 1, -9}, - /* 0xC6 */ {877, 7, 11, 9, 0, -10}, - /* 0xC7 */ {887, 8, 12, 9, 0, -8}, - /* 0xC8 */ {899, 7, 11, 9, 0, -10}, - /* 0xC9 */ {909, 6, 10, 8, 1, -9}, - /* 0xCA */ {917, 7, 11, 8, 1, -8}, - /* 0xCB */ {927, 6, 10, 8, 1, -9}, - /* 0xCC */ {935, 6, 11, 8, 1, -10}, - /* 0xCD */ {944, 2, 10, 3, 1, -9}, - /* 0xCE */ {947, 3, 10, 4, 0, -9}, - /* 0xCF */ {951, 7, 10, 8, 1, -9}, - /* 0xD0 */ {960, 8, 9, 8, 0, -8}, - /* 0xD1 */ {969, 7, 10, 9, 1, -9}, - /* 0xD2 */ {978, 7, 10, 9, 1, -9}, - /* 0xD3 */ {987, 9, 10, 9, 0, -9}, - /* 0xD4 */ {999, 9, 11, 9, 0, -10}, - /* 0xD5 */ {1012, 9, 11, 9, 0, -10}, - /* 0xD6 */ {1025, 9, 11, 9, 0, -10}, - /* 0xD7 */ {1038, 5, 5, 7, 1, -5}, - /* 0xD8 */ {1042, 7, 10, 9, 1, -9}, - /* 0xD9 */ {1051, 7, 10, 9, 1, -9}, - /* 0xDA */ {1060, 7, 10, 9, 1, -9}, - /* 0xDB */ {1069, 7, 10, 9, 1, -9}, - /* 0xDC */ {1078, 7, 10, 9, 1, -9}, - /* 0xDD */ {1087, 7, 10, 8, 1, -9}, - /* 0xDE */ {1096, 6, 12, 7, 0, -8}, - /* 0xDF */ {1105, 6, 9, 7, 1, -8}, - /* 0xE0 */ {1112, 3, 9, 4, 1, -8}, - /* 0xE1 */ {1116, 7, 10, 7, 0, -9}, - /* 0xE2 */ {1125, 7, 10, 7, 0, -9}, - /* 0xE3 */ {1134, 7, 10, 7, 0, -9}, - /* 0xE4 */ {1143, 7, 9, 7, 0, -8}, - /* 0xE5 */ {1151, 2, 10, 3, 1, -9}, - /* 0xE6 */ {1154, 6, 10, 6, 0, -9}, - /* 0xE7 */ {1162, 6, 10, 6, 0, -6}, - /* 0xE8 */ {1170, 6, 10, 6, 0, -9}, - /* 0xE9 */ {1178, 6, 10, 6, 0, -9}, - /* 0xEA */ {1186, 6, 9, 6, 0, -6}, - /* 0xEB */ {1193, 6, 9, 6, 0, -8}, - /* 0xEC */ {1200, 6, 10, 6, 0, -9}, - /* 0xED */ {1208, 2, 10, 3, 1, -9}, - /* 0xEE */ {1211, 3, 10, 3, 0, -9}, - /* 0xEF */ {1215, 7, 10, 7, 0, -9}, - /* 0xF0 */ {1224, 7, 9, 7, 0, -8}, - /* 0xF1 */ {1232, 5, 10, 6, 1, -9}, - /* 0xF2 */ {1239, 5, 10, 6, 1, -9}, - /* 0xF3 */ {1246, 6, 10, 6, 0, -9}, - /* 0xF4 */ {1254, 6, 10, 6, 0, -9}, - /* 0xF5 */ {1262, 6, 10, 6, 0, -9}, - /* 0xF6 */ {1270, 6, 9, 6, 0, -8}, - /* 0xF7 */ {1277, 5, 5, 7, 1, -5}, - /* 0xF8 */ {1281, 3, 10, 4, 1, -9}, - /* 0xF9 */ {1285, 5, 10, 6, 1, -9}, - /* 0xFA */ {1292, 5, 9, 6, 1, -8}, - /* 0xFB */ {1298, 5, 10, 6, 1, -9}, - /* 0xFC */ {1305, 5, 9, 6, 1, -8}, - /* 0xFD */ {1311, 6, 12, 6, 0, -8}, - /* 0xFE */ {1320, 4, 11, 3, 0, -7}, - /* 0xFF */ {1326, 1, 1, 4, 1, -7}, +/* 0x01 */ { 0, 9, 10, 11, 1, -9 }, +/* 0x02 */ { 12, 9, 10, 11, 1, -8 }, +/* 0x03 */ { 24, 10, 10, 12, 1, -8 }, +/* 0x04 */ { 37, 10, 10, 12, 1, -8 }, +/* 0x05 */ { 50, 10, 10, 12, 1, -9 }, +/* 0x06 */ { 63, 11, 11, 13, 1, -9 }, +/* 0x07 */ { 79, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 79, 12, 9, 14, 1, -8 }, +/* 0x09 */ { 93, 14, 8, 16, 1, -7 }, +/* 0x0A */ { 107, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 107, 9, 10, 11, 1, -9 }, +/* 0x0C */ { 119, 13, 9, 15, 1, -8 }, +/* 0x0D */ { 134, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 134, 9, 11, 11, 1, -9 }, +/* 0x0F */ { 147, 10, 10, 12, 1, -9 }, +/* 0x10 */ { 160, 11, 10, 13, 1, -9 }, +/* 0x11 */ { 174, 13, 10, 15, 1, -9 }, +/* 0x12 */ { 191, 10, 10, 12, 1, -9 }, +/* 0x13 */ { 204, 11, 10, 13, 1, -9 }, +/* 0x14 */ { 218, 10, 10, 12, 1, -9 }, +/* 0x15 */ { 231, 14, 10, 16, 1, -9 }, +/* 0x16 */ { 249, 8, 10, 10, 1, -9 }, +/* 0x17 */ { 259, 12, 10, 14, 1, -9 }, +/* 0x18 */ { 274, 13, 10, 15, 1, -9 }, +/* 0x19 */ { 291, 12, 10, 14, 1, -9 }, +/* 0x1A */ { 306, 9, 10, 11, 1, -8 }, +/* 0x1B */ { 318, 14, 10, 16, 1, -9 }, +/* 0x1C */ { 336, 11, 10, 13, 1, -9 }, +/* 0x1D */ { 350, 11, 10, 13, 1, -9 }, +/* 0x1E */ { 364, 12, 10, 14, 1, -9 }, +/* 0x1F */ { 379, 8, 10, 11, 2, -9 }, +/* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, +/* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, +/* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, +/* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, +/* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, +/* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, +/* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, +/* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, +/* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, +/* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, +/* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, +/* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, +/* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, +/* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, +/* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, +/* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, +/* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, +/* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, +/* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, +/* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, +/* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, +/* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, +/* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, +/* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, +/* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, +/* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, +/* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, +/* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, +/* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, +/* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, +/* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, +/* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, +/* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, +/* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, +/* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, +/* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, +/* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, +/* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, +/* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, +/* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, +/* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, +/* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, +/* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, +/* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, +/* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, +/* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, +/* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, +/* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, +/* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, +/* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, +/* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, +/* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, +/* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, +/* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, +/* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, +/* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, +/* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, +/* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, +/* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, +/* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, +/* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, +/* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, +/* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, +/* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, +/* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, +/* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, +/* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, +/* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, +/* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, +/* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, +/* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, +/* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, +/* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, +/* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, +/* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, +/* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, +/* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, +/* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, +/* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, +/* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, +/* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, +/* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, +/* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, +/* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, +/* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, +/* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, +/* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, +/* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, +/* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, +/* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, +/* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, +/* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, +/* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, +/* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, +/* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, +/* 0x7F */ { 919, 9, 10, 0, 1, -8 }, +/* 0x80 */ { 931, 7, 9, 8, 0, -8 }, +/* 0x81 */ { 939, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 939, 1, 3, 3, 1, 0 }, +/* 0x83 */ { 940, 0, 0, 8, 0, 0 }, +/* 0x84 */ { 940, 3, 3, 5, 1, 0 }, +/* 0x85 */ { 942, 5, 1, 7, 1, 0 }, +/* 0x86 */ { 943, 5, 11, 7, 1, -8 }, +/* 0x87 */ { 950, 5, 11, 7, 1, -8 }, +/* 0x88 */ { 957, 0, 0, 8, 0, 0 }, +/* 0x89 */ { 957, 12, 9, 12, 0, -8 }, +/* 0x8A */ { 971, 6, 11, 8, 1, -9 }, +/* 0x8B */ { 980, 2, 3, 4, 1, -4 }, +/* 0x8C */ { 981, 6, 11, 8, 1, -10 }, +/* 0x8D */ { 990, 6, 10, 8, 0, -9 }, +/* 0x8E */ { 998, 7, 10, 7, 0, -9 }, +/* 0x8F */ { 1007, 7, 10, 7, 0, -9 }, +/* 0x90 */ { 1016, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 1016, 1, 3, 3, 1, -8 }, +/* 0x92 */ { 1017, 1, 3, 2, 1, -8 }, +/* 0x93 */ { 1018, 3, 3, 5, 1, -8 }, +/* 0x94 */ { 1020, 3, 3, 5, 1, -8 }, +/* 0x95 */ { 1022, 3, 3, 5, 1, -5 }, +/* 0x96 */ { 1024, 6, 1, 6, 0, -3 }, +/* 0x97 */ { 1025, 12, 1, 12, 0, -3 }, +/* 0x98 */ { 1027, 0, 0, 8, 0, 0 }, +/* 0x99 */ { 1027, 11, 7, 12, 1, -8 }, +/* 0x9A */ { 1037, 4, 9, 6, 1, -8 }, +/* 0x9B */ { 1042, 2, 3, 3, 1, -4 }, +/* 0x9C */ { 1043, 4, 10, 6, 1, -9 }, +/* 0x9D */ { 1048, 4, 9, 5, 0, -8 }, +/* 0x9E */ { 1053, 5, 10, 6, 0, -9 }, +/* 0x9F */ { 1060, 5, 10, 6, 0, -9 }, +/* 0xA0 */ { 1067, 0, 0, 3, 0, 0 }, +/* 0xA1 */ { 1067, 3, 2, 4, 0, -8 }, +/* 0xA2 */ { 1068, 4, 2, 4, 0, -8 }, +/* 0xA3 */ { 1069, 6, 9, 7, 0, -8 }, +/* 0xA4 */ { 1076, 5, 4, 7, 1, -5 }, +/* 0xA5 */ { 1079, 8, 11, 8, 1, -8 }, +/* 0xA6 */ { 1090, 1, 12, 3, 1, -8 }, +/* 0xA7 */ { 1092, 5, 12, 7, 1, -8 }, +/* 0xA8 */ { 1100, 3, 1, 4, 0, -7 }, +/* 0xA9 */ { 1101, 9, 9, 10, 0, -8 }, +/* 0xAA */ { 1112, 6, 12, 8, 1, -8 }, +/* 0xAB */ { 1121, 4, 4, 6, 1, -4 }, +/* 0xAC */ { 1123, 6, 3, 7, 1, -4 }, +/* 0xAD */ { 1126, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 1126, 9, 9, 10, 0, -8 }, +/* 0xAF */ { 1137, 7, 10, 7, 0, -9 }, +/* 0xB0 */ { 1146, 4, 4, 7, 2, -8 }, +/* 0xB1 */ { 1148, 5, 7, 7, 1, -6 }, +/* 0xB2 */ { 1153, 3, 2, 4, 1, 1 }, +/* 0xB3 */ { 1154, 3, 9, 3, 0, -8 }, +/* 0xB4 */ { 1158, 1, 1, 4, 1, -8 }, +/* 0xB5 */ { 1159, 6, 9, 7, 1, -6 }, +/* 0xB6 */ { 1166, 6, 10, 6, 1, -8 }, +/* 0xB7 */ { 1174, 1, 1, 3, 1, -2 }, +/* 0xB8 */ { 1175, 3, 3, 4, 1, 1 }, +/* 0xB9 */ { 1177, 8, 9, 7, 0, -6 }, +/* 0xBA */ { 1186, 4, 10, 6, 1, -6 }, +/* 0xBB */ { 1191, 4, 4, 6, 1, -5 }, +/* 0xBC */ { 1193, 5, 9, 7, 1, -8 }, +/* 0xBD */ { 1199, 3, 1, 4, 0, -8 }, +/* 0xBE */ { 1200, 3, 10, 3, 1, -9 }, +/* 0xBF */ { 1204, 5, 9, 6, 0, -8 }, +/* 0xC0 */ { 1210, 7, 10, 9, 1, -9 }, +/* 0xC1 */ { 1219, 8, 10, 8, 0, -9 }, +/* 0xC2 */ { 1229, 8, 10, 8, 0, -9 }, +/* 0xC3 */ { 1239, 8, 10, 8, 0, -9 }, +/* 0xC4 */ { 1249, 8, 10, 8, 0, -9 }, +/* 0xC5 */ { 1259, 5, 10, 7, 1, -9 }, +/* 0xC6 */ { 1266, 7, 11, 9, 0, -10 }, +/* 0xC7 */ { 1276, 8, 12, 9, 0, -8 }, +/* 0xC8 */ { 1288, 7, 11, 9, 0, -10 }, +/* 0xC9 */ { 1298, 6, 10, 8, 1, -9 }, +/* 0xCA */ { 1306, 7, 11, 8, 1, -8 }, +/* 0xCB */ { 1316, 6, 10, 8, 1, -9 }, +/* 0xCC */ { 1324, 6, 11, 8, 1, -10 }, +/* 0xCD */ { 1333, 2, 10, 3, 1, -9 }, +/* 0xCE */ { 1336, 3, 10, 4, 0, -9 }, +/* 0xCF */ { 1340, 7, 10, 8, 1, -9 }, +/* 0xD0 */ { 1349, 8, 9, 8, 0, -8 }, +/* 0xD1 */ { 1358, 7, 10, 9, 1, -9 }, +/* 0xD2 */ { 1367, 7, 10, 9, 1, -9 }, +/* 0xD3 */ { 1376, 9, 10, 9, 0, -9 }, +/* 0xD4 */ { 1388, 9, 11, 9, 0, -10 }, +/* 0xD5 */ { 1401, 9, 11, 9, 0, -10 }, +/* 0xD6 */ { 1414, 9, 11, 9, 0, -10 }, +/* 0xD7 */ { 1427, 5, 5, 7, 1, -5 }, +/* 0xD8 */ { 1431, 7, 10, 9, 1, -9 }, +/* 0xD9 */ { 1440, 7, 10, 9, 1, -9 }, +/* 0xDA */ { 1449, 7, 10, 9, 1, -9 }, +/* 0xDB */ { 1458, 7, 10, 9, 1, -9 }, +/* 0xDC */ { 1467, 7, 10, 9, 1, -9 }, +/* 0xDD */ { 1476, 7, 10, 8, 1, -9 }, +/* 0xDE */ { 1485, 6, 12, 7, 0, -8 }, +/* 0xDF */ { 1494, 6, 9, 7, 1, -8 }, +/* 0xE0 */ { 1501, 3, 9, 4, 1, -8 }, +/* 0xE1 */ { 1505, 7, 10, 7, 0, -9 }, +/* 0xE2 */ { 1514, 7, 10, 7, 0, -9 }, +/* 0xE3 */ { 1523, 7, 10, 7, 0, -9 }, +/* 0xE4 */ { 1532, 7, 9, 7, 0, -8 }, +/* 0xE5 */ { 1540, 2, 10, 3, 1, -9 }, +/* 0xE6 */ { 1543, 6, 10, 6, 0, -9 }, +/* 0xE7 */ { 1551, 6, 10, 6, 0, -6 }, +/* 0xE8 */ { 1559, 6, 10, 6, 0, -9 }, +/* 0xE9 */ { 1567, 6, 10, 6, 0, -9 }, +/* 0xEA */ { 1575, 6, 9, 6, 0, -6 }, +/* 0xEB */ { 1582, 6, 9, 6, 0, -8 }, +/* 0xEC */ { 1589, 6, 10, 6, 0, -9 }, +/* 0xED */ { 1597, 2, 10, 3, 1, -9 }, +/* 0xEE */ { 1600, 3, 10, 3, 0, -9 }, +/* 0xEF */ { 1604, 7, 10, 7, 0, -9 }, +/* 0xF0 */ { 1613, 7, 9, 7, 0, -8 }, +/* 0xF1 */ { 1621, 5, 10, 6, 1, -9 }, +/* 0xF2 */ { 1628, 5, 10, 6, 1, -9 }, +/* 0xF3 */ { 1635, 6, 10, 6, 0, -9 }, +/* 0xF4 */ { 1643, 6, 10, 6, 0, -9 }, +/* 0xF5 */ { 1651, 6, 10, 6, 0, -9 }, +/* 0xF6 */ { 1659, 6, 9, 6, 0, -8 }, +/* 0xF7 */ { 1666, 5, 5, 7, 1, -5 }, +/* 0xF8 */ { 1670, 3, 10, 4, 1, -9 }, +/* 0xF9 */ { 1674, 5, 10, 6, 1, -9 }, +/* 0xFA */ { 1681, 5, 9, 6, 1, -8 }, +/* 0xFB */ { 1687, 5, 10, 6, 1, -9 }, +/* 0xFC */ { 1694, 5, 9, 6, 1, -8 }, +/* 0xFD */ { 1700, 6, 12, 6, 0, -8 }, +/* 0xFE */ { 1709, 4, 11, 3, 0, -7 }, +/* 0xFF */ { 1715, 1, 1, 4, 1, -7 }, }; -const GFXfont FreeSans6pt_Win1250 PROGMEM = {(uint8_t *)FreeSans6pt_Win1250Bitmaps, (GFXglyph *)FreeSans6pt_Win1250Glyphs, 0x20, - 0xFF, 14}; +const GFXfont FreeSans6pt_Win1250 PROGMEM = { +(uint8_t*)FreeSans6pt_Win1250Bitmaps, +(GFXglyph*)FreeSans6pt_Win1250Glyphs, +0x01, 0xFF, 14 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h index 4d3ad1705..8fe505fbd 100644 --- a/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h @@ -1,457 +1,527 @@ +// trunk-ignore-all(clang-format) #pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1251 +*/ const uint8_t FreeSans6pt_Win1251Bitmaps[] PROGMEM = { - /* ' ' 0x20 */ - 0xFC, 0x80, /* '!' 0x21 */ - 0xB6, 0x80, /* '"' 0x22 */ - 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '#' 0x23 */ - 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '$' 0x24 */ - 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '%' 0x25 */ - 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* '&' 0x26 */ - 0xE0, /* ''' 0x27 */ - 0x5A, 0xAA, 0x94, /* '(' 0x28 */ - 0x89, 0x12, 0x49, 0x29, 0x00, /* ')' 0x29 */ - 0x5E, 0x80, /* '*' 0x2A */ - 0x21, 0x3E, 0x42, 0x00, /* '+' 0x2B */ - 0xE0, /* ',' 0x2C */ - 0xC0, /* '-' 0x2D */ - 0x80, /* '.' 0x2E */ - 0x24, 0xA4, 0xA4, 0x80, /* '/' 0x2F */ - 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '0' 0x30 */ - 0x27, 0x92, 0x49, 0x20, /* '1' 0x31 */ - 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '2' 0x32 */ - 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '3' 0x33 */ - 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '4' 0x34 */ - 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '5' 0x35 */ - 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '6' 0x36 */ - 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '7' 0x37 */ - 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '8' 0x38 */ - 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* '9' 0x39 */ - 0x82, /* ':' 0x3A */ - 0x87, /* ';' 0x3B */ - 0x3E, 0x30, 0x60, 0x80, /* '<' 0x3C */ - 0xF8, 0x3E, /* '=' 0x3D */ - 0xE0, 0xC6, 0xC8, 0x00, /* '>' 0x3E */ - 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '?' 0x3F */ - 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* '@' 0x40 */ - 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'A' 0x41 */ - 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'B' 0x42 */ - 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'C' 0x43 */ - 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'D' 0x44 */ - 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'E' 0x45 */ - 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'F' 0x46 */ - 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'G' 0x47 */ - 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'H' 0x48 */ - 0xFF, 0x80, /* 'I' 0x49 */ - 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'J' 0x4A */ - 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'K' 0x4B */ - 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'L' 0x4C */ - 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'M' 0x4D */ - 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'N' 0x4E */ - 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'O' 0x4F */ - 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'P' 0x50 */ - 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'Q' 0x51 */ - 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'R' 0x52 */ - 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'S' 0x53 */ - 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'T' 0x54 */ - 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'U' 0x55 */ - 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'V' 0x56 */ - 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'W' 0x57 */ - 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'X' 0x58 */ - 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Y' 0x59 */ - 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* 'Z' 0x5A */ - 0xEA, 0xAA, 0xAB, /* '[' 0x5B */ - 0x92, 0x24, 0x89, 0x20, /* '\' 0x5C */ - 0xD5, 0x55, 0x57, /* ']' 0x5D */ - 0x46, 0xA9, /* '^' 0x5E */ - 0xFE, /* '_' 0x5F */ - 0x80, /* '`' 0x60 */ - 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'a' 0x61 */ - 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'b' 0x62 */ - 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'c' 0x63 */ - 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'd' 0x64 */ - 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'e' 0x65 */ - 0x6B, 0xA4, 0x92, 0x40, /* 'f' 0x66 */ - 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'g' 0x67 */ - 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'h' 0x68 */ - 0xBF, 0x80, /* 'i' 0x69 */ - 0x45, 0x55, 0x57, /* 'j' 0x6A */ - 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'k' 0x6B */ - 0xFF, 0x80, /* 'l' 0x6C */ - 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'm' 0x6D */ - 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'n' 0x6E */ - 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'o' 0x6F */ - 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'p' 0x70 */ - 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'q' 0x71 */ - 0xF2, 0x49, 0x20, /* 'r' 0x72 */ - 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 's' 0x73 */ - 0x5D, 0x24, 0x93, /* 't' 0x74 */ - 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'u' 0x75 */ - 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'v' 0x76 */ - 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'w' 0x77 */ - 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'x' 0x78 */ - 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'y' 0x79 */ - 0x78, 0x44, 0x46, 0x23, 0xE0, /* 'z' 0x7A */ - 0x6A, 0xAA, 0xA9, /* '{' 0x7B */ - 0xFF, 0xE0, /* '|' 0x7C */ - 0x95, 0x55, 0x56, /* '}' 0x7D */ - 0x66, 0x60, /* '~' 0x7E */ - 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x7F */ - 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, /* 0x80 */ - 0x10, 0x8F, 0xE0, 0x82, 0x08, 0x20, 0x82, 0x00, /* 0x81 */ - 0xE0, /* 0x82 */ - 0x24, 0x0F, 0x88, 0x88, 0x80, /* 0x83 */ - 0xB6, 0x80, /* 0x84 */ - 0xA8, /* 0x85 */ - 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x86 */ - 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x87 */ - 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x88 */ - 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x89 */ - 0x7C, 0x08, 0x81, 0x10, 0x22, 0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, /* 0x8A */ - 0x64, /* 0x8B */ - 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0, /* 0x8C */ - 0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, /* 0x8D */ - 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, /* 0x8E */ - 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, /* 0x8F */ - 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, /* 0x90 */ - 0xE0, /* 0x91 */ - 0xE0, /* 0x92 */ - 0xB6, 0x80, /* 0x93 */ - 0xB6, 0x80, /* 0x94 */ - 0xFF, 0x80, /* 0x95 */ - 0xFC, /* 0x96 */ - 0xFF, 0xF0, /* 0x97 */ - /* 0x98 */ - 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x99 */ - 0x78, 0x24, 0x13, 0xC9, 0x14, 0x8E, 0x7C, /* 0x9A */ - 0x98, /* 0x9B */ - 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, /* 0x9C */ - 0x24, 0x09, 0xAC, 0xCA, 0x90, /* 0x9D */ - 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, /* 0x9E */ - 0x8C, 0x63, 0x18, 0xFC, 0x80, /* 0x9F */ - /* 0xA0 */ - 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, /* 0xA1 */ - 0x51, 0x22, 0x95, 0xA8, 0xC4, 0x23, 0x10, /* 0xA2 */ - 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, /* 0xA3 */ - 0xFC, 0x63, 0xF0, /* 0xA4 */ - 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, /* 0xA5 */ - 0xF9, 0xF0, /* 0xA6 */ - 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA7 */ - 0x28, 0x0F, 0xE0, 0x82, 0x0F, 0xE0, 0x82, 0x0F, 0xC0, /* 0xA8 */ - 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xA9 */ - 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, /* 0xAA */ - 0x5A, 0xA5, /* 0xAB */ - 0x51, 0x55, 0x56, /* 0xAC */ - /* 0xAD */ - 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAE */ - 0xA1, 0x24, 0x92, 0x49, 0x00, /* 0xAF */ - 0x69, 0x96, /* 0xB0 */ - 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB1 */ - 0xFF, 0x80, /* 0xB2 */ - 0xDF, 0x80, /* 0xB3 */ - 0x27, 0xC9, 0x24, /* 0xB4 */ - 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB5 */ - 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB6 */ - 0x80, /* 0xB7 */ - 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, /* 0xB8 */ - 0x88, 0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x8B, 0x80, /* 0xB9 */ - 0x79, 0x1F, 0x30, 0x45, 0xE0, /* 0xBA */ - 0xA5, 0x5A, /* 0xBB */ - 0x45, 0x55, 0x57, /* 0xBC */ - 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, /* 0xBD */ - 0x7A, 0x1C, 0x1C, 0xBC, /* 0xBE */ - 0xB4, 0x24, 0x92, 0x40, /* 0xBF */ - 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 0xC0 */ - 0xFE, 0x08, 0x20, 0xFA, 0x18, 0x61, 0xF8, /* 0xC1 */ - 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 0xC2 */ - 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, /* 0xC3 */ - 0x1F, 0x08, 0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, /* 0xC4 */ - 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 0xC5 */ - 0x88, 0xA4, 0x9A, 0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, /* 0xC6 */ - 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, /* 0xC7 */ - 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68, 0xE1, 0xC2, /* 0xC8 */ - 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, /* 0xC9 */ - 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, /* 0xCA */ - 0x3E, 0x44, 0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, /* 0xCB */ - 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 0xCC */ - 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 0xCD */ - 0x3C, 0x42, 0x81, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, /* 0xCE */ - 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, /* 0xCF */ - 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, /* 0xD0 */ - 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, /* 0xD1 */ - 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 0xD2 */ - 0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, /* 0xD3 */ - 0x08, 0x1F, 0x32, 0x71, 0x18, 0x8C, 0x47, 0x26, 0xFE, 0x08, 0x00, /* 0xD4 */ - 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, /* 0xD5 */ - 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, /* 0xD6 */ - 0x8E, 0x38, 0xE3, 0x8D, 0xF0, 0xC3, 0x0C, /* 0xD7 */ - 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, /* 0xD8 */ - 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF, 0x80, 0x40, 0x20, /* 0xD9 */ - 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, /* 0xDA */ - 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC, 0x2E, 0x17, 0x0B, 0xF9, 0x80, /* 0xDB */ - 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, /* 0xDC */ - 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, /* 0xDD */ - 0x87, 0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, /* 0xDE */ - 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, /* 0xDF */ - 0x79, 0x11, 0xD9, 0xCD, 0xD0, /* 0xE0 */ - 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, /* 0xE1 */ - 0xF4, 0xBD, 0x29, 0xF8, /* 0xE2 */ - 0xF8, 0x88, 0x88, /* 0xE3 */ - 0x3C, 0x48, 0x91, 0x22, 0x5F, 0xE0, 0x80, /* 0xE4 */ - 0x79, 0x1F, 0xF0, 0x45, 0xE0, /* 0xE5 */ - 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, /* 0xE6 */ - 0x78, 0x23, 0x82, 0xCD, 0xE0, /* 0xE7 */ - 0x9C, 0xEB, 0x5C, 0xC4, /* 0xE8 */ - 0x70, 0x27, 0x3A, 0xD7, 0x31, /* 0xE9 */ - 0x9A, 0xCC, 0xA9, /* 0xEA */ - 0x7A, 0x52, 0x94, 0xE4, /* 0xEB */ - 0x8F, 0x3D, 0x6D, 0xA6, 0x90, /* 0xEC */ - 0x8C, 0x7F, 0x18, 0xC4, /* 0xED */ - 0x79, 0x1C, 0x71, 0x45, 0xE0, /* 0xEE */ - 0xFC, 0x63, 0x18, 0xC4, /* 0xEF */ - 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, /* 0xF0 */ - 0x79, 0x1C, 0x30, 0x45, 0xE0, /* 0xF1 */ - 0xF9, 0x08, 0x42, 0x10, /* 0xF2 */ - 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, /* 0xF3 */ - 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04, 0x01, 0x00, 0x40, /* 0xF4 */ - 0x4B, 0x8C, 0x65, 0xE4, /* 0xF5 */ - 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, /* 0xF6 */ - 0x99, 0x97, 0x11, /* 0xF7 */ - 0x96, 0x59, 0x65, 0x97, 0xF0, /* 0xF8 */ - 0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, /* 0xF9 */ - 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, /* 0xFA */ - 0x86, 0x1F, 0x63, 0x8F, 0xD0, /* 0xFB */ - 0x84, 0x3D, 0x18, 0xF8, /* 0xFC */ - 0xF4, 0xDE, 0x19, 0xF8, /* 0xFD */ - 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, /* 0xFE */ - 0xFC, 0x7E, 0xD4, 0xC4, /* 0xFF */ +/* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, +/* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, +/* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, +/* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, +/* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, +/* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, +/* 0x07 */ +/* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, +/* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, +/* 0x0A */ +/* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, +/* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, +/* 0x0D */ +/* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, +/* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, +/* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, +/* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, +/* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, +/* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, +/* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, +/* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, +/* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, +/* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, +/* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, +/* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, +/* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, +/* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, +/* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, +/* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, +/* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, +/* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFC, 0x80, +/* '"' 0x22 */ 0xB6, 0x80, +/* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, +/* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, +/* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, +/* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, +/* ''' 0x27 */ 0xE0, +/* '(' 0x28 */ 0x5A, 0xAA, 0x94, +/* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, +/* '*' 0x2A */ 0x5E, 0x80, +/* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, +/* ',' 0x2C */ 0xE0, +/* '-' 0x2D */ 0xC0, +/* '.' 0x2E */ 0x80, +/* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, +/* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, +/* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, +/* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, +/* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, +/* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, +/* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, +/* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, +/* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, +/* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, +/* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, +/* ':' 0x3A */ 0x82, +/* ';' 0x3B */ 0x87, +/* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, +/* '=' 0x3D */ 0xF8, 0x3E, +/* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, +/* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, +/* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, +/* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, +/* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, +/* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, +/* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, +/* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, +/* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 'I' 0x49 */ 0xFF, 0x80, +/* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, +/* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, +/* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, +/* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, +/* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, +/* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, +/* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, +/* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, +/* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, +/* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, +/* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, +/* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, +/* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, +/* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, +/* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, +/* '[' 0x5B */ 0xEA, 0xAA, 0xAB, +/* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, +/* ']' 0x5D */ 0xD5, 0x55, 0x57, +/* '^' 0x5E */ 0x46, 0xA9, +/* '_' 0x5F */ 0xFE, +/* '`' 0x60 */ 0x80, +/* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, +/* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, +/* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, +/* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, +/* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, +/* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, +/* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, +/* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, +/* 'i' 0x69 */ 0xBF, 0x80, +/* 'j' 0x6A */ 0x45, 0x55, 0x57, +/* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, +/* 'l' 0x6C */ 0xFF, 0x80, +/* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, +/* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, +/* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, +/* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, +/* 'r' 0x72 */ 0xF2, 0x49, 0x20, +/* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, +/* 't' 0x74 */ 0x5D, 0x24, 0x93, +/* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, +/* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, +/* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, +/* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, +/* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, +/* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, +/* '{' 0x7B */ 0x6A, 0xAA, 0xA9, +/* '|' 0x7C */ 0xFF, 0xE0, +/* '}' 0x7D */ 0x95, 0x55, 0x56, +/* '~' 0x7E */ 0x66, 0x60, +/* 0x7F */ +/* 0x80 */ 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, +/* 0x81 */ 0x10, 0x8F, 0xE0, 0x82, 0x08, 0x20, 0x82, 0x00, +/* 0x82 */ 0xE0, +/* 0x83 */ 0x24, 0x0F, 0x88, 0x88, 0x80, +/* 0x84 */ 0xB6, 0x80, +/* 0x85 */ 0xA8, +/* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, +/* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, +/* 0x88 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, +/* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, +/* 0x8A */ 0x7C, 0x08, 0x81, 0x10, 0x22, 0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, +/* 0x8B */ 0x64, +/* 0x8C */ 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0, +/* 0x8D */ 0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, +/* 0x8E */ 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, +/* 0x8F */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, +/* 0x90 */ 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, +/* 0x91 */ 0xE0, +/* 0x92 */ 0xE0, +/* 0x93 */ 0xB6, 0x80, +/* 0x94 */ 0xB6, 0x80, +/* 0x95 */ 0xFF, 0x80, +/* 0x96 */ 0xFC, +/* 0x97 */ 0xFF, 0xF0, +/* 0x98 */ +/* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, +/* 0x9A */ 0x78, 0x24, 0x13, 0xC9, 0x14, 0x8E, 0x7C, +/* 0x9B */ 0x98, +/* 0x9C */ 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, +/* 0x9D */ 0x24, 0x09, 0xAC, 0xCA, 0x90, +/* 0x9E */ 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, +/* 0x9F */ 0x8C, 0x63, 0x18, 0xFC, 0x80, +/* 0xA0 */ +/* 0xA1 */ 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, +/* 0xA2 */ 0x51, 0x22, 0x95, 0xA8, 0xC4, 0x23, 0x10, +/* 0xA3 */ 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, +/* 0xA4 */ 0xFC, 0x63, 0xF0, +/* 0xA5 */ 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, +/* 0xA6 */ 0xF9, 0xF0, +/* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, +/* 0xA8 */ 0x28, 0x0F, 0xE0, 0x82, 0x0F, 0xE0, 0x82, 0x0F, 0xC0, +/* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, +/* 0xAA */ 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, +/* 0xAB */ 0x5A, 0xA5, +/* 0xAC */ 0x51, 0x55, 0x56, +/* 0xAD */ +/* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, +/* 0xAF */ 0xA1, 0x24, 0x92, 0x49, 0x00, +/* 0xB0 */ 0x69, 0x96, +/* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, +/* 0xB2 */ 0xFF, 0x80, +/* 0xB3 */ 0xDF, 0x80, +/* 0xB4 */ 0x27, 0xC9, 0x24, +/* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, +/* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, +/* 0xB7 */ 0x80, +/* 0xB8 */ 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, +/* 0xB9 */ 0x88, 0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x8B, 0x80, +/* 0xBA */ 0x79, 0x1F, 0x30, 0x45, 0xE0, +/* 0xBB */ 0xA5, 0x5A, +/* 0xBC */ 0x45, 0x55, 0x57, +/* 0xBD */ 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, +/* 0xBE */ 0x7A, 0x1C, 0x1C, 0xBC, +/* 0xBF */ 0xB4, 0x24, 0x92, 0x40, +/* 0xC0 */ 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 0xC1 */ 0xFE, 0x08, 0x20, 0xFA, 0x18, 0x61, 0xF8, +/* 0xC2 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, +/* 0xC3 */ 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, +/* 0xC4 */ 0x1F, 0x08, 0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, +/* 0xC5 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 0xC6 */ 0x88, 0xA4, 0x9A, 0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, +/* 0xC7 */ 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, +/* 0xC8 */ 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68, 0xE1, 0xC2, +/* 0xC9 */ 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, +/* 0xCA */ 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, +/* 0xCB */ 0x3E, 0x44, 0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, +/* 0xCC */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, +/* 0xCD */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 0xCE */ 0x3C, 0x42, 0x81, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, +/* 0xCF */ 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, +/* 0xD0 */ 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, +/* 0xD1 */ 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, +/* 0xD2 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 0xD3 */ 0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, +/* 0xD4 */ 0x08, 0x1F, 0x32, 0x71, 0x18, 0x8C, 0x47, 0x26, 0xFE, 0x08, 0x00, +/* 0xD5 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, +/* 0xD6 */ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, +/* 0xD7 */ 0x8E, 0x38, 0xE3, 0x8D, 0xF0, 0xC3, 0x0C, +/* 0xD8 */ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, +/* 0xD9 */ 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF, 0x80, 0x40, 0x20, +/* 0xDA */ 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, +/* 0xDB */ 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC, 0x2E, 0x17, 0x0B, 0xF9, 0x80, +/* 0xDC */ 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, +/* 0xDD */ 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, +/* 0xDE */ 0x87, 0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, +/* 0xDF */ 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, +/* 0xE0 */ 0x79, 0x11, 0xD9, 0xCD, 0xD0, +/* 0xE1 */ 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, +/* 0xE2 */ 0xF4, 0xBD, 0x29, 0xF8, +/* 0xE3 */ 0xF8, 0x88, 0x88, +/* 0xE4 */ 0x3C, 0x48, 0x91, 0x22, 0x5F, 0xE0, 0x80, +/* 0xE5 */ 0x79, 0x1F, 0xF0, 0x45, 0xE0, +/* 0xE6 */ 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, +/* 0xE7 */ 0x78, 0x23, 0x82, 0xCD, 0xE0, +/* 0xE8 */ 0x9C, 0xEB, 0x5C, 0xC4, +/* 0xE9 */ 0x70, 0x27, 0x3A, 0xD7, 0x31, +/* 0xEA */ 0x9A, 0xCC, 0xA9, +/* 0xEB */ 0x7A, 0x52, 0x94, 0xE4, +/* 0xEC */ 0x8F, 0x3D, 0x6D, 0xA6, 0x90, +/* 0xED */ 0x8C, 0x7F, 0x18, 0xC4, +/* 0xEE */ 0x79, 0x1C, 0x71, 0x45, 0xE0, +/* 0xEF */ 0xFC, 0x63, 0x18, 0xC4, +/* 0xF0 */ 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, +/* 0xF1 */ 0x79, 0x1C, 0x30, 0x45, 0xE0, +/* 0xF2 */ 0xF9, 0x08, 0x42, 0x10, +/* 0xF3 */ 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, +/* 0xF4 */ 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04, 0x01, 0x00, 0x40, +/* 0xF5 */ 0x4B, 0x8C, 0x65, 0xE4, +/* 0xF6 */ 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, +/* 0xF7 */ 0x99, 0x97, 0x11, +/* 0xF8 */ 0x96, 0x59, 0x65, 0x97, 0xF0, +/* 0xF9 */ 0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, +/* 0xFA */ 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, +/* 0xFB */ 0x86, 0x1F, 0x63, 0x8F, 0xD0, +/* 0xFC */ 0x84, 0x3D, 0x18, 0xF8, +/* 0xFD */ 0xF4, 0xDE, 0x19, 0xF8, +/* 0xFE */ 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, +/* 0xFF */ 0xFC, 0x7E, 0xD4, 0xC4, }; const GFXglyph FreeSans6pt_Win1251Glyphs[] PROGMEM = { - /* ' ' 0x20 */ {0, 0, 0, 3, 0, 0}, - /* '!' 0x21 */ {0, 1, 9, 4, 2, -8}, - /* '"' 0x22 */ {2, 3, 3, 4, 0, -8}, - /* '#' 0x23 */ {4, 7, 8, 7, 0, -7}, - /* '$' 0x24 */ {11, 6, 11, 7, 0, -9}, - /* '%' 0x25 */ {20, 10, 9, 11, 0, -8}, - /* '&' 0x26 */ {32, 6, 9, 8, 1, -8}, - /* ''' 0x27 */ {39, 1, 3, 2, 1, -8}, - /* '(' 0x28 */ {40, 2, 11, 4, 1, -8}, - /* ')' 0x29 */ {43, 3, 11, 4, 0, -8}, - /* '*' 0x2A */ {48, 3, 3, 5, 1, -8}, - /* '+' 0x2B */ {50, 5, 5, 7, 1, -4}, - /* ',' 0x2C */ {54, 1, 3, 3, 1, 0}, - /* '-' 0x2D */ {55, 2, 1, 4, 1, -3}, - /* '.' 0x2E */ {56, 1, 1, 3, 1, 0}, - /* '/' 0x2F */ {57, 3, 9, 3, 0, -8}, - /* '0' 0x30 */ {61, 5, 9, 7, 1, -8}, - /* '1' 0x31 */ {67, 3, 9, 7, 1, -8}, - /* '2' 0x32 */ {71, 6, 9, 7, 0, -8}, - /* '3' 0x33 */ {78, 6, 9, 7, 0, -8}, - /* '4' 0x34 */ {85, 6, 9, 7, 0, -8}, - /* '5' 0x35 */ {92, 5, 9, 7, 1, -8}, - /* '6' 0x36 */ {98, 5, 9, 7, 1, -8}, - /* '7' 0x37 */ {104, 5, 9, 7, 1, -8}, - /* '8' 0x38 */ {110, 6, 9, 7, 0, -8}, - /* '9' 0x39 */ {117, 6, 9, 7, 0, -8}, - /* ':' 0x3A */ {124, 1, 7, 3, 1, -6}, - /* ';' 0x3B */ {125, 1, 8, 3, 1, -5}, - /* '<' 0x3C */ {126, 5, 5, 7, 1, -4}, - /* '=' 0x3D */ {130, 5, 3, 7, 1, -3}, - /* '>' 0x3E */ {132, 5, 5, 7, 1, -4}, - /* '?' 0x3F */ {136, 5, 9, 7, 1, -8}, - /* '@' 0x40 */ {142, 11, 11, 12, 0, -8}, - /* 'A' 0x41 */ {158, 8, 9, 8, 0, -8}, - /* 'B' 0x42 */ {167, 6, 9, 8, 1, -8}, - /* 'C' 0x43 */ {174, 8, 9, 9, 0, -8}, - /* 'D' 0x44 */ {183, 7, 9, 8, 1, -8}, - /* 'E' 0x45 */ {191, 6, 9, 8, 1, -8}, - /* 'F' 0x46 */ {198, 6, 9, 7, 1, -8}, - /* 'G' 0x47 */ {205, 8, 9, 9, 0, -8}, - /* 'H' 0x48 */ {214, 7, 9, 9, 1, -8}, - /* 'I' 0x49 */ {222, 1, 9, 3, 1, -8}, - /* 'J' 0x4A */ {224, 5, 9, 6, 0, -8}, - /* 'K' 0x4B */ {230, 7, 9, 8, 1, -8}, - /* 'L' 0x4C */ {238, 5, 9, 7, 1, -8}, - /* 'M' 0x4D */ {244, 8, 9, 10, 1, -8}, - /* 'N' 0x4E */ {253, 7, 9, 9, 1, -8}, - /* 'O' 0x4F */ {261, 9, 9, 9, 0, -8}, - /* 'P' 0x50 */ {272, 6, 9, 8, 1, -8}, - /* 'Q' 0x51 */ {279, 9, 10, 9, 0, -8}, - /* 'R' 0x52 */ {291, 7, 9, 9, 1, -8}, - /* 'S' 0x53 */ {299, 6, 9, 8, 1, -8}, - /* 'T' 0x54 */ {306, 7, 9, 8, 0, -8}, - /* 'U' 0x55 */ {314, 7, 9, 9, 1, -8}, - /* 'V' 0x56 */ {322, 7, 9, 8, 0, -8}, - /* 'W' 0x57 */ {330, 11, 9, 11, 0, -8}, - /* 'X' 0x58 */ {343, 6, 9, 8, 1, -8}, - /* 'Y' 0x59 */ {350, 8, 9, 8, 0, -8}, - /* 'Z' 0x5A */ {359, 7, 9, 7, 0, -8}, - /* '[' 0x5B */ {367, 2, 12, 3, 1, -8}, - /* '\' 0x5C */ {370, 3, 9, 3, 0, -8}, - /* ']' 0x5D */ {374, 2, 12, 3, 0, -8}, - /* '^' 0x5E */ {377, 4, 4, 6, 1, -8}, - /* '_' 0x5F */ {379, 7, 1, 7, 0, 2}, - /* '`' 0x60 */ {380, 1, 1, 3, 1, -8}, - /* 'a' 0x61 */ {381, 6, 7, 7, 0, -6}, - /* 'b' 0x62 */ {387, 5, 9, 7, 1, -8}, - /* 'c' 0x63 */ {393, 6, 7, 6, 0, -6}, - /* 'd' 0x64 */ {399, 6, 9, 7, 0, -8}, - /* 'e' 0x65 */ {406, 6, 7, 6, 0, -6}, - /* 'f' 0x66 */ {412, 3, 9, 3, 0, -8}, - /* 'g' 0x67 */ {416, 6, 10, 7, 0, -6}, - /* 'h' 0x68 */ {424, 5, 9, 6, 1, -8}, - /* 'i' 0x69 */ {430, 1, 9, 3, 1, -8}, - /* 'j' 0x6A */ {432, 2, 12, 3, 0, -8}, - /* 'k' 0x6B */ {435, 5, 9, 6, 1, -8}, - /* 'l' 0x6C */ {441, 1, 9, 3, 1, -8}, - /* 'm' 0x6D */ {443, 8, 7, 10, 1, -6}, - /* 'n' 0x6E */ {450, 5, 7, 6, 1, -6}, - /* 'o' 0x6F */ {455, 6, 7, 6, 0, -6}, - /* 'p' 0x70 */ {461, 5, 9, 7, 1, -6}, - /* 'q' 0x71 */ {467, 6, 9, 7, 0, -6}, - /* 'r' 0x72 */ {474, 3, 7, 4, 1, -6}, - /* 's' 0x73 */ {477, 5, 7, 6, 0, -6}, - /* 't' 0x74 */ {482, 3, 8, 3, 0, -7}, - /* 'u' 0x75 */ {485, 5, 7, 6, 1, -6}, - /* 'v' 0x76 */ {490, 6, 7, 6, 0, -6}, - /* 'w' 0x77 */ {496, 8, 7, 9, 0, -6}, - /* 'x' 0x78 */ {503, 5, 7, 6, 0, -6}, - /* 'y' 0x79 */ {508, 5, 10, 6, 0, -6}, - /* 'z' 0x7A */ {515, 5, 7, 6, 0, -6}, - /* '{' 0x7B */ {520, 2, 12, 4, 1, -8}, - /* '|' 0x7C */ {523, 1, 11, 3, 1, -8}, - /* '}' 0x7D */ {525, 2, 12, 4, 1, -8}, - /* '~' 0x7E */ {528, 6, 2, 6, 0, -4}, - /* 0x7F */ {530, 9, 10, 11, 1, -8}, - /* 0x80 */ {542, 9, 11, 9, 0, -8}, - /* 0x81 */ {555, 6, 10, 7, 1, -9}, - /* 0x82 */ {563, 1, 3, 3, 1, 0}, - /* 0x83 */ {564, 4, 9, 5, 1, -8}, - /* 0x84 */ {569, 3, 3, 5, 1, 0}, - /* 0x85 */ {571, 5, 1, 7, 1, 0}, - /* 0x86 */ {572, 5, 11, 7, 1, -8}, - /* 0x87 */ {579, 5, 11, 7, 1, -8}, - /* 0x88 */ {586, 7, 9, 8, 0, -8}, - /* 0x89 */ {594, 12, 9, 12, 0, -8}, - /* 0x8A */ {608, 11, 9, 13, 1, -8}, - /* 0x8B */ {621, 2, 3, 4, 1, -4}, - /* 0x8C */ {622, 11, 9, 12, 1, -8}, - /* 0x8D */ {635, 6, 11, 8, 1, -10}, - /* 0x8E */ {644, 9, 9, 9, 0, -8}, - /* 0x8F */ {655, 7, 11, 9, 1, -8}, - /* 0x90 */ {665, 6, 11, 7, 0, -8}, - /* 0x91 */ {674, 1, 3, 3, 1, -8}, - /* 0x92 */ {675, 1, 3, 2, 1, -8}, - /* 0x93 */ {676, 3, 3, 5, 1, -8}, - /* 0x94 */ {678, 3, 3, 5, 1, -8}, - /* 0x95 */ {680, 3, 3, 5, 1, -5}, - /* 0x96 */ {682, 6, 1, 6, 0, -3}, - /* 0x97 */ {683, 12, 1, 12, 0, -3}, - /* 0x98 */ {685, 0, 0, 8, 0, 0}, - /* 0x99 */ {685, 11, 7, 12, 1, -8}, - /* 0x9A */ {695, 9, 6, 10, 0, -5}, - /* 0x9B */ {702, 2, 3, 3, 1, -4}, - /* 0x9C */ {703, 9, 6, 10, 1, -5}, - /* 0x9D */ {710, 4, 9, 6, 1, -8}, - /* 0x9E */ {715, 6, 9, 7, 0, -8}, - /* 0x9F */ {722, 5, 7, 7, 1, -5}, - /* 0xA0 */ {727, 0, 0, 3, 0, 0}, - /* 0xA1 */ {727, 7, 11, 7, 0, -10}, - /* 0xA2 */ {737, 5, 11, 6, 0, -7}, - /* 0xA3 */ {744, 5, 9, 6, 0, -8}, - /* 0xA4 */ {750, 5, 4, 7, 1, -5}, - /* 0xA5 */ {753, 6, 10, 7, 1, -9}, - /* 0xA6 */ {761, 1, 12, 3, 1, -8}, - /* 0xA7 */ {763, 5, 12, 7, 1, -8}, - /* 0xA8 */ {771, 6, 11, 8, 1, -10}, - /* 0xA9 */ {780, 9, 9, 10, 0, -8}, - /* 0xAA */ {791, 7, 9, 9, 1, -8}, - /* 0xAB */ {799, 4, 4, 6, 1, -4}, - /* 0xAC */ {801, 2, 12, 3, 0, -8}, - /* 0xAD */ {804, 0, 0, 0, 0, 0}, - /* 0xAE */ {804, 9, 9, 10, 0, -8}, - /* 0xAF */ {815, 3, 11, 3, 0, -10}, - /* 0xB0 */ {820, 4, 4, 7, 2, -8}, - /* 0xB1 */ {822, 5, 7, 7, 1, -6}, - /* 0xB2 */ {827, 1, 9, 3, 1, -8}, - /* 0xB3 */ {829, 1, 9, 3, 1, -8}, - /* 0xB4 */ {831, 3, 8, 5, 1, -7}, - /* 0xB5 */ {834, 6, 9, 7, 1, -6}, - /* 0xB6 */ {841, 6, 10, 6, 1, -8}, - /* 0xB7 */ {849, 1, 1, 3, 1, -2}, - /* 0xB8 */ {850, 6, 9, 7, 0, -8}, - /* 0xB9 */ {857, 9, 9, 11, 1, -8}, - /* 0xBA */ {868, 6, 6, 6, 0, -5}, - /* 0xBB */ {873, 4, 4, 6, 1, -5}, - /* 0xBC */ {875, 2, 12, 3, 0, -8}, - /* 0xBD */ {878, 6, 9, 8, 1, -8}, - /* 0xBE */ {885, 5, 6, 6, 0, -5}, - /* 0xBF */ {889, 3, 9, 3, 0, -8}, - /* 0xC0 */ {893, 8, 9, 8, 0, -8}, - /* 0xC1 */ {902, 6, 9, 8, 1, -8}, - /* 0xC2 */ {909, 6, 9, 8, 1, -8}, - /* 0xC3 */ {916, 6, 9, 7, 1, -8}, - /* 0xC4 */ {923, 9, 11, 10, 0, -8}, - /* 0xC5 */ {936, 6, 9, 8, 1, -8}, - /* 0xC6 */ {943, 9, 9, 11, 1, -8}, - /* 0xC7 */ {954, 6, 9, 8, 1, -8}, - /* 0xC8 */ {961, 7, 9, 9, 1, -8}, - /* 0xC9 */ {969, 7, 11, 9, 1, -10}, - /* 0xCA */ {979, 6, 9, 8, 1, -8}, - /* 0xCB */ {986, 7, 9, 8, 0, -8}, - /* 0xCC */ {994, 8, 9, 10, 1, -8}, - /* 0xCD */ {1003, 7, 9, 9, 1, -8}, - /* 0xCE */ {1011, 8, 9, 10, 1, -8}, - /* 0xCF */ {1020, 7, 9, 9, 1, -8}, - /* 0xD0 */ {1028, 6, 9, 8, 1, -8}, - /* 0xD1 */ {1035, 7, 9, 9, 1, -8}, - /* 0xD2 */ {1043, 7, 9, 7, 0, -8}, - /* 0xD3 */ {1051, 7, 9, 7, 0, -8}, - /* 0xD4 */ {1059, 9, 9, 10, 1, -8}, - /* 0xD5 */ {1070, 6, 9, 8, 1, -8}, - /* 0xD6 */ {1077, 8, 11, 9, 1, -8}, - /* 0xD7 */ {1088, 6, 9, 8, 1, -8}, - /* 0xD8 */ {1095, 8, 9, 10, 1, -8}, - /* 0xD9 */ {1104, 9, 11, 10, 1, -8}, - /* 0xDA */ {1117, 10, 9, 10, 0, -8}, - /* 0xDB */ {1129, 9, 9, 10, 1, -8}, - /* 0xDC */ {1140, 6, 9, 8, 1, -8}, - /* 0xDD */ {1147, 7, 9, 9, 1, -8}, - /* 0xDE */ {1155, 10, 9, 12, 1, -8}, - /* 0xDF */ {1167, 6, 9, 8, 1, -8}, - /* 0xE0 */ {1174, 6, 6, 7, 0, -5}, - /* 0xE1 */ {1179, 6, 9, 7, 0, -8}, - /* 0xE2 */ {1186, 5, 6, 6, 1, -5}, - /* 0xE3 */ {1190, 4, 6, 5, 1, -5}, - /* 0xE4 */ {1193, 7, 7, 7, 0, -5}, - /* 0xE5 */ {1200, 6, 6, 7, 0, -5}, - /* 0xE6 */ {1205, 8, 6, 9, 1, -5}, - /* 0xE7 */ {1211, 6, 6, 6, 0, -5}, - /* 0xE8 */ {1216, 5, 6, 7, 1, -5}, - /* 0xE9 */ {1220, 5, 8, 7, 1, -7}, - /* 0xEA */ {1225, 4, 6, 6, 1, -5}, - /* 0xEB */ {1228, 5, 6, 6, 0, -5}, - /* 0xEC */ {1232, 6, 6, 7, 1, -5}, - /* 0xED */ {1237, 5, 6, 7, 1, -5}, - /* 0xEE */ {1241, 6, 6, 7, 0, -5}, - /* 0xEF */ {1246, 5, 6, 7, 1, -5}, - /* 0xF0 */ {1250, 5, 9, 7, 1, -5}, - /* 0xF1 */ {1256, 6, 6, 6, 0, -5}, - /* 0xF2 */ {1261, 5, 6, 5, 0, -5}, - /* 0xF3 */ {1265, 5, 9, 6, 0, -5}, - /* 0xF4 */ {1271, 10, 11, 10, 0, -7}, - /* 0xF5 */ {1285, 5, 6, 6, 0, -5}, - /* 0xF6 */ {1289, 6, 7, 7, 1, -5}, - /* 0xF7 */ {1295, 4, 6, 6, 1, -5}, - /* 0xF8 */ {1298, 6, 6, 8, 1, -5}, - /* 0xF9 */ {1303, 7, 7, 9, 1, -5}, - /* 0xFA */ {1310, 7, 6, 8, 0, -5}, - /* 0xFB */ {1316, 6, 6, 8, 1, -5}, - /* 0xFC */ {1321, 5, 6, 6, 1, -5}, - /* 0xFD */ {1325, 5, 6, 6, 1, -5}, - /* 0xFE */ {1329, 8, 6, 9, 1, -5}, - /* 0xFF */ {1335, 5, 6, 7, 1, -5}, +/* 0x01 */ { 0, 9, 10, 11, 1, -9 }, +/* 0x02 */ { 12, 9, 10, 11, 1, -8 }, +/* 0x03 */ { 24, 10, 10, 12, 1, -8 }, +/* 0x04 */ { 37, 10, 10, 12, 1, -8 }, +/* 0x05 */ { 50, 10, 10, 12, 1, -9 }, +/* 0x06 */ { 63, 11, 11, 13, 1, -9 }, +/* 0x07 */ { 79, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 79, 12, 9, 14, 1, -8 }, +/* 0x09 */ { 93, 14, 8, 16, 1, -7 }, +/* 0x0A */ { 107, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 107, 9, 10, 11, 1, -9 }, +/* 0x0C */ { 119, 13, 9, 15, 1, -8 }, +/* 0x0D */ { 134, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 134, 9, 11, 11, 1, -9 }, +/* 0x0F */ { 147, 10, 10, 12, 1, -9 }, +/* 0x10 */ { 160, 11, 10, 13, 1, -9 }, +/* 0x11 */ { 174, 13, 10, 15, 1, -9 }, +/* 0x12 */ { 191, 10, 10, 12, 1, -9 }, +/* 0x13 */ { 204, 11, 10, 13, 1, -9 }, +/* 0x14 */ { 218, 10, 10, 12, 1, -9 }, +/* 0x15 */ { 231, 14, 10, 16, 1, -9 }, +/* 0x16 */ { 249, 8, 10, 10, 1, -9 }, +/* 0x17 */ { 259, 12, 10, 14, 1, -9 }, +/* 0x18 */ { 274, 13, 10, 15, 1, -9 }, +/* 0x19 */ { 291, 12, 10, 14, 1, -9 }, +/* 0x1A */ { 306, 9, 10, 11, 1, -8 }, +/* 0x1B */ { 318, 14, 10, 16, 1, -9 }, +/* 0x1C */ { 336, 11, 10, 13, 1, -9 }, +/* 0x1D */ { 350, 11, 10, 13, 1, -9 }, +/* 0x1E */ { 364, 12, 10, 14, 1, -9 }, +/* 0x1F */ { 379, 8, 10, 11, 2, -9 }, +/* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, +/* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, +/* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, +/* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, +/* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, +/* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, +/* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, +/* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, +/* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, +/* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, +/* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, +/* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, +/* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, +/* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, +/* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, +/* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, +/* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, +/* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, +/* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, +/* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, +/* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, +/* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, +/* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, +/* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, +/* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, +/* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, +/* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, +/* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, +/* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, +/* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, +/* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, +/* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, +/* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, +/* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, +/* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, +/* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, +/* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, +/* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, +/* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, +/* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, +/* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, +/* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, +/* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, +/* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, +/* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, +/* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, +/* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, +/* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, +/* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, +/* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, +/* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, +/* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, +/* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, +/* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, +/* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, +/* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, +/* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, +/* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, +/* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, +/* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, +/* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, +/* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, +/* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, +/* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, +/* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, +/* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, +/* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, +/* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, +/* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, +/* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, +/* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, +/* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, +/* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, +/* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, +/* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, +/* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, +/* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, +/* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, +/* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, +/* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, +/* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, +/* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, +/* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, +/* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, +/* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, +/* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, +/* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, +/* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, +/* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, +/* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, +/* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, +/* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, +/* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, +/* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, +/* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, +/* 0x7F */ { 919, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 919, 9, 11, 9, 0, -8 }, +/* 0x81 */ { 932, 6, 10, 7, 1, -9 }, +/* 0x82 */ { 940, 1, 3, 3, 1, 0 }, +/* 0x83 */ { 941, 4, 9, 5, 1, -8 }, +/* 0x84 */ { 946, 3, 3, 5, 1, 0 }, +/* 0x85 */ { 948, 5, 1, 7, 1, 0 }, +/* 0x86 */ { 949, 5, 11, 7, 1, -8 }, +/* 0x87 */ { 956, 5, 11, 7, 1, -8 }, +/* 0x88 */ { 963, 7, 9, 8, 0, -8 }, +/* 0x89 */ { 971, 12, 9, 12, 0, -8 }, +/* 0x8A */ { 985, 11, 9, 13, 1, -8 }, +/* 0x8B */ { 998, 2, 3, 4, 1, -4 }, +/* 0x8C */ { 999, 11, 9, 12, 1, -8 }, +/* 0x8D */ { 1012, 6, 11, 8, 1, -10 }, +/* 0x8E */ { 1021, 9, 9, 9, 0, -8 }, +/* 0x8F */ { 1032, 7, 11, 9, 1, -8 }, +/* 0x90 */ { 1042, 6, 11, 7, 0, -8 }, +/* 0x91 */ { 1051, 1, 3, 3, 1, -8 }, +/* 0x92 */ { 1052, 1, 3, 2, 1, -8 }, +/* 0x93 */ { 1053, 3, 3, 5, 1, -8 }, +/* 0x94 */ { 1055, 3, 3, 5, 1, -8 }, +/* 0x95 */ { 1057, 3, 3, 5, 1, -5 }, +/* 0x96 */ { 1059, 6, 1, 6, 0, -3 }, +/* 0x97 */ { 1060, 12, 1, 12, 0, -3 }, +/* 0x98 */ { 1062, 0, 0, 8, 0, 0 }, +/* 0x99 */ { 1062, 11, 7, 12, 1, -8 }, +/* 0x9A */ { 1072, 9, 6, 10, 0, -5 }, +/* 0x9B */ { 1079, 2, 3, 3, 1, -4 }, +/* 0x9C */ { 1080, 9, 6, 10, 1, -5 }, +/* 0x9D */ { 1087, 4, 9, 6, 1, -8 }, +/* 0x9E */ { 1092, 6, 9, 7, 0, -8 }, +/* 0x9F */ { 1099, 5, 7, 7, 1, -5 }, +/* 0xA0 */ { 1104, 0, 0, 3, 0, 0 }, +/* 0xA1 */ { 1104, 7, 11, 7, 0, -10 }, +/* 0xA2 */ { 1114, 5, 11, 6, 0, -7 }, +/* 0xA3 */ { 1121, 5, 9, 6, 0, -8 }, +/* 0xA4 */ { 1127, 5, 4, 7, 1, -5 }, +/* 0xA5 */ { 1130, 6, 10, 7, 1, -9 }, +/* 0xA6 */ { 1138, 1, 12, 3, 1, -8 }, +/* 0xA7 */ { 1140, 5, 12, 7, 1, -8 }, +/* 0xA8 */ { 1148, 6, 11, 8, 1, -10 }, +/* 0xA9 */ { 1157, 9, 9, 10, 0, -8 }, +/* 0xAA */ { 1168, 7, 9, 9, 1, -8 }, +/* 0xAB */ { 1176, 4, 4, 6, 1, -4 }, +/* 0xAC */ { 1178, 2, 12, 3, 0, -8 }, +/* 0xAD */ { 1181, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 1181, 9, 9, 10, 0, -8 }, +/* 0xAF */ { 1192, 3, 11, 3, 0, -10 }, +/* 0xB0 */ { 1197, 4, 4, 7, 2, -8 }, +/* 0xB1 */ { 1199, 5, 7, 7, 1, -6 }, +/* 0xB2 */ { 1204, 1, 9, 3, 1, -8 }, +/* 0xB3 */ { 1206, 1, 9, 3, 1, -8 }, +/* 0xB4 */ { 1208, 3, 8, 5, 1, -7 }, +/* 0xB5 */ { 1211, 6, 9, 7, 1, -6 }, +/* 0xB6 */ { 1218, 6, 10, 6, 1, -8 }, +/* 0xB7 */ { 1226, 1, 1, 3, 1, -2 }, +/* 0xB8 */ { 1227, 6, 9, 7, 0, -8 }, +/* 0xB9 */ { 1234, 9, 9, 11, 1, -8 }, +/* 0xBA */ { 1245, 6, 6, 6, 0, -5 }, +/* 0xBB */ { 1250, 4, 4, 6, 1, -5 }, +/* 0xBC */ { 1252, 2, 12, 3, 0, -8 }, +/* 0xBD */ { 1255, 6, 9, 8, 1, -8 }, +/* 0xBE */ { 1262, 5, 6, 6, 0, -5 }, +/* 0xBF */ { 1266, 3, 9, 3, 0, -8 }, +/* 0xC0 */ { 1270, 8, 9, 8, 0, -8 }, +/* 0xC1 */ { 1279, 6, 9, 8, 1, -8 }, +/* 0xC2 */ { 1286, 6, 9, 8, 1, -8 }, +/* 0xC3 */ { 1293, 6, 9, 7, 1, -8 }, +/* 0xC4 */ { 1300, 9, 11, 10, 0, -8 }, +/* 0xC5 */ { 1313, 6, 9, 8, 1, -8 }, +/* 0xC6 */ { 1320, 9, 9, 11, 1, -8 }, +/* 0xC7 */ { 1331, 6, 9, 8, 1, -8 }, +/* 0xC8 */ { 1338, 7, 9, 9, 1, -8 }, +/* 0xC9 */ { 1346, 7, 11, 9, 1, -10 }, +/* 0xCA */ { 1356, 6, 9, 8, 1, -8 }, +/* 0xCB */ { 1363, 7, 9, 8, 0, -8 }, +/* 0xCC */ { 1371, 8, 9, 10, 1, -8 }, +/* 0xCD */ { 1380, 7, 9, 9, 1, -8 }, +/* 0xCE */ { 1388, 8, 9, 10, 1, -8 }, +/* 0xCF */ { 1397, 7, 9, 9, 1, -8 }, +/* 0xD0 */ { 1405, 6, 9, 8, 1, -8 }, +/* 0xD1 */ { 1412, 7, 9, 9, 1, -8 }, +/* 0xD2 */ { 1420, 7, 9, 7, 0, -8 }, +/* 0xD3 */ { 1428, 7, 9, 7, 0, -8 }, +/* 0xD4 */ { 1436, 9, 9, 10, 1, -8 }, +/* 0xD5 */ { 1447, 6, 9, 8, 1, -8 }, +/* 0xD6 */ { 1454, 8, 11, 9, 1, -8 }, +/* 0xD7 */ { 1465, 6, 9, 8, 1, -8 }, +/* 0xD8 */ { 1472, 8, 9, 10, 1, -8 }, +/* 0xD9 */ { 1481, 9, 11, 10, 1, -8 }, +/* 0xDA */ { 1494, 10, 9, 10, 0, -8 }, +/* 0xDB */ { 1506, 9, 9, 10, 1, -8 }, +/* 0xDC */ { 1517, 6, 9, 8, 1, -8 }, +/* 0xDD */ { 1524, 7, 9, 9, 1, -8 }, +/* 0xDE */ { 1532, 10, 9, 12, 1, -8 }, +/* 0xDF */ { 1544, 6, 9, 8, 1, -8 }, +/* 0xE0 */ { 1551, 6, 6, 7, 0, -5 }, +/* 0xE1 */ { 1556, 6, 9, 7, 0, -8 }, +/* 0xE2 */ { 1563, 5, 6, 6, 1, -5 }, +/* 0xE3 */ { 1567, 4, 6, 5, 1, -5 }, +/* 0xE4 */ { 1570, 7, 7, 7, 0, -5 }, +/* 0xE5 */ { 1577, 6, 6, 7, 0, -5 }, +/* 0xE6 */ { 1582, 8, 6, 9, 1, -5 }, +/* 0xE7 */ { 1588, 6, 6, 6, 0, -5 }, +/* 0xE8 */ { 1593, 5, 6, 7, 1, -5 }, +/* 0xE9 */ { 1597, 5, 8, 7, 1, -7 }, +/* 0xEA */ { 1602, 4, 6, 6, 1, -5 }, +/* 0xEB */ { 1605, 5, 6, 6, 0, -5 }, +/* 0xEC */ { 1609, 6, 6, 7, 1, -5 }, +/* 0xED */ { 1614, 5, 6, 7, 1, -5 }, +/* 0xEE */ { 1618, 6, 6, 7, 0, -5 }, +/* 0xEF */ { 1623, 5, 6, 7, 1, -5 }, +/* 0xF0 */ { 1627, 5, 9, 7, 1, -5 }, +/* 0xF1 */ { 1633, 6, 6, 6, 0, -5 }, +/* 0xF2 */ { 1638, 5, 6, 5, 0, -5 }, +/* 0xF3 */ { 1642, 5, 9, 6, 0, -5 }, +/* 0xF4 */ { 1648, 10, 11, 10, 0, -7 }, +/* 0xF5 */ { 1662, 5, 6, 6, 0, -5 }, +/* 0xF6 */ { 1666, 6, 7, 7, 1, -5 }, +/* 0xF7 */ { 1672, 4, 6, 6, 1, -5 }, +/* 0xF8 */ { 1675, 6, 6, 8, 1, -5 }, +/* 0xF9 */ { 1680, 7, 7, 9, 1, -5 }, +/* 0xFA */ { 1687, 7, 6, 8, 0, -5 }, +/* 0xFB */ { 1693, 6, 6, 8, 1, -5 }, +/* 0xFC */ { 1698, 5, 6, 6, 1, -5 }, +/* 0xFD */ { 1702, 5, 6, 6, 1, -5 }, +/* 0xFE */ { 1706, 8, 6, 9, 1, -5 }, +/* 0xFF */ { 1712, 5, 6, 7, 1, -5 }, }; -const GFXfont FreeSans6pt_Win1251 PROGMEM = {(uint8_t *)FreeSans6pt_Win1251Bitmaps, (GFXglyph *)FreeSans6pt_Win1251Glyphs, 0x20, - 0xFF, 14}; +const GFXfont FreeSans6pt_Win1251 PROGMEM = { +(uint8_t*)FreeSans6pt_Win1251Bitmaps, +(GFXglyph*)FreeSans6pt_Win1251Glyphs, +0x01, 0xFF, 14 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h index 32f995270..b17be8756 100644 --- a/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h @@ -1,457 +1,527 @@ +// trunk-ignore-all(clang-format) #pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1252 +*/ const uint8_t FreeSans6pt_Win1252Bitmaps[] PROGMEM = { - /* ' ' 0x20 */ - 0xFC, 0x80, /* '!' 0x21 */ - 0xB6, 0x80, /* '"' 0x22 */ - 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '#' 0x23 */ - 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '$' 0x24 */ - 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '%' 0x25 */ - 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* '&' 0x26 */ - 0xE0, /* ''' 0x27 */ - 0x5A, 0xAA, 0x94, /* '(' 0x28 */ - 0x89, 0x12, 0x49, 0x29, 0x00, /* ')' 0x29 */ - 0x5E, 0x80, /* '*' 0x2A */ - 0x21, 0x3E, 0x42, 0x00, /* '+' 0x2B */ - 0xE0, /* ',' 0x2C */ - 0xC0, /* '-' 0x2D */ - 0x80, /* '.' 0x2E */ - 0x24, 0xA4, 0xA4, 0x80, /* '/' 0x2F */ - 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '0' 0x30 */ - 0x27, 0x92, 0x49, 0x20, /* '1' 0x31 */ - 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '2' 0x32 */ - 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '3' 0x33 */ - 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '4' 0x34 */ - 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '5' 0x35 */ - 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '6' 0x36 */ - 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '7' 0x37 */ - 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '8' 0x38 */ - 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* '9' 0x39 */ - 0x82, /* ':' 0x3A */ - 0x87, /* ';' 0x3B */ - 0x3E, 0x30, 0x60, 0x80, /* '<' 0x3C */ - 0xF8, 0x3E, /* '=' 0x3D */ - 0xE0, 0xC6, 0xC8, 0x00, /* '>' 0x3E */ - 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '?' 0x3F */ - 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* '@' 0x40 */ - 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'A' 0x41 */ - 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'B' 0x42 */ - 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'C' 0x43 */ - 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'D' 0x44 */ - 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'E' 0x45 */ - 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'F' 0x46 */ - 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'G' 0x47 */ - 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'H' 0x48 */ - 0xFF, 0x80, /* 'I' 0x49 */ - 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'J' 0x4A */ - 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'K' 0x4B */ - 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'L' 0x4C */ - 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'M' 0x4D */ - 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'N' 0x4E */ - 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'O' 0x4F */ - 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'P' 0x50 */ - 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'Q' 0x51 */ - 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'R' 0x52 */ - 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'S' 0x53 */ - 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'T' 0x54 */ - 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'U' 0x55 */ - 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'V' 0x56 */ - 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'W' 0x57 */ - 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'X' 0x58 */ - 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Y' 0x59 */ - 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* 'Z' 0x5A */ - 0xEA, 0xAA, 0xAB, /* '[' 0x5B */ - 0x92, 0x24, 0x89, 0x20, /* '\' 0x5C */ - 0xD5, 0x55, 0x57, /* ']' 0x5D */ - 0x46, 0xA9, /* '^' 0x5E */ - 0xFE, /* '_' 0x5F */ - 0x80, /* '`' 0x60 */ - 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'a' 0x61 */ - 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'b' 0x62 */ - 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'c' 0x63 */ - 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'd' 0x64 */ - 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'e' 0x65 */ - 0x6B, 0xA4, 0x92, 0x40, /* 'f' 0x66 */ - 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'g' 0x67 */ - 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'h' 0x68 */ - 0xBF, 0x80, /* 'i' 0x69 */ - 0x45, 0x55, 0x57, /* 'j' 0x6A */ - 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'k' 0x6B */ - 0xFF, 0x80, /* 'l' 0x6C */ - 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'm' 0x6D */ - 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'n' 0x6E */ - 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'o' 0x6F */ - 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'p' 0x70 */ - 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'q' 0x71 */ - 0xF2, 0x49, 0x20, /* 'r' 0x72 */ - 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 's' 0x73 */ - 0x5D, 0x24, 0x93, /* 't' 0x74 */ - 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'u' 0x75 */ - 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'v' 0x76 */ - 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'w' 0x77 */ - 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'x' 0x78 */ - 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'y' 0x79 */ - 0x78, 0x44, 0x46, 0x23, 0xE0, /* 'z' 0x7A */ - 0x6A, 0xAA, 0xA9, /* '{' 0x7B */ - 0xFF, 0xE0, /* '|' 0x7C */ - 0x95, 0x55, 0x56, /* '}' 0x7D */ - 0x66, 0x60, /* '~' 0x7E */ - 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x7F */ - 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x80 */ - /* 0x81 */ - 0xE0, /* 0x82 */ - 0x6B, 0xA4, 0x92, 0x49, 0x60, /* 0x83 */ - 0xB6, 0x80, /* 0x84 */ - 0xA8, /* 0x85 */ - 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x86 */ - 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x87 */ - 0x54, /* 0x88 */ - 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x89 */ - 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8A */ - 0x64, /* 0x8B */ - 0x3B, 0xE8, 0xC2, 0x08, 0x41, 0x08, 0x3F, 0x04, 0x20, 0x82, 0x30, 0x3B, 0xE0, /* 0x8C */ - /* 0x8D */ - 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x8E */ - /* 0x8F */ - /* 0x90 */ - 0xE0, /* 0x91 */ - 0xE0, /* 0x92 */ - 0xB6, 0x80, /* 0x93 */ - 0xB6, 0x80, /* 0x94 */ - 0xFF, 0x80, /* 0x95 */ - 0xFC, /* 0x96 */ - 0xFF, 0xF0, /* 0x97 */ - 0xDB, /* 0x98 */ - 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x99 */ - 0x52, 0x69, 0x8E, 0x19, 0x60, /* 0x9A */ - 0x98, /* 0x9B */ - 0x7B, 0xD9, 0xCE, 0x10, 0xC3, 0xF8, 0x41, 0x9C, 0x5E, 0xF0, /* 0x9C */ - /* 0x9D */ - 0x51, 0x1E, 0x11, 0x11, 0x88, 0xF8, /* 0x9E */ - 0x29, 0x05, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0x9F */ - /* 0xA0 */ - 0xBF, 0x80, /* 0xA1 */ - 0x23, 0xAB, 0x4A, 0x52, 0xAE, 0x20, /* 0xA2 */ - 0x39, 0x14, 0x10, 0xF0, 0x82, 0x1C, 0x4C, /* 0xA3 */ - 0xFC, 0x63, 0xF0, /* 0xA4 */ - 0x8C, 0x54, 0xAF, 0x93, 0xE4, 0x20, /* 0xA5 */ - 0xF9, 0xF0, /* 0xA6 */ - 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA7 */ - 0xA0, /* 0xA8 */ - 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xA9 */ - 0x61, 0x79, 0x60, /* 0xAA */ - 0x5A, 0xA5, /* 0xAB */ - 0xFC, 0x10, 0x40, /* 0xAC */ - /* 0xAD */ - 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAE */ - 0xE0, /* 0xAF */ - 0x69, 0x96, /* 0xB0 */ - 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB1 */ - 0x69, 0x3C, 0xF0, /* 0xB2 */ - 0x79, 0x29, 0x70, /* 0xB3 */ - 0x80, /* 0xB4 */ - 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB5 */ - 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB6 */ - 0x80, /* 0xB7 */ - 0x67, 0x80, /* 0xB8 */ - 0x75, 0x50, /* 0xB9 */ - 0x69, 0x96, 0xF0, /* 0xBA */ - 0xA5, 0x5A, /* 0xBB */ - 0x42, 0x30, 0x84, 0x41, 0x10, 0x48, 0x82, 0x61, 0x28, 0x8F, 0x20, 0x80, /* 0xBC */ - 0x40, 0x63, 0x11, 0x09, 0x74, 0xA8, 0x84, 0x44, 0x44, 0x43, 0x80, /* 0xBD */ - 0x71, 0x24, 0x82, 0x20, 0x50, 0x98, 0x9A, 0x61, 0x28, 0x4F, 0x20, 0x80, /* 0xBE */ - 0x20, 0x08, 0x44, 0x42, 0x11, 0x70, /* 0xBF */ - 0x10, 0x08, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC0 */ - 0x08, 0x10, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC1 */ - 0x18, 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC2 */ - 0x34, 0x2C, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC3 */ - 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 0xC4 */ - 0x18, 0x24, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC5 */ - 0x1F, 0xC5, 0x02, 0x40, 0x90, 0x47, 0xDF, 0x04, 0x42, 0x10, 0x87, 0xC0, /* 0xC6 */ - 0x3E, 0x61, 0xC0, 0x80, 0x80, 0x80, 0xC1, 0x63, 0x3E, 0x0C, 0x04, 0x1C, /* 0xC7 */ - 0x20, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xC8 */ - 0x08, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xC9 */ - 0x10, 0xA0, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xCA */ - 0x28, 0x0F, 0xE0, 0x83, 0xE8, 0x20, 0x83, 0xF0, /* 0xCB */ - 0x91, 0x55, 0x50, /* 0xCC */ - 0x62, 0xAA, 0xA0, /* 0xCD */ - 0x54, 0x24, 0x92, 0x48, /* 0xCE */ - 0xA1, 0x24, 0x92, 0x48, /* 0xCF */ - 0x7C, 0x42, 0x41, 0x41, 0xF1, 0x41, 0x41, 0x42, 0x7C, /* 0xD0 */ - 0x14, 0x53, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, /* 0xD1 */ - 0x10, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, /* 0xD2 */ - 0x04, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, /* 0xD3 */ - 0x08, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD4 */ - 0x1A, 0x0B, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD5 */ - 0x14, 0x00, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD6 */ - 0x8A, 0x88, 0xA8, 0x80, /* 0xD7 */ - 0x3E, 0xB1, 0xB0, 0xF0, 0x98, 0x8C, 0x87, 0x86, 0xC6, 0xBE, 0x00, /* 0xD8 */ - 0x20, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xD9 */ - 0x08, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDA */ - 0x10, 0x52, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDB */ - 0x29, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDC */ - 0x09, 0x25, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0xDD */ - 0x83, 0xE8, 0x61, 0x87, 0xE8, 0x20, 0x80, /* 0xDE */ - 0x7A, 0x18, 0x61, 0x8A, 0x18, 0x61, 0xB8, /* 0xDF */ - 0x20, 0x20, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE0 */ - 0x10, 0x40, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE1 */ - 0x10, 0x50, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE2 */ - 0x68, 0xB0, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE3 */ - 0x28, 0x01, 0xE4, 0x20, 0x47, 0xB1, 0x46, 0x76, /* 0xE4 */ - 0x10, 0x50, 0x43, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE5 */ - 0x7B, 0xA1, 0x90, 0x45, 0xFF, 0x84, 0x23, 0x17, 0x38, /* 0xE6 */ - 0x7B, 0x18, 0x20, 0x83, 0x17, 0x8C, 0x11, 0xC0, /* 0xE7 */ - 0x20, 0x40, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xE8 */ - 0x10, 0x80, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xE9 */ - 0x10, 0xA0, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xEA */ - 0x28, 0x07, 0xB3, 0x87, 0xF8, 0x31, 0x78, /* 0xEB */ - 0x91, 0x55, 0x50, /* 0xEC */ - 0x62, 0xAA, 0xA0, /* 0xED */ - 0x54, 0x24, 0x92, 0x48, /* 0xEE */ - 0xA1, 0x24, 0x92, 0x40, /* 0xEF */ - 0x28, 0x42, 0x8F, 0x46, 0x18, 0x52, 0x30, /* 0xF0 */ - 0x6A, 0xC1, 0x6C, 0xC6, 0x31, 0x8C, 0x40, /* 0xF1 */ - 0x20, 0x40, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF2 */ - 0x10, 0x80, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF3 */ - 0x10, 0xA0, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF4 */ - 0x69, 0x60, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF5 */ - 0x28, 0x07, 0xB3, 0x86, 0x18, 0x73, 0x78, /* 0xF6 */ - 0x20, 0x3E, 0x02, 0x00, /* 0xF7 */ - 0x7F, 0x39, 0x69, 0xC7, 0x3F, 0x80, /* 0xF8 */ - 0x41, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xF9 */ - 0x11, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFA */ - 0x22, 0x81, 0x18, 0xC6, 0x31, 0x9B, 0x40, /* 0xFB */ - 0x50, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFC */ - 0x10, 0x88, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, /* 0xFD */ - 0x84, 0x3D, 0xB8, 0xC6, 0x3B, 0xF4, 0x20, /* 0xFE */ - 0x28, 0x08, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, /* 0xFF */ +/* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, +/* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, +/* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, +/* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, +/* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, +/* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, +/* 0x07 */ +/* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, +/* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, +/* 0x0A */ +/* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, +/* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, +/* 0x0D */ +/* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, +/* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, +/* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, +/* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, +/* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, +/* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, +/* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, +/* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, +/* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, +/* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, +/* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, +/* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, +/* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, +/* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, +/* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, +/* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, +/* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, +/* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFC, 0x80, +/* '"' 0x22 */ 0xB6, 0x80, +/* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, +/* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, +/* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, +/* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, +/* ''' 0x27 */ 0xE0, +/* '(' 0x28 */ 0x5A, 0xAA, 0x94, +/* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, +/* '*' 0x2A */ 0x5E, 0x80, +/* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, +/* ',' 0x2C */ 0xE0, +/* '-' 0x2D */ 0xC0, +/* '.' 0x2E */ 0x80, +/* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, +/* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, +/* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, +/* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, +/* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, +/* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, +/* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, +/* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, +/* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, +/* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, +/* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, +/* ':' 0x3A */ 0x82, +/* ';' 0x3B */ 0x87, +/* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, +/* '=' 0x3D */ 0xF8, 0x3E, +/* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, +/* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, +/* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, +/* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, +/* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, +/* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, +/* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, +/* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, +/* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 'I' 0x49 */ 0xFF, 0x80, +/* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, +/* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, +/* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, +/* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, +/* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, +/* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, +/* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, +/* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, +/* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, +/* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, +/* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, +/* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, +/* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, +/* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, +/* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, +/* '[' 0x5B */ 0xEA, 0xAA, 0xAB, +/* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, +/* ']' 0x5D */ 0xD5, 0x55, 0x57, +/* '^' 0x5E */ 0x46, 0xA9, +/* '_' 0x5F */ 0xFE, +/* '`' 0x60 */ 0x80, +/* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, +/* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, +/* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, +/* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, +/* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, +/* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, +/* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, +/* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, +/* 'i' 0x69 */ 0xBF, 0x80, +/* 'j' 0x6A */ 0x45, 0x55, 0x57, +/* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, +/* 'l' 0x6C */ 0xFF, 0x80, +/* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, +/* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, +/* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, +/* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, +/* 'r' 0x72 */ 0xF2, 0x49, 0x20, +/* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, +/* 't' 0x74 */ 0x5D, 0x24, 0x93, +/* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, +/* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, +/* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, +/* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, +/* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, +/* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, +/* '{' 0x7B */ 0x6A, 0xAA, 0xA9, +/* '|' 0x7C */ 0xFF, 0xE0, +/* '}' 0x7D */ 0x95, 0x55, 0x56, +/* '~' 0x7E */ 0x66, 0x60, +/* 0x7F */ +/* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, +/* 0x81 */ +/* 0x82 */ 0xE0, +/* 0x83 */ 0x6B, 0xA4, 0x92, 0x49, 0x60, +/* 0x84 */ 0xB6, 0x80, +/* 0x85 */ 0xA8, +/* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, +/* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, +/* 0x88 */ 0x54, +/* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, +/* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, +/* 0x8B */ 0x64, +/* 0x8C */ 0x3B, 0xE8, 0xC2, 0x08, 0x41, 0x08, 0x3F, 0x04, 0x20, 0x82, 0x30, 0x3B, 0xE0, +/* 0x8D */ +/* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0xE0, +/* 0x92 */ 0xE0, +/* 0x93 */ 0xB6, 0x80, +/* 0x94 */ 0xB6, 0x80, +/* 0x95 */ 0xFF, 0x80, +/* 0x96 */ 0xFC, +/* 0x97 */ 0xFF, 0xF0, +/* 0x98 */ 0xDB, +/* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, +/* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, +/* 0x9B */ 0x98, +/* 0x9C */ 0x7B, 0xD9, 0xCE, 0x10, 0xC3, 0xF8, 0x41, 0x9C, 0x5E, 0xF0, +/* 0x9D */ +/* 0x9E */ 0x51, 0x1E, 0x11, 0x11, 0x88, 0xF8, +/* 0x9F */ 0x29, 0x05, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, +/* 0xA0 */ +/* 0xA1 */ 0xBF, 0x80, +/* 0xA2 */ 0x23, 0xAB, 0x4A, 0x52, 0xAE, 0x20, +/* 0xA3 */ 0x39, 0x14, 0x10, 0xF0, 0x82, 0x1C, 0x4C, +/* 0xA4 */ 0xFC, 0x63, 0xF0, +/* 0xA5 */ 0x8C, 0x54, 0xAF, 0x93, 0xE4, 0x20, +/* 0xA6 */ 0xF9, 0xF0, +/* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, +/* 0xA8 */ 0xA0, +/* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, +/* 0xAA */ 0x61, 0x79, 0x60, +/* 0xAB */ 0x5A, 0xA5, +/* 0xAC */ 0xFC, 0x10, 0x40, +/* 0xAD */ +/* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, +/* 0xAF */ 0xE0, +/* 0xB0 */ 0x69, 0x96, +/* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, +/* 0xB2 */ 0x69, 0x3C, 0xF0, +/* 0xB3 */ 0x79, 0x29, 0x70, +/* 0xB4 */ 0x80, +/* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, +/* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, +/* 0xB7 */ 0x80, +/* 0xB8 */ 0x67, 0x80, +/* 0xB9 */ 0x75, 0x50, +/* 0xBA */ 0x69, 0x96, 0xF0, +/* 0xBB */ 0xA5, 0x5A, +/* 0xBC */ 0x42, 0x30, 0x84, 0x41, 0x10, 0x48, 0x82, 0x61, 0x28, 0x8F, 0x20, 0x80, +/* 0xBD */ 0x40, 0x63, 0x11, 0x09, 0x74, 0xA8, 0x84, 0x44, 0x44, 0x43, 0x80, +/* 0xBE */ 0x71, 0x24, 0x82, 0x20, 0x50, 0x98, 0x9A, 0x61, 0x28, 0x4F, 0x20, 0x80, +/* 0xBF */ 0x20, 0x08, 0x44, 0x42, 0x11, 0x70, +/* 0xC0 */ 0x10, 0x08, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC1 */ 0x08, 0x10, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC2 */ 0x18, 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC3 */ 0x34, 0x2C, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC4 */ 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 0xC5 */ 0x18, 0x24, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, +/* 0xC6 */ 0x1F, 0xC5, 0x02, 0x40, 0x90, 0x47, 0xDF, 0x04, 0x42, 0x10, 0x87, 0xC0, +/* 0xC7 */ 0x3E, 0x61, 0xC0, 0x80, 0x80, 0x80, 0xC1, 0x63, 0x3E, 0x0C, 0x04, 0x1C, +/* 0xC8 */ 0x20, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, +/* 0xC9 */ 0x08, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, +/* 0xCA */ 0x10, 0xA0, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, +/* 0xCB */ 0x28, 0x0F, 0xE0, 0x83, 0xE8, 0x20, 0x83, 0xF0, +/* 0xCC */ 0x91, 0x55, 0x50, +/* 0xCD */ 0x62, 0xAA, 0xA0, +/* 0xCE */ 0x54, 0x24, 0x92, 0x48, +/* 0xCF */ 0xA1, 0x24, 0x92, 0x48, +/* 0xD0 */ 0x7C, 0x42, 0x41, 0x41, 0xF1, 0x41, 0x41, 0x42, 0x7C, +/* 0xD1 */ 0x14, 0x53, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, +/* 0xD2 */ 0x10, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, +/* 0xD3 */ 0x04, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, +/* 0xD4 */ 0x08, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, +/* 0xD5 */ 0x1A, 0x0B, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, +/* 0xD6 */ 0x14, 0x00, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, +/* 0xD7 */ 0x8A, 0x88, 0xA8, 0x80, +/* 0xD8 */ 0x3E, 0xB1, 0xB0, 0xF0, 0x98, 0x8C, 0x87, 0x86, 0xC6, 0xBE, 0x00, +/* 0xD9 */ 0x20, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDA */ 0x08, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDB */ 0x10, 0x52, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDC */ 0x29, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, +/* 0xDD */ 0x09, 0x25, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, +/* 0xDE */ 0x83, 0xE8, 0x61, 0x87, 0xE8, 0x20, 0x80, +/* 0xDF */ 0x7A, 0x18, 0x61, 0x8A, 0x18, 0x61, 0xB8, +/* 0xE0 */ 0x20, 0x20, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE1 */ 0x10, 0x40, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE2 */ 0x10, 0x50, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE3 */ 0x68, 0xB0, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE4 */ 0x28, 0x01, 0xE4, 0x20, 0x47, 0xB1, 0x46, 0x76, +/* 0xE5 */ 0x10, 0x50, 0x43, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, +/* 0xE6 */ 0x7B, 0xA1, 0x90, 0x45, 0xFF, 0x84, 0x23, 0x17, 0x38, +/* 0xE7 */ 0x7B, 0x18, 0x20, 0x83, 0x17, 0x8C, 0x11, 0xC0, +/* 0xE8 */ 0x20, 0x40, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, +/* 0xE9 */ 0x10, 0x80, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, +/* 0xEA */ 0x10, 0xA0, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, +/* 0xEB */ 0x28, 0x07, 0xB3, 0x87, 0xF8, 0x31, 0x78, +/* 0xEC */ 0x91, 0x55, 0x50, +/* 0xED */ 0x62, 0xAA, 0xA0, +/* 0xEE */ 0x54, 0x24, 0x92, 0x48, +/* 0xEF */ 0xA1, 0x24, 0x92, 0x40, +/* 0xF0 */ 0x28, 0x42, 0x8F, 0x46, 0x18, 0x52, 0x30, +/* 0xF1 */ 0x6A, 0xC1, 0x6C, 0xC6, 0x31, 0x8C, 0x40, +/* 0xF2 */ 0x20, 0x40, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF3 */ 0x10, 0x80, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF4 */ 0x10, 0xA0, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF5 */ 0x69, 0x60, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, +/* 0xF6 */ 0x28, 0x07, 0xB3, 0x86, 0x18, 0x73, 0x78, +/* 0xF7 */ 0x20, 0x3E, 0x02, 0x00, +/* 0xF8 */ 0x7F, 0x39, 0x69, 0xC7, 0x3F, 0x80, +/* 0xF9 */ 0x41, 0x23, 0x18, 0xC6, 0x33, 0x68, +/* 0xFA */ 0x11, 0x23, 0x18, 0xC6, 0x33, 0x68, +/* 0xFB */ 0x22, 0x81, 0x18, 0xC6, 0x31, 0x9B, 0x40, +/* 0xFC */ 0x50, 0x23, 0x18, 0xC6, 0x33, 0x68, +/* 0xFD */ 0x10, 0x88, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, +/* 0xFE */ 0x84, 0x3D, 0xB8, 0xC6, 0x3B, 0xF4, 0x20, +/* 0xFF */ 0x28, 0x08, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, }; const GFXglyph FreeSans6pt_Win1252Glyphs[] PROGMEM = { - /* ' ' 0x20 */ {0, 0, 0, 3, 0, 0}, - /* '!' 0x21 */ {0, 1, 9, 4, 2, -8}, - /* '"' 0x22 */ {2, 3, 3, 4, 0, -8}, - /* '#' 0x23 */ {4, 7, 8, 7, 0, -7}, - /* '$' 0x24 */ {11, 6, 11, 7, 0, -9}, - /* '%' 0x25 */ {20, 10, 9, 11, 0, -8}, - /* '&' 0x26 */ {32, 6, 9, 8, 1, -8}, - /* ''' 0x27 */ {39, 1, 3, 2, 1, -8}, - /* '(' 0x28 */ {40, 2, 11, 4, 1, -8}, - /* ')' 0x29 */ {43, 3, 11, 4, 0, -8}, - /* '*' 0x2A */ {48, 3, 3, 5, 1, -8}, - /* '+' 0x2B */ {50, 5, 5, 7, 1, -4}, - /* ',' 0x2C */ {54, 1, 3, 3, 1, 0}, - /* '-' 0x2D */ {55, 2, 1, 4, 1, -3}, - /* '.' 0x2E */ {56, 1, 1, 3, 1, 0}, - /* '/' 0x2F */ {57, 3, 9, 3, 0, -8}, - /* '0' 0x30 */ {61, 5, 9, 7, 1, -8}, - /* '1' 0x31 */ {67, 3, 9, 7, 1, -8}, - /* '2' 0x32 */ {71, 6, 9, 7, 0, -8}, - /* '3' 0x33 */ {78, 6, 9, 7, 0, -8}, - /* '4' 0x34 */ {85, 6, 9, 7, 0, -8}, - /* '5' 0x35 */ {92, 5, 9, 7, 1, -8}, - /* '6' 0x36 */ {98, 5, 9, 7, 1, -8}, - /* '7' 0x37 */ {104, 5, 9, 7, 1, -8}, - /* '8' 0x38 */ {110, 6, 9, 7, 0, -8}, - /* '9' 0x39 */ {117, 6, 9, 7, 0, -8}, - /* ':' 0x3A */ {124, 1, 7, 3, 1, -6}, - /* ';' 0x3B */ {125, 1, 8, 3, 1, -5}, - /* '<' 0x3C */ {126, 5, 5, 7, 1, -4}, - /* '=' 0x3D */ {130, 5, 3, 7, 1, -3}, - /* '>' 0x3E */ {132, 5, 5, 7, 1, -4}, - /* '?' 0x3F */ {136, 5, 9, 7, 1, -8}, - /* '@' 0x40 */ {142, 11, 11, 12, 0, -8}, - /* 'A' 0x41 */ {158, 8, 9, 8, 0, -8}, - /* 'B' 0x42 */ {167, 6, 9, 8, 1, -8}, - /* 'C' 0x43 */ {174, 8, 9, 9, 0, -8}, - /* 'D' 0x44 */ {183, 7, 9, 8, 1, -8}, - /* 'E' 0x45 */ {191, 6, 9, 8, 1, -8}, - /* 'F' 0x46 */ {198, 6, 9, 7, 1, -8}, - /* 'G' 0x47 */ {205, 8, 9, 9, 0, -8}, - /* 'H' 0x48 */ {214, 7, 9, 9, 1, -8}, - /* 'I' 0x49 */ {222, 1, 9, 3, 1, -8}, - /* 'J' 0x4A */ {224, 5, 9, 6, 0, -8}, - /* 'K' 0x4B */ {230, 7, 9, 8, 1, -8}, - /* 'L' 0x4C */ {238, 5, 9, 7, 1, -8}, - /* 'M' 0x4D */ {244, 8, 9, 10, 1, -8}, - /* 'N' 0x4E */ {253, 7, 9, 9, 1, -8}, - /* 'O' 0x4F */ {261, 9, 9, 9, 0, -8}, - /* 'P' 0x50 */ {272, 6, 9, 8, 1, -8}, - /* 'Q' 0x51 */ {279, 9, 10, 9, 0, -8}, - /* 'R' 0x52 */ {291, 7, 9, 9, 1, -8}, - /* 'S' 0x53 */ {299, 6, 9, 8, 1, -8}, - /* 'T' 0x54 */ {306, 7, 9, 8, 0, -8}, - /* 'U' 0x55 */ {314, 7, 9, 9, 1, -8}, - /* 'V' 0x56 */ {322, 7, 9, 8, 0, -8}, - /* 'W' 0x57 */ {330, 11, 9, 11, 0, -8}, - /* 'X' 0x58 */ {343, 6, 9, 8, 1, -8}, - /* 'Y' 0x59 */ {350, 8, 9, 8, 0, -8}, - /* 'Z' 0x5A */ {359, 7, 9, 7, 0, -8}, - /* '[' 0x5B */ {367, 2, 12, 3, 1, -8}, - /* '\' 0x5C */ {370, 3, 9, 3, 0, -8}, - /* ']' 0x5D */ {374, 2, 12, 3, 0, -8}, - /* '^' 0x5E */ {377, 4, 4, 6, 1, -8}, - /* '_' 0x5F */ {379, 7, 1, 7, 0, 2}, - /* '`' 0x60 */ {380, 1, 1, 3, 1, -8}, - /* 'a' 0x61 */ {381, 6, 7, 7, 0, -6}, - /* 'b' 0x62 */ {387, 5, 9, 7, 1, -8}, - /* 'c' 0x63 */ {393, 6, 7, 6, 0, -6}, - /* 'd' 0x64 */ {399, 6, 9, 7, 0, -8}, - /* 'e' 0x65 */ {406, 6, 7, 6, 0, -6}, - /* 'f' 0x66 */ {412, 3, 9, 3, 0, -8}, - /* 'g' 0x67 */ {416, 6, 10, 7, 0, -6}, - /* 'h' 0x68 */ {424, 5, 9, 6, 1, -8}, - /* 'i' 0x69 */ {430, 1, 9, 3, 1, -8}, - /* 'j' 0x6A */ {432, 2, 12, 3, 0, -8}, - /* 'k' 0x6B */ {435, 5, 9, 6, 1, -8}, - /* 'l' 0x6C */ {441, 1, 9, 3, 1, -8}, - /* 'm' 0x6D */ {443, 8, 7, 10, 1, -6}, - /* 'n' 0x6E */ {450, 5, 7, 6, 1, -6}, - /* 'o' 0x6F */ {455, 6, 7, 6, 0, -6}, - /* 'p' 0x70 */ {461, 5, 9, 7, 1, -6}, - /* 'q' 0x71 */ {467, 6, 9, 7, 0, -6}, - /* 'r' 0x72 */ {474, 3, 7, 4, 1, -6}, - /* 's' 0x73 */ {477, 5, 7, 6, 0, -6}, - /* 't' 0x74 */ {482, 3, 8, 3, 0, -7}, - /* 'u' 0x75 */ {485, 5, 7, 6, 1, -6}, - /* 'v' 0x76 */ {490, 6, 7, 6, 0, -6}, - /* 'w' 0x77 */ {496, 8, 7, 9, 0, -6}, - /* 'x' 0x78 */ {503, 5, 7, 6, 0, -6}, - /* 'y' 0x79 */ {508, 5, 10, 6, 0, -6}, - /* 'z' 0x7A */ {515, 5, 7, 6, 0, -6}, - /* '{' 0x7B */ {520, 2, 12, 4, 1, -8}, - /* '|' 0x7C */ {523, 1, 11, 3, 1, -8}, - /* '}' 0x7D */ {525, 2, 12, 4, 1, -8}, - /* '~' 0x7E */ {528, 6, 2, 6, 0, -4}, - /* 0x7F */ {530, 9, 10, 11, 1, -8}, - /* 0x80 */ {542, 7, 9, 8, 0, -8}, - /* 0x81 */ {550, 0, 0, 8, 0, 0}, - /* 0x82 */ {550, 1, 3, 3, 1, 0}, - /* 0x83 */ {551, 3, 12, 3, 0, -8}, - /* 0x84 */ {556, 3, 3, 5, 1, 0}, - /* 0x85 */ {558, 5, 1, 7, 1, 0}, - /* 0x86 */ {559, 5, 11, 7, 1, -8}, - /* 0x87 */ {566, 5, 11, 7, 1, -8}, - /* 0x88 */ {573, 3, 2, 4, 0, -9}, - /* 0x89 */ {574, 12, 9, 12, 0, -8}, - /* 0x8A */ {588, 6, 11, 8, 1, -9}, - /* 0x8B */ {597, 2, 3, 4, 1, -4}, - /* 0x8C */ {598, 11, 9, 12, 0, -8}, - /* 0x8D */ {611, 0, 0, 8, 0, 0}, - /* 0x8E */ {611, 7, 10, 7, 0, -9}, - /* 0x8F */ {620, 0, 0, 8, 0, 0}, - /* 0x90 */ {620, 0, 0, 8, 0, 0}, - /* 0x91 */ {620, 1, 3, 3, 1, -8}, - /* 0x92 */ {621, 1, 3, 2, 1, -8}, - /* 0x93 */ {622, 3, 3, 5, 1, -8}, - /* 0x94 */ {624, 3, 3, 5, 1, -8}, - /* 0x95 */ {626, 3, 3, 5, 1, -5}, - /* 0x96 */ {628, 6, 1, 6, 0, -3}, - /* 0x97 */ {629, 12, 1, 12, 0, -3}, - /* 0x98 */ {631, 4, 2, 4, 0, -8}, - /* 0x99 */ {632, 11, 7, 12, 1, -8}, - /* 0x9A */ {642, 4, 9, 6, 1, -8}, - /* 0x9B */ {647, 2, 3, 3, 1, -4}, - /* 0x9C */ {648, 11, 7, 11, 0, -6}, - /* 0x9D */ {658, 0, 0, 8, 0, 0}, - /* 0x9E */ {658, 5, 9, 6, 0, -8}, - /* 0x9F */ {664, 7, 10, 8, 1, -9}, - /* 0xA0 */ {673, 0, 0, 3, 0, 0}, - /* 0xA1 */ {673, 1, 9, 4, 1, -5}, - /* 0xA2 */ {675, 5, 9, 7, 1, -7}, - /* 0xA3 */ {681, 6, 9, 7, 0, -8}, - /* 0xA4 */ {688, 5, 4, 7, 1, -5}, - /* 0xA5 */ {691, 5, 9, 7, 1, -8}, - /* 0xA6 */ {697, 1, 12, 3, 1, -8}, - /* 0xA7 */ {699, 5, 12, 7, 1, -8}, - /* 0xA8 */ {707, 3, 1, 4, 0, -7}, - /* 0xA9 */ {708, 9, 9, 10, 0, -8}, - /* 0xAA */ {719, 4, 5, 4, 0, -8}, - /* 0xAB */ {722, 4, 4, 6, 1, -4}, - /* 0xAC */ {724, 6, 3, 7, 1, -4}, - /* 0xAD */ {727, 0, 0, 0, 0, 0}, - /* 0xAE */ {727, 9, 9, 10, 0, -8}, - /* 0xAF */ {738, 3, 1, 4, 0, -8}, - /* 0xB0 */ {739, 4, 4, 7, 2, -8}, - /* 0xB1 */ {741, 5, 7, 7, 1, -6}, - /* 0xB2 */ {746, 4, 5, 4, 0, -9}, - /* 0xB3 */ {749, 4, 5, 4, 0, -9}, - /* 0xB4 */ {752, 1, 1, 4, 1, -8}, - /* 0xB5 */ {753, 6, 9, 7, 1, -6}, - /* 0xB6 */ {760, 6, 10, 6, 1, -8}, - /* 0xB7 */ {768, 1, 1, 3, 1, -2}, - /* 0xB8 */ {769, 3, 3, 4, 1, 1}, - /* 0xB9 */ {771, 2, 6, 4, 1, -9}, - /* 0xBA */ {773, 4, 5, 4, 0, -8}, - /* 0xBB */ {776, 4, 4, 6, 1, -5}, - /* 0xBC */ {778, 10, 9, 10, 1, -8}, - /* 0xBD */ {790, 9, 9, 10, 1, -8}, - /* 0xBE */ {801, 10, 9, 11, 0, -8}, - /* 0xBF */ {813, 5, 9, 7, 1, -5}, - /* 0xC0 */ {819, 8, 10, 8, 0, -9}, - /* 0xC1 */ {829, 8, 10, 8, 0, -9}, - /* 0xC2 */ {839, 8, 10, 8, 0, -9}, - /* 0xC3 */ {849, 8, 10, 8, 0, -9}, - /* 0xC4 */ {859, 8, 10, 8, 0, -9}, - /* 0xC5 */ {869, 8, 10, 8, 0, -9}, - /* 0xC6 */ {879, 10, 9, 12, 1, -8}, - /* 0xC7 */ {891, 8, 12, 9, 0, -8}, - /* 0xC8 */ {903, 6, 10, 8, 1, -9}, - /* 0xC9 */ {911, 6, 10, 8, 1, -9}, - /* 0xCA */ {919, 6, 10, 8, 1, -9}, - /* 0xCB */ {927, 6, 10, 8, 1, -9}, - /* 0xCC */ {935, 2, 10, 3, 0, -9}, - /* 0xCD */ {938, 2, 10, 3, 1, -9}, - /* 0xCE */ {941, 3, 10, 4, 0, -9}, - /* 0xCF */ {945, 3, 10, 4, 0, -9}, - /* 0xD0 */ {949, 8, 9, 8, 0, -8}, - /* 0xD1 */ {958, 7, 10, 9, 1, -9}, - /* 0xD2 */ {967, 9, 10, 9, 0, -9}, - /* 0xD3 */ {979, 9, 10, 9, 0, -9}, - /* 0xD4 */ {991, 9, 11, 9, 0, -10}, - /* 0xD5 */ {1004, 9, 11, 9, 0, -10}, - /* 0xD6 */ {1017, 9, 11, 9, 0, -10}, - /* 0xD7 */ {1030, 5, 5, 7, 1, -5}, - /* 0xD8 */ {1034, 9, 9, 9, 0, -8}, - /* 0xD9 */ {1045, 7, 10, 9, 1, -9}, - /* 0xDA */ {1054, 7, 10, 9, 1, -9}, - /* 0xDB */ {1063, 7, 10, 9, 1, -9}, - /* 0xDC */ {1072, 7, 10, 9, 1, -9}, - /* 0xDD */ {1081, 7, 10, 8, 1, -9}, - /* 0xDE */ {1090, 6, 9, 8, 1, -8}, - /* 0xDF */ {1097, 6, 9, 7, 1, -8}, - /* 0xE0 */ {1104, 7, 10, 7, 0, -9}, - /* 0xE1 */ {1113, 7, 10, 7, 0, -9}, - /* 0xE2 */ {1122, 7, 10, 7, 0, -9}, - /* 0xE3 */ {1131, 7, 10, 7, 0, -9}, - /* 0xE4 */ {1140, 7, 9, 7, 0, -8}, - /* 0xE5 */ {1148, 7, 10, 7, 0, -9}, - /* 0xE6 */ {1157, 10, 7, 10, 0, -6}, - /* 0xE7 */ {1166, 6, 10, 6, 0, -6}, - /* 0xE8 */ {1174, 6, 10, 6, 0, -9}, - /* 0xE9 */ {1182, 6, 10, 6, 0, -9}, - /* 0xEA */ {1190, 6, 10, 6, 0, -9}, - /* 0xEB */ {1198, 6, 9, 6, 0, -8}, - /* 0xEC */ {1205, 2, 10, 3, 0, -9}, - /* 0xED */ {1208, 2, 10, 3, 1, -9}, - /* 0xEE */ {1211, 3, 10, 3, 0, -9}, - /* 0xEF */ {1215, 3, 9, 3, 0, -8}, - /* 0xF0 */ {1219, 6, 9, 6, 0, -8}, - /* 0xF1 */ {1226, 5, 10, 6, 1, -9}, - /* 0xF2 */ {1233, 6, 10, 6, 0, -9}, - /* 0xF3 */ {1241, 6, 10, 6, 0, -9}, - /* 0xF4 */ {1249, 6, 10, 6, 0, -9}, - /* 0xF5 */ {1257, 6, 10, 6, 0, -9}, - /* 0xF6 */ {1265, 6, 9, 6, 0, -8}, - /* 0xF7 */ {1272, 5, 5, 7, 1, -5}, - /* 0xF8 */ {1276, 6, 7, 6, 0, -6}, - /* 0xF9 */ {1282, 5, 9, 6, 1, -8}, - /* 0xFA */ {1288, 5, 9, 6, 1, -8}, - /* 0xFB */ {1294, 5, 10, 6, 1, -9}, - /* 0xFC */ {1301, 5, 9, 6, 1, -8}, - /* 0xFD */ {1307, 6, 12, 6, 0, -8}, - /* 0xFE */ {1316, 5, 11, 7, 1, -8}, - /* 0xFF */ {1323, 6, 12, 6, 0, -8}, +/* 0x01 */ { 0, 9, 10, 11, 1, -9 }, +/* 0x02 */ { 12, 9, 10, 11, 1, -8 }, +/* 0x03 */ { 24, 10, 10, 12, 1, -8 }, +/* 0x04 */ { 37, 10, 10, 12, 1, -8 }, +/* 0x05 */ { 50, 10, 10, 12, 1, -9 }, +/* 0x06 */ { 63, 11, 11, 13, 1, -9 }, +/* 0x07 */ { 79, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 79, 12, 9, 14, 1, -8 }, +/* 0x09 */ { 93, 14, 8, 16, 1, -7 }, +/* 0x0A */ { 107, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 107, 9, 10, 11, 1, -9 }, +/* 0x0C */ { 119, 13, 9, 15, 1, -8 }, +/* 0x0D */ { 134, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 134, 9, 11, 11, 1, -9 }, +/* 0x0F */ { 147, 10, 10, 12, 1, -9 }, +/* 0x10 */ { 160, 11, 10, 13, 1, -9 }, +/* 0x11 */ { 174, 13, 10, 15, 1, -9 }, +/* 0x12 */ { 191, 10, 10, 12, 1, -9 }, +/* 0x13 */ { 204, 11, 10, 13, 1, -9 }, +/* 0x14 */ { 218, 10, 10, 12, 1, -9 }, +/* 0x15 */ { 231, 14, 10, 16, 1, -9 }, +/* 0x16 */ { 249, 8, 10, 10, 1, -9 }, +/* 0x17 */ { 259, 12, 10, 14, 1, -9 }, +/* 0x18 */ { 274, 13, 10, 15, 1, -9 }, +/* 0x19 */ { 291, 12, 10, 14, 1, -9 }, +/* 0x1A */ { 306, 9, 10, 11, 1, -8 }, +/* 0x1B */ { 318, 14, 10, 16, 1, -9 }, +/* 0x1C */ { 336, 11, 10, 13, 1, -9 }, +/* 0x1D */ { 350, 11, 10, 13, 1, -9 }, +/* 0x1E */ { 364, 12, 10, 14, 1, -9 }, +/* 0x1F */ { 379, 8, 10, 11, 2, -9 }, +/* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, +/* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, +/* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, +/* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, +/* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, +/* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, +/* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, +/* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, +/* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, +/* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, +/* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, +/* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, +/* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, +/* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, +/* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, +/* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, +/* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, +/* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, +/* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, +/* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, +/* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, +/* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, +/* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, +/* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, +/* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, +/* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, +/* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, +/* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, +/* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, +/* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, +/* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, +/* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, +/* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, +/* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, +/* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, +/* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, +/* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, +/* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, +/* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, +/* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, +/* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, +/* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, +/* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, +/* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, +/* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, +/* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, +/* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, +/* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, +/* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, +/* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, +/* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, +/* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, +/* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, +/* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, +/* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, +/* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, +/* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, +/* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, +/* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, +/* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, +/* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, +/* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, +/* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, +/* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, +/* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, +/* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, +/* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, +/* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, +/* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, +/* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, +/* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, +/* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, +/* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, +/* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, +/* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, +/* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, +/* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, +/* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, +/* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, +/* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, +/* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, +/* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, +/* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, +/* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, +/* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, +/* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, +/* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, +/* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, +/* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, +/* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, +/* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, +/* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, +/* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, +/* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, +/* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, +/* 0x7F */ { 919, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 919, 7, 9, 8, 0, -8 }, +/* 0x81 */ { 927, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 927, 1, 3, 3, 1, 0 }, +/* 0x83 */ { 928, 3, 12, 3, 0, -8 }, +/* 0x84 */ { 933, 3, 3, 5, 1, 0 }, +/* 0x85 */ { 935, 5, 1, 7, 1, 0 }, +/* 0x86 */ { 936, 5, 11, 7, 1, -8 }, +/* 0x87 */ { 943, 5, 11, 7, 1, -8 }, +/* 0x88 */ { 950, 3, 2, 4, 0, -9 }, +/* 0x89 */ { 951, 12, 9, 12, 0, -8 }, +/* 0x8A */ { 965, 6, 11, 8, 1, -9 }, +/* 0x8B */ { 974, 2, 3, 4, 1, -4 }, +/* 0x8C */ { 975, 11, 9, 12, 0, -8 }, +/* 0x8D */ { 988, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 988, 7, 10, 7, 0, -9 }, +/* 0x8F */ { 997, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 997, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 997, 1, 3, 3, 1, -8 }, +/* 0x92 */ { 998, 1, 3, 2, 1, -8 }, +/* 0x93 */ { 999, 3, 3, 5, 1, -8 }, +/* 0x94 */ { 1001, 3, 3, 5, 1, -8 }, +/* 0x95 */ { 1003, 3, 3, 5, 1, -5 }, +/* 0x96 */ { 1005, 6, 1, 6, 0, -3 }, +/* 0x97 */ { 1006, 12, 1, 12, 0, -3 }, +/* 0x98 */ { 1008, 4, 2, 4, 0, -8 }, +/* 0x99 */ { 1009, 11, 7, 12, 1, -8 }, +/* 0x9A */ { 1019, 4, 9, 6, 1, -8 }, +/* 0x9B */ { 1024, 2, 3, 3, 1, -4 }, +/* 0x9C */ { 1025, 11, 7, 11, 0, -6 }, +/* 0x9D */ { 1035, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 1035, 5, 9, 6, 0, -8 }, +/* 0x9F */ { 1041, 7, 10, 8, 1, -9 }, +/* 0xA0 */ { 1050, 0, 0, 3, 0, 0 }, +/* 0xA1 */ { 1050, 1, 9, 4, 1, -5 }, +/* 0xA2 */ { 1052, 5, 9, 7, 1, -7 }, +/* 0xA3 */ { 1058, 6, 9, 7, 0, -8 }, +/* 0xA4 */ { 1065, 5, 4, 7, 1, -5 }, +/* 0xA5 */ { 1068, 5, 9, 7, 1, -8 }, +/* 0xA6 */ { 1074, 1, 12, 3, 1, -8 }, +/* 0xA7 */ { 1076, 5, 12, 7, 1, -8 }, +/* 0xA8 */ { 1084, 3, 1, 4, 0, -7 }, +/* 0xA9 */ { 1085, 9, 9, 10, 0, -8 }, +/* 0xAA */ { 1096, 4, 5, 4, 0, -8 }, +/* 0xAB */ { 1099, 4, 4, 6, 1, -4 }, +/* 0xAC */ { 1101, 6, 3, 7, 1, -4 }, +/* 0xAD */ { 1104, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 1104, 9, 9, 10, 0, -8 }, +/* 0xAF */ { 1115, 3, 1, 4, 0, -8 }, +/* 0xB0 */ { 1116, 4, 4, 7, 2, -8 }, +/* 0xB1 */ { 1118, 5, 7, 7, 1, -6 }, +/* 0xB2 */ { 1123, 4, 5, 4, 0, -9 }, +/* 0xB3 */ { 1126, 4, 5, 4, 0, -9 }, +/* 0xB4 */ { 1129, 1, 1, 4, 1, -8 }, +/* 0xB5 */ { 1130, 6, 9, 7, 1, -6 }, +/* 0xB6 */ { 1137, 6, 10, 6, 1, -8 }, +/* 0xB7 */ { 1145, 1, 1, 3, 1, -2 }, +/* 0xB8 */ { 1146, 3, 3, 4, 1, 1 }, +/* 0xB9 */ { 1148, 2, 6, 4, 1, -9 }, +/* 0xBA */ { 1150, 4, 5, 4, 0, -8 }, +/* 0xBB */ { 1153, 4, 4, 6, 1, -5 }, +/* 0xBC */ { 1155, 10, 9, 10, 1, -8 }, +/* 0xBD */ { 1167, 9, 9, 10, 1, -8 }, +/* 0xBE */ { 1178, 10, 9, 11, 0, -8 }, +/* 0xBF */ { 1190, 5, 9, 7, 1, -5 }, +/* 0xC0 */ { 1196, 8, 10, 8, 0, -9 }, +/* 0xC1 */ { 1206, 8, 10, 8, 0, -9 }, +/* 0xC2 */ { 1216, 8, 10, 8, 0, -9 }, +/* 0xC3 */ { 1226, 8, 10, 8, 0, -9 }, +/* 0xC4 */ { 1236, 8, 10, 8, 0, -9 }, +/* 0xC5 */ { 1246, 8, 10, 8, 0, -9 }, +/* 0xC6 */ { 1256, 10, 9, 12, 1, -8 }, +/* 0xC7 */ { 1268, 8, 12, 9, 0, -8 }, +/* 0xC8 */ { 1280, 6, 10, 8, 1, -9 }, +/* 0xC9 */ { 1288, 6, 10, 8, 1, -9 }, +/* 0xCA */ { 1296, 6, 10, 8, 1, -9 }, +/* 0xCB */ { 1304, 6, 10, 8, 1, -9 }, +/* 0xCC */ { 1312, 2, 10, 3, 0, -9 }, +/* 0xCD */ { 1315, 2, 10, 3, 1, -9 }, +/* 0xCE */ { 1318, 3, 10, 4, 0, -9 }, +/* 0xCF */ { 1322, 3, 10, 4, 0, -9 }, +/* 0xD0 */ { 1326, 8, 9, 8, 0, -8 }, +/* 0xD1 */ { 1335, 7, 10, 9, 1, -9 }, +/* 0xD2 */ { 1344, 9, 10, 9, 0, -9 }, +/* 0xD3 */ { 1356, 9, 10, 9, 0, -9 }, +/* 0xD4 */ { 1368, 9, 11, 9, 0, -10 }, +/* 0xD5 */ { 1381, 9, 11, 9, 0, -10 }, +/* 0xD6 */ { 1394, 9, 11, 9, 0, -10 }, +/* 0xD7 */ { 1407, 5, 5, 7, 1, -5 }, +/* 0xD8 */ { 1411, 9, 9, 9, 0, -8 }, +/* 0xD9 */ { 1422, 7, 10, 9, 1, -9 }, +/* 0xDA */ { 1431, 7, 10, 9, 1, -9 }, +/* 0xDB */ { 1440, 7, 10, 9, 1, -9 }, +/* 0xDC */ { 1449, 7, 10, 9, 1, -9 }, +/* 0xDD */ { 1458, 7, 10, 8, 1, -9 }, +/* 0xDE */ { 1467, 6, 9, 8, 1, -8 }, +/* 0xDF */ { 1474, 6, 9, 7, 1, -8 }, +/* 0xE0 */ { 1481, 7, 10, 7, 0, -9 }, +/* 0xE1 */ { 1490, 7, 10, 7, 0, -9 }, +/* 0xE2 */ { 1499, 7, 10, 7, 0, -9 }, +/* 0xE3 */ { 1508, 7, 10, 7, 0, -9 }, +/* 0xE4 */ { 1517, 7, 9, 7, 0, -8 }, +/* 0xE5 */ { 1525, 7, 10, 7, 0, -9 }, +/* 0xE6 */ { 1534, 10, 7, 10, 0, -6 }, +/* 0xE7 */ { 1543, 6, 10, 6, 0, -6 }, +/* 0xE8 */ { 1551, 6, 10, 6, 0, -9 }, +/* 0xE9 */ { 1559, 6, 10, 6, 0, -9 }, +/* 0xEA */ { 1567, 6, 10, 6, 0, -9 }, +/* 0xEB */ { 1575, 6, 9, 6, 0, -8 }, +/* 0xEC */ { 1582, 2, 10, 3, 0, -9 }, +/* 0xED */ { 1585, 2, 10, 3, 1, -9 }, +/* 0xEE */ { 1588, 3, 10, 3, 0, -9 }, +/* 0xEF */ { 1592, 3, 9, 3, 0, -8 }, +/* 0xF0 */ { 1596, 6, 9, 6, 0, -8 }, +/* 0xF1 */ { 1603, 5, 10, 6, 1, -9 }, +/* 0xF2 */ { 1610, 6, 10, 6, 0, -9 }, +/* 0xF3 */ { 1618, 6, 10, 6, 0, -9 }, +/* 0xF4 */ { 1626, 6, 10, 6, 0, -9 }, +/* 0xF5 */ { 1634, 6, 10, 6, 0, -9 }, +/* 0xF6 */ { 1642, 6, 9, 6, 0, -8 }, +/* 0xF7 */ { 1649, 5, 5, 7, 1, -5 }, +/* 0xF8 */ { 1653, 6, 7, 6, 0, -6 }, +/* 0xF9 */ { 1659, 5, 9, 6, 1, -8 }, +/* 0xFA */ { 1665, 5, 9, 6, 1, -8 }, +/* 0xFB */ { 1671, 5, 10, 6, 1, -9 }, +/* 0xFC */ { 1678, 5, 9, 6, 1, -8 }, +/* 0xFD */ { 1684, 6, 12, 6, 0, -8 }, +/* 0xFE */ { 1693, 5, 11, 7, 1, -8 }, +/* 0xFF */ { 1700, 6, 12, 6, 0, -8 }, }; -const GFXfont FreeSans6pt_Win1252 PROGMEM = {(uint8_t *)FreeSans6pt_Win1252Bitmaps, (GFXglyph *)FreeSans6pt_Win1252Glyphs, 0x20, - 0xFF, 14}; +const GFXfont FreeSans6pt_Win1252 PROGMEM = { +(uint8_t*)FreeSans6pt_Win1252Bitmaps, +(GFXglyph*)FreeSans6pt_Win1252Glyphs, +0x01, 0xFF, 10 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h index 7022939a0..dd66801a1 100644 --- a/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h @@ -1,494 +1,527 @@ +// trunk-ignore-all(clang-format) #pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1250 +*/ const uint8_t FreeSans9pt_Win1250Bitmaps[] PROGMEM = { - /* ' ' 0x20 */ - 0xFF, 0xFF, 0xF0, 0xC0, /* '!' 0x21 */ - 0xDE, 0xF7, 0x20, /* '"' 0x22 */ - 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '#' 0x23 */ - 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '$' 0x24 */ - 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, - 0x63, 0x04, 0x77, 0x08, 0x3C, /* '%' 0x25 */ - 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* '&' 0x26 */ - 0xFE, /* ''' 0x27 */ - 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* '(' 0x28 */ - 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* ')' 0x29 */ - 0x25, 0x7E, 0xA5, 0x00, /* '*' 0x2A */ - 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* '+' 0x2B */ - 0xD6, /* ',' 0x2C */ - 0xF0, /* '-' 0x2D */ - 0xC0, /* '.' 0x2E */ - 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '/' 0x2F */ - 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '0' 0x30 */ - 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '1' 0x31 */ - 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '2' 0x32 */ - 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '3' 0x33 */ - 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '4' 0x34 */ - 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '5' 0x35 */ - 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ - 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '7' 0x37 */ - 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '8' 0x38 */ - 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* '9' 0x39 */ - 0xC0, 0x00, 0x30, /* ':' 0x3A */ - 0xC0, 0x00, 0x00, 0x64, 0xA0, /* ';' 0x3B */ - 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '<' 0x3C */ - 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '=' 0x3D */ - 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '>' 0x3E */ - 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '?' 0x3F */ - 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, - 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* '@' 0x40 */ - 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, - 0x30, /* 'A' 0x41 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'B' 0x42 */ - 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'C' 0x43 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'D' 0x44 */ - 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'E' 0x45 */ - 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'F' 0x46 */ - 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, - 0x10, /* 'G' 0x47 */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'H' 0x48 */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 'I' 0x49 */ - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'J' 0x4A */ - 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'K' 0x4B */ - 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'L' 0x4C */ - 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, - 0x80, /* 'M' 0x4D */ - 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'N' 0x4E */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, - 0x00, /* 'O' 0x4F */ - 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'P' 0x50 */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, - 0x00, 0x08, /* 'Q' 0x51 */ - 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, - 0x70, /* 'R' 0x52 */ - 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'S' 0x53 */ - 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'T' 0x54 */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'U' 0x55 */ - 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'V' 0x56 */ - 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, - 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'W' 0x57 */ - 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'X' 0x58 */ - 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, - 0x00, /* 'Y' 0x59 */ - 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* 'Z' 0x5A */ - 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '[' 0x5B */ - 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* '\' 0x5C */ - 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* ']' 0x5D */ - 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '^' 0x5E */ - 0xFF, 0xC0, /* '_' 0x5F */ - 0xC6, 0x30, /* '`' 0x60 */ - 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'a' 0x61 */ - 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'b' 0x62 */ - 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'c' 0x63 */ - 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'd' 0x64 */ - 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'e' 0x65 */ - 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'f' 0x66 */ - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'g' 0x67 */ - 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'h' 0x68 */ - 0xC3, 0xFF, 0xFF, 0xC0, /* 'i' 0x69 */ - 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'j' 0x6A */ - 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'k' 0x6B */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 'l' 0x6C */ - 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'm' 0x6D */ - 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'n' 0x6E */ - 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'o' 0x6F */ - 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'p' 0x70 */ - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'q' 0x71 */ - 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 'r' 0x72 */ - 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 's' 0x73 */ - 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 't' 0x74 */ - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'u' 0x75 */ - 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'v' 0x76 */ - 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'w' 0x77 */ - 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'x' 0x78 */ - 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'y' 0x79 */ - 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* 'z' 0x7A */ - 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '{' 0x7B */ - 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '|' 0x7C */ - 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '}' 0x7D */ - 0x61, 0x24, 0x38, /* '~' 0x7E */ - 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, - 0xFF, 0xFC, /* 0x7F */ - 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x80 */ - /* 0x81 */ - 0xDC, /* 0x82 */ - /* 0x83 */ - 0xDA, 0x76, /* 0x84 */ - 0xCC, 0xC0, /* 0x85 */ - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x86 */ - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ - /* 0x88 */ - 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, - 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x89 */ - 0x1B, 0x03, 0x83, 0xF1, 0x86, 0xC0, 0xF0, 0x3C, 0x01, 0xE0, 0x1F, 0x00, 0xE0, 0x0F, 0x03, 0xC0, 0xD8, 0x63, 0xF0, /* 0x8A */ - 0x69, /* 0x8B */ - 0x06, 0x03, 0x03, 0xF1, 0x86, 0xC0, 0xF0, 0x3C, 0x01, 0xE0, 0x1F, 0x00, 0xE0, 0x0F, 0x03, 0xC0, 0xD8, 0x63, 0xF0, /* 0x8C */ - 0x33, 0x0F, 0x3F, 0xE1, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, /* 0x8D */ - 0x1B, 0x03, 0x8F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, /* 0x8E */ - 0x0C, 0x06, 0x0F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, /* 0x8F */ - /* 0x90 */ - 0x6B, /* 0x91 */ - 0xD6, /* 0x92 */ - 0x4C, 0xA5, 0xB0, /* 0x93 */ - 0xDA, 0x53, 0x20, /* 0x94 */ - 0x6F, 0xFF, 0x60, /* 0x95 */ - 0xFE, /* 0x96 */ - 0xFF, 0xFF, /* 0x97 */ - /* 0x98 */ - 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, - 0x33, 0x30, /* 0x99 */ - 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9A */ - 0x96, /* 0x9B */ - 0x0C, 0x18, 0x10, 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9C */ - 0x0D, 0xA7, 0x3C, 0x61, 0x86, 0x18, 0x61, 0x86, 0x18, 0x70, /* 0x9D */ - 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0x9E */ - 0x0C, 0x10, 0x47, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0x9F */ - /* 0xA0 */ - 0x8A, 0x9C, /* 0xA1 */ - 0x85, 0xE0, /* 0xA2 */ - 0x60, 0x30, 0x18, 0x0C, 0x86, 0xC3, 0xC1, 0xC1, 0xC0, 0xE0, 0x30, 0x18, 0x0C, 0x07, 0xF8, /* 0xA3 */ - 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA4 */ - 0x06, 0x00, 0xF0, 0x0F, 0x01, 0x30, 0x13, 0x81, 0x38, 0x21, 0x82, 0x1C, 0x3F, 0xC6, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, 0x06, - 0x00, 0xC0, 0x0C, 0x00, 0x70, /* 0xA5 */ - 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA6 */ - 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, - 0x00, /* 0xA7 */ - 0xCC, /* 0xA8 */ - 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, - 0x0F, 0xC0, /* 0xA9 */ - 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x02, 0x00, 0xE0, 0x18, 0x1C, - 0x00, /* 0xAA */ - 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAB */ - 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAC */ - /* 0xAD */ - 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, - 0x0F, 0xC0, /* 0xAE */ - 0x0C, 0x00, 0x0F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, /* 0xAF */ - 0x74, 0x63, 0x17, 0x00, /* 0xB0 */ - 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB1 */ - 0x6C, 0xC7, /* 0xB2 */ - 0x66, 0x66, 0x67, 0x6E, 0x66, 0x66, 0x60, /* 0xB3 */ - 0x36, 0xC0, /* 0xB4 */ - 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB5 */ - 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB6 */ - 0xE0, /* 0xB7 */ - 0x21, 0xC7, 0xE0, /* 0xB8 */ - 0x7E, 0x38, 0xCC, 0x30, 0x0C, 0x0F, 0x1E, 0xCC, 0x33, 0x0C, 0xC7, 0x1E, 0xE0, 0x10, 0x0C, 0x03, 0x00, 0x70, /* 0xB9 */ - 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xC3, 0x7E, 0x10, 0x1C, 0x0C, 0x38, /* 0xBA */ - 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBB */ - 0xC6, 0xC4, 0xC8, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 0xBC */ - 0x6F, 0x69, 0x00, /* 0xBD */ - 0xDE, 0xB9, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0xBE */ - 0x30, 0x03, 0xF8, 0x30, 0xC3, 0x06, 0x18, 0x60, 0x83, 0x07, 0xF0, /* 0xBF */ - 0x06, 0x00, 0xC0, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, - 0xC0, 0x70, /* 0xC0 */ - 0x06, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC1 */ - 0x0C, 0x04, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC2 */ - 0x21, 0x07, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC3 */ - 0x33, 0x00, 0x00, 0xC0, 0x78, 0x1E, 0x04, 0x83, 0x30, 0xCC, 0x33, 0x1F, 0xE6, 0x19, 0x02, 0xC0, 0xF0, 0x30, /* 0xC4 */ - 0x30, 0x60, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 0xC5 */ - 0x06, 0x01, 0x80, 0x00, 0x0F, 0xC3, 0x0C, 0xC0, 0xD0, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0x01, 0xA0, 0x36, 0x0C, 0x61, 0x87, - 0xC0, /* 0xC6 */ - 0x1F, 0x06, 0x19, 0x83, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0xE1, 0xF0, 0x08, 0x01, 0xC0, - 0x18, 0x0E, 0x00, /* 0xC7 */ - 0x19, 0x81, 0xE0, 0x00, 0x0F, 0xC3, 0x0C, 0xC0, 0xF0, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0x01, 0xA0, 0x36, 0x0C, 0x61, 0x87, - 0xC0, /* 0xC8 */ - 0x0C, 0x0C, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xC9 */ - 0xFF, 0xD8, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x3F, 0xF6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0F, 0xFC, 0x01, 0x80, 0x60, - 0x0C, 0x00, 0xE0, /* 0xCA */ - 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0xFE, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCB */ - 0x33, 0x0F, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFE, /* 0xCC */ - 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xC0, /* 0xCD */ - 0x76, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* 0xCE */ - 0x66, 0x0F, 0x00, 0x03, 0xF8, 0xC3, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC1, 0xB0, 0xEF, 0xE0, /* 0xCF */ - 0x7F, 0x0C, 0x31, 0x83, 0x30, 0x36, 0x06, 0xC0, 0xFE, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x30, 0xE7, 0xF0, /* 0xD0 */ - 0x03, 0x01, 0x83, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, - 0xC0, /* 0xD1 */ - 0x19, 0x81, 0xE3, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, - 0xC0, /* 0xD2 */ - 0x03, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD3 */ - 0x0F, 0x01, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD4 */ - 0x0D, 0x81, 0xB0, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD5 */ - 0x19, 0x81, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD6 */ - 0x83, 0x89, 0xA1, 0x83, 0x89, 0xA1, 0x80, /* 0xD7 */ - 0x33, 0x01, 0xE0, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, - 0xC0, 0x70, /* 0xD8 */ - 0x04, 0x01, 0x43, 0x11, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xD9 */ - 0x06, 0x01, 0x83, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xDA */ - 0x0D, 0x83, 0x63, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xDB */ - 0x1B, 0x00, 0x03, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xDC */ - 0x03, 0x0C, 0x63, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x1D, 0x80, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, - 0x60, /* 0xDD */ - 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x40, 0x3C, 0x06, 0x1E, - 0x00, /* 0xDE */ - 0x3C, 0x33, 0x30, 0xD8, 0x6C, 0x36, 0x33, 0x39, 0x86, 0xC1, 0xE0, 0xF0, 0x78, 0x6D, 0xE0, /* 0xDF */ - 0x19, 0x89, 0xBE, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0xE0 */ - 0x0C, 0x04, 0x04, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE1 */ - 0x10, 0x14, 0x1B, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE2 */ - 0x66, 0x1E, 0x00, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE3 */ - 0x66, 0x00, 0x1F, 0x9C, 0x6C, 0x30, 0x18, 0x3C, 0xF6, 0xC3, 0x61, 0xB1, 0xCF, 0x70, /* 0xE4 */ - 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xD8, /* 0xE5 */ - 0x0C, 0x08, 0x10, 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE6 */ - 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x10, 0x1C, 0x0C, 0x38, /* 0xE7 */ - 0x44, 0x28, 0x38, 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE8 */ - 0x0C, 0x08, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE9 */ - 0x3C, 0x62, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3E, 0x04, 0x0C, 0x0C, 0x06, /* 0xEA */ - 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEB */ - 0x64, 0x2C, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEC */ - 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, /* 0xED */ - 0x69, 0x06, 0x66, 0x66, 0x66, 0x66, 0x60, /* 0xEE */ - 0x03, 0x30, 0x32, 0x03, 0x43, 0xB0, 0x67, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x06, 0x70, 0x3B, - 0x00, /* 0xEF */ - 0x03, 0x07, 0xC0, 0xC7, 0x66, 0x76, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xCC, 0xE3, 0xB0, /* 0xF0 */ - 0x0C, 0x18, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF1 */ - 0x66, 0x3C, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF2 */ - 0x0C, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF3 */ - 0x18, 0x24, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF4 */ - 0x36, 0x6C, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF5 */ - 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF6 */ - 0x18, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x30, /* 0xF7 */ - 0xDB, 0x81, 0xBE, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0xF8 */ - 0x10, 0x28, 0x10, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xF9 */ - 0x06, 0x0C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFA */ - 0x36, 0x6C, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFB */ - 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFC */ - 0x06, 0x04, 0x08, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xFD */ - 0x63, 0x3C, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xE2, 0x1C, 0x6F, /* 0xFE */ - 0xC0, /* 0xFF */ +/* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, +/* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, +/* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, +/* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, +/* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, +/* 0x0A */ +/* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, +/* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, +/* 0x0D */ +/* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, +/* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, +/* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, +/* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, +/* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, +/* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, +/* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, +/* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, +/* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, +/* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, +/* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, +/* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, +/* '"' 0x22 */ 0xDE, 0xF7, 0x20, +/* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, +/* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, +/* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, +/* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, +/* ''' 0x27 */ 0xFE, +/* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, +/* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, +/* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, +/* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, +/* ',' 0x2C */ 0xD6, +/* '-' 0x2D */ 0xF0, +/* '.' 0x2E */ 0xC0, +/* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, +/* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, +/* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, +/* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, +/* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, +/* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, +/* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, +/* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, +/* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, +/* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, +/* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, +/* ':' 0x3A */ 0xC0, 0x00, 0x30, +/* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, +/* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, +/* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, +/* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, +/* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, +/* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, +/* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, +/* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, +/* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, +/* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, +/* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, +/* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, +/* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, +/* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, +/* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, +/* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, +/* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, +/* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, +/* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, +/* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, +/* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, +/* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, +/* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, +/* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, +/* '_' 0x5F */ 0xFF, 0xC0, +/* '`' 0x60 */ 0xC6, 0x30, +/* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, +/* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, +/* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, +/* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, +/* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, +/* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, +/* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, +/* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, +/* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, +/* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, +/* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, +/* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, +/* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, +/* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, +/* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, +/* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, +/* '~' 0x7E */ 0x61, 0x24, 0x38, +/* 0x7F */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, 0xFF, 0xFC, +/* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, +/* 0x81 */ +/* 0x82 */ 0xDC, +/* 0x83 */ +/* 0x84 */ 0xDA, 0x76, +/* 0x85 */ 0xCC, 0xC0, +/* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, +/* 0x88 */ +/* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, +/* 0x8A */ 0x1B, 0x03, 0x83, 0xF1, 0x86, 0xC0, 0xF0, 0x3C, 0x01, 0xE0, 0x1F, 0x00, 0xE0, 0x0F, 0x03, 0xC0, 0xD8, 0x63, 0xF0, +/* 0x8B */ 0x69, +/* 0x8C */ 0x06, 0x03, 0x03, 0xF1, 0x86, 0xC0, 0xF0, 0x3C, 0x01, 0xE0, 0x1F, 0x00, 0xE0, 0x0F, 0x03, 0xC0, 0xD8, 0x63, 0xF0, +/* 0x8D */ 0x33, 0x0F, 0x3F, 0xE1, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, +/* 0x8E */ 0x1B, 0x03, 0x8F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, +/* 0x8F */ 0x0C, 0x06, 0x0F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, +/* 0x90 */ +/* 0x91 */ 0x6B, +/* 0x92 */ 0xD6, +/* 0x93 */ 0x4C, 0xA5, 0xB0, +/* 0x94 */ 0xDA, 0x53, 0x20, +/* 0x95 */ 0x6F, 0xFF, 0x60, +/* 0x96 */ 0xFE, +/* 0x97 */ 0xFF, 0xFF, +/* 0x98 */ +/* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, +/* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 0x9B */ 0x96, +/* 0x9C */ 0x0C, 0x18, 0x10, 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 0x9D */ 0x0D, 0xA7, 0x3C, 0x61, 0x86, 0x18, 0x61, 0x86, 0x18, 0x70, +/* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, +/* 0x9F */ 0x0C, 0x10, 0x47, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, +/* 0xA0 */ +/* 0xA1 */ 0x8A, 0x9C, +/* 0xA2 */ 0x85, 0xE0, +/* 0xA3 */ 0x60, 0x30, 0x18, 0x0C, 0x86, 0xC3, 0xC1, 0xC1, 0xC0, 0xE0, 0x30, 0x18, 0x0C, 0x07, 0xF8, +/* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, +/* 0xA5 */ 0x06, 0x00, 0xF0, 0x0F, 0x01, 0x30, 0x13, 0x81, 0x38, 0x21, 0x82, 0x1C, 0x3F, 0xC6, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, 0x06, 0x00, 0xC0, 0x0C, 0x00, 0x70, +/* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, +/* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, +/* 0xA8 */ 0xCC, +/* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAA */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x02, 0x00, 0xE0, 0x18, 0x1C, 0x00, +/* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, +/* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xAD */ +/* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAF */ 0x0C, 0x00, 0x0F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, +/* 0xB0 */ 0x74, 0x63, 0x17, 0x00, +/* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, +/* 0xB2 */ 0x6C, 0xC7, +/* 0xB3 */ 0x66, 0x66, 0x67, 0x6E, 0x66, 0x66, 0x60, +/* 0xB4 */ 0x36, 0xC0, +/* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, +/* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, +/* 0xB7 */ 0xE0, +/* 0xB8 */ 0x21, 0xC7, 0xE0, +/* 0xB9 */ 0x7E, 0x38, 0xCC, 0x30, 0x0C, 0x0F, 0x1E, 0xCC, 0x33, 0x0C, 0xC7, 0x1E, 0xE0, 0x10, 0x0C, 0x03, 0x00, 0x70, +/* 0xBA */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xC3, 0x7E, 0x10, 0x1C, 0x0C, 0x38, +/* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, +/* 0xBC */ 0xC6, 0xC4, 0xC8, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 0xBD */ 0x6F, 0x69, 0x00, +/* 0xBE */ 0xDE, 0xB9, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, +/* 0xBF */ 0x30, 0x03, 0xF8, 0x30, 0xC3, 0x06, 0x18, 0x60, 0x83, 0x07, 0xF0, +/* 0xC0 */ 0x06, 0x00, 0xC0, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 0xC1 */ 0x06, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC2 */ 0x0C, 0x04, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC3 */ 0x21, 0x07, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC4 */ 0x33, 0x00, 0x00, 0xC0, 0x78, 0x1E, 0x04, 0x83, 0x30, 0xCC, 0x33, 0x1F, 0xE6, 0x19, 0x02, 0xC0, 0xF0, 0x30, +/* 0xC5 */ 0x30, 0x60, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 0xC6 */ 0x06, 0x01, 0x80, 0x00, 0x0F, 0xC3, 0x0C, 0xC0, 0xD0, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0x01, 0xA0, 0x36, 0x0C, 0x61, 0x87, 0xC0, +/* 0xC7 */ 0x1F, 0x06, 0x19, 0x83, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0xE1, 0xF0, 0x08, 0x01, 0xC0, 0x18, 0x0E, 0x00, +/* 0xC8 */ 0x19, 0x81, 0xE0, 0x00, 0x0F, 0xC3, 0x0C, 0xC0, 0xF0, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0x01, 0xA0, 0x36, 0x0C, 0x61, 0x87, 0xC0, +/* 0xC9 */ 0x0C, 0x0C, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, +/* 0xCA */ 0xFF, 0xD8, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x3F, 0xF6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0F, 0xFC, 0x01, 0x80, 0x60, 0x0C, 0x00, 0xE0, +/* 0xCB */ 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0xFE, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, +/* 0xCC */ 0x33, 0x0F, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFE, +/* 0xCD */ 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xC0, +/* 0xCE */ 0x76, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* 0xCF */ 0x66, 0x0F, 0x00, 0x03, 0xF8, 0xC3, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC1, 0xB0, 0xEF, 0xE0, +/* 0xD0 */ 0x7F, 0x0C, 0x31, 0x83, 0x30, 0x36, 0x06, 0xC0, 0xFE, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x30, 0xE7, 0xF0, +/* 0xD1 */ 0x03, 0x01, 0x83, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, 0xC0, +/* 0xD2 */ 0x19, 0x81, 0xE3, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, 0xC0, +/* 0xD3 */ 0x03, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD4 */ 0x0F, 0x01, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD5 */ 0x0D, 0x81, 0xB0, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD6 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD7 */ 0x83, 0x89, 0xA1, 0x83, 0x89, 0xA1, 0x80, +/* 0xD8 */ 0x33, 0x01, 0xE0, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 0xD9 */ 0x04, 0x01, 0x43, 0x11, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDA */ 0x06, 0x01, 0x83, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDB */ 0x0D, 0x83, 0x63, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDC */ 0x1B, 0x00, 0x03, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDD */ 0x03, 0x0C, 0x63, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x1D, 0x80, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xDE */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x40, 0x3C, 0x06, 0x1E, 0x00, +/* 0xDF */ 0x3C, 0x33, 0x30, 0xD8, 0x6C, 0x36, 0x33, 0x39, 0x86, 0xC1, 0xE0, 0xF0, 0x78, 0x6D, 0xE0, +/* 0xE0 */ 0x19, 0x89, 0xBE, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, +/* 0xE1 */ 0x0C, 0x04, 0x04, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE2 */ 0x10, 0x14, 0x1B, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE3 */ 0x66, 0x1E, 0x00, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE4 */ 0x66, 0x00, 0x1F, 0x9C, 0x6C, 0x30, 0x18, 0x3C, 0xF6, 0xC3, 0x61, 0xB1, 0xCF, 0x70, +/* 0xE5 */ 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xD8, +/* 0xE6 */ 0x0C, 0x08, 0x10, 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xE7 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x10, 0x1C, 0x0C, 0x38, +/* 0xE8 */ 0x44, 0x28, 0x38, 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xE9 */ 0x0C, 0x08, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xEA */ 0x3C, 0x62, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3E, 0x04, 0x0C, 0x0C, 0x06, +/* 0xEB */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xEC */ 0x64, 0x2C, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xED */ 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, +/* 0xEE */ 0x69, 0x06, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 0xEF */ 0x03, 0x30, 0x32, 0x03, 0x43, 0xB0, 0x67, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x06, 0x70, 0x3B, 0x00, +/* 0xF0 */ 0x03, 0x07, 0xC0, 0xC7, 0x66, 0x76, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xCC, 0xE3, 0xB0, +/* 0xF1 */ 0x0C, 0x18, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 0xF2 */ 0x66, 0x3C, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 0xF3 */ 0x0C, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF4 */ 0x18, 0x24, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF5 */ 0x36, 0x6C, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF6 */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF7 */ 0x18, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x30, +/* 0xF8 */ 0xDB, 0x81, 0xBE, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, +/* 0xF9 */ 0x10, 0x28, 0x10, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFA */ 0x06, 0x0C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFB */ 0x36, 0x6C, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFC */ 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFD */ 0x06, 0x04, 0x08, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 0xFE */ 0x63, 0x3C, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xE2, 0x1C, 0x6F, +/* 0xFF */ 0xC0, }; const GFXglyph FreeSans9pt_Win1250Glyphs[] PROGMEM = { - /* ' ' 0x20 */ {0, 0, 0, 5, 0, 0}, - /* '!' 0x21 */ {0, 2, 13, 6, 2, -12}, - /* '"' 0x22 */ {4, 5, 4, 6, 1, -12}, - /* '#' 0x23 */ {7, 10, 12, 10, 0, -11}, - /* '$' 0x24 */ {22, 9, 16, 10, 1, -13}, - /* '%' 0x25 */ {40, 16, 13, 16, 1, -12}, - /* '&' 0x26 */ {66, 10, 13, 12, 1, -12}, - /* ''' 0x27 */ {83, 2, 4, 4, 1, -12}, - /* '(' 0x28 */ {84, 4, 17, 6, 1, -12}, - /* ')' 0x29 */ {93, 4, 17, 6, 1, -12}, - /* '*' 0x2A */ {102, 5, 5, 7, 1, -12}, - /* '+' 0x2B */ {106, 6, 8, 11, 3, -7}, - /* ',' 0x2C */ {112, 2, 4, 5, 2, 0}, - /* '-' 0x2D */ {113, 4, 1, 6, 1, -4}, - /* '.' 0x2E */ {114, 2, 1, 5, 1, 0}, - /* '/' 0x2F */ {115, 5, 13, 5, 0, -12}, - /* '0' 0x30 */ {124, 8, 13, 10, 1, -12}, - /* '1' 0x31 */ {137, 4, 13, 10, 3, -12}, - /* '2' 0x32 */ {144, 9, 13, 10, 1, -12}, - /* '3' 0x33 */ {159, 8, 13, 10, 1, -12}, - /* '4' 0x34 */ {172, 7, 13, 10, 2, -12}, - /* '5' 0x35 */ {184, 9, 13, 10, 1, -12}, - /* '6' 0x36 */ {199, 9, 13, 10, 1, -12}, - /* '7' 0x37 */ {214, 8, 13, 10, 0, -12}, - /* '8' 0x38 */ {227, 9, 13, 10, 1, -12}, - /* '9' 0x39 */ {242, 8, 13, 10, 1, -12}, - /* ':' 0x3A */ {255, 2, 10, 5, 1, -9}, - /* ';' 0x3B */ {258, 3, 12, 5, 1, -8}, - /* '<' 0x3C */ {263, 9, 9, 11, 1, -8}, - /* '=' 0x3D */ {274, 9, 4, 11, 1, -5}, - /* '>' 0x3E */ {279, 9, 8, 11, 1, -7}, - /* '?' 0x3F */ {288, 9, 13, 10, 1, -12}, - /* '@' 0x40 */ {303, 17, 16, 18, 1, -12}, - /* 'A' 0x41 */ {337, 12, 13, 12, 0, -12}, - /* 'B' 0x42 */ {357, 11, 13, 12, 1, -12}, - /* 'C' 0x43 */ {375, 11, 13, 13, 1, -12}, - /* 'D' 0x44 */ {393, 11, 13, 13, 1, -12}, - /* 'E' 0x45 */ {411, 9, 13, 11, 1, -12}, - /* 'F' 0x46 */ {426, 8, 13, 11, 1, -12}, - /* 'G' 0x47 */ {439, 12, 13, 14, 1, -12}, - /* 'H' 0x48 */ {459, 11, 13, 13, 1, -12}, - /* 'I' 0x49 */ {477, 2, 13, 5, 2, -12}, - /* 'J' 0x4A */ {481, 7, 13, 10, 1, -12}, - /* 'K' 0x4B */ {493, 10, 13, 12, 1, -12}, - /* 'L' 0x4C */ {510, 8, 13, 10, 1, -12}, - /* 'M' 0x4D */ {523, 13, 13, 15, 1, -12}, - /* 'N' 0x4E */ {545, 11, 13, 13, 1, -12}, - /* 'O' 0x4F */ {563, 13, 13, 14, 1, -12}, - /* 'P' 0x50 */ {585, 10, 13, 12, 1, -12}, - /* 'Q' 0x51 */ {602, 13, 14, 14, 1, -12}, - /* 'R' 0x52 */ {625, 12, 13, 13, 1, -12}, - /* 'S' 0x53 */ {645, 10, 13, 12, 1, -12}, - /* 'T' 0x54 */ {662, 9, 13, 11, 1, -12}, - /* 'U' 0x55 */ {677, 11, 13, 13, 1, -12}, - /* 'V' 0x56 */ {695, 11, 13, 11, 0, -12}, - /* 'W' 0x57 */ {713, 16, 13, 17, 0, -12}, - /* 'X' 0x58 */ {739, 10, 13, 12, 1, -12}, - /* 'Y' 0x59 */ {756, 12, 13, 12, 0, -12}, - /* 'Z' 0x5A */ {776, 10, 13, 11, 1, -12}, - /* '[' 0x5B */ {793, 3, 17, 5, 1, -12}, - /* '\' 0x5C */ {800, 5, 13, 5, 0, -12}, - /* ']' 0x5D */ {809, 3, 17, 5, 0, -12}, - /* '^' 0x5E */ {816, 7, 7, 8, 1, -12}, - /* '_' 0x5F */ {823, 10, 1, 10, 0, 3}, - /* '`' 0x60 */ {825, 4, 3, 5, 0, -12}, - /* 'a' 0x61 */ {827, 9, 10, 10, 1, -9}, - /* 'b' 0x62 */ {839, 9, 13, 10, 1, -12}, - /* 'c' 0x63 */ {854, 8, 10, 9, 1, -9}, - /* 'd' 0x64 */ {864, 8, 13, 10, 1, -12}, - /* 'e' 0x65 */ {877, 8, 10, 10, 1, -9}, - /* 'f' 0x66 */ {887, 4, 13, 5, 1, -12}, - /* 'g' 0x67 */ {894, 8, 14, 10, 1, -9}, - /* 'h' 0x68 */ {908, 8, 13, 10, 1, -12}, - /* 'i' 0x69 */ {921, 2, 13, 4, 1, -12}, - /* 'j' 0x6A */ {925, 4, 17, 4, 0, -12}, - /* 'k' 0x6B */ {934, 8, 13, 9, 1, -12}, - /* 'l' 0x6C */ {947, 2, 13, 4, 1, -12}, - /* 'm' 0x6D */ {951, 13, 10, 15, 1, -9}, - /* 'n' 0x6E */ {968, 8, 10, 10, 1, -9}, - /* 'o' 0x6F */ {978, 8, 10, 10, 1, -9}, - /* 'p' 0x70 */ {988, 9, 13, 10, 1, -9}, - /* 'q' 0x71 */ {1003, 8, 13, 10, 1, -9}, - /* 'r' 0x72 */ {1016, 5, 10, 6, 1, -9}, - /* 's' 0x73 */ {1023, 8, 10, 9, 1, -9}, - /* 't' 0x74 */ {1033, 4, 12, 5, 1, -11}, - /* 'u' 0x75 */ {1039, 8, 10, 10, 1, -9}, - /* 'v' 0x76 */ {1049, 9, 10, 9, 0, -9}, - /* 'w' 0x77 */ {1061, 13, 10, 13, 0, -9}, - /* 'x' 0x78 */ {1078, 7, 10, 9, 1, -9}, - /* 'y' 0x79 */ {1087, 8, 14, 9, 0, -9}, - /* 'z' 0x7A */ {1101, 7, 10, 9, 1, -9}, - /* '{' 0x7B */ {1110, 4, 17, 6, 1, -12}, - /* '|' 0x7C */ {1119, 2, 17, 4, 2, -12}, - /* '}' 0x7D */ {1124, 4, 17, 6, 1, -12}, - /* '~' 0x7E */ {1133, 7, 3, 9, 1, -7}, - /* 0x7F */ {1136, 13, 14, 15, 1, -12}, - /* 0x80 */ {1159, 10, 13, 12, 1, -12}, - /* 0x81 */ {1176, 0, 0, 0, 0, 0}, - /* 0x82 */ {1176, 2, 3, 5, 1, 0}, - /* 0x83 */ {1177, 0, 0, 0, 0, 0}, - /* 0x84 */ {1177, 5, 3, 7, 1, 0}, - /* 0x85 */ {1179, 10, 1, 12, 1, 0}, - /* 0x86 */ {1181, 8, 16, 10, 1, -12}, - /* 0x87 */ {1197, 8, 16, 10, 1, -12}, - /* 0x88 */ {1213, 0, 0, 0, 0, 0}, - /* 0x89 */ {1213, 18, 13, 18, 0, -12}, - /* 0x8A */ {1243, 10, 15, 12, 1, -14}, - /* 0x8B */ {1262, 2, 4, 4, 1, -6}, - /* 0x8C */ {1263, 10, 15, 12, 1, -14}, - /* 0x8D */ {1282, 9, 15, 11, 1, -14}, - /* 0x8E */ {1299, 10, 15, 11, 1, -14}, - /* 0x8F */ {1318, 10, 15, 11, 1, -14}, - /* 0x90 */ {1337, 0, 0, 0, 0, 0}, - /* 0x91 */ {1337, 2, 4, 4, 2, -12}, - /* 0x92 */ {1338, 2, 4, 4, 1, -12}, - /* 0x93 */ {1339, 5, 4, 7, 2, -12}, - /* 0x94 */ {1342, 5, 4, 7, 1, -12}, - /* 0x95 */ {1345, 4, 5, 7, 1, -8}, - /* 0x96 */ {1348, 7, 1, 9, 1, -4}, - /* 0x97 */ {1349, 16, 1, 18, 1, -4}, - /* 0x98 */ {1351, 0, 0, 0, 0, 0}, - /* 0x99 */ {1351, 18, 10, 18, 1, -13}, - /* 0x9A */ {1374, 8, 13, 9, 1, -12}, - /* 0x9B */ {1387, 2, 4, 5, 2, -6}, - /* 0x9C */ {1388, 8, 13, 9, 1, -12}, - /* 0x9D */ {1401, 6, 13, 8, 1, -12}, - /* 0x9E */ {1411, 7, 13, 9, 1, -12}, - /* 0x9F */ {1423, 7, 13, 9, 1, -12}, - /* 0xA0 */ {1435, 0, 0, 5, 0, 0}, - /* 0xA1 */ {1435, 5, 3, 6, 0, -12}, - /* 0xA2 */ {1437, 6, 2, 6, 0, -12}, - /* 0xA3 */ {1439, 9, 13, 11, 1, -12}, - /* 0xA4 */ {1454, 7, 6, 10, 2, -8}, - /* 0xA5 */ {1460, 12, 17, 12, 1, -12}, - /* 0xA6 */ {1486, 2, 17, 5, 2, -12}, - /* 0xA7 */ {1491, 9, 17, 10, 1, -12}, - /* 0xA8 */ {1511, 6, 1, 6, 0, -11}, - /* 0xA9 */ {1512, 14, 13, 14, 1, -12}, - /* 0xAA */ {1535, 10, 17, 12, 1, -12}, - /* 0xAB */ {1557, 7, 6, 9, 1, -7}, - /* 0xAC */ {1563, 9, 5, 11, 2, -5}, - /* 0xAD */ {1569, 0, 0, 0, 0, 0}, - /* 0xAE */ {1569, 14, 13, 14, 1, -12}, - /* 0xAF */ {1592, 10, 15, 11, 1, -14}, - /* 0xB0 */ {1611, 5, 5, 11, 3, -11}, - /* 0xB1 */ {1615, 9, 11, 11, 1, -10}, - /* 0xB2 */ {1628, 4, 4, 6, 1, 1}, - /* 0xB3 */ {1630, 4, 13, 5, 1, -12}, - /* 0xB4 */ {1637, 4, 3, 6, 2, -12}, - /* 0xB5 */ {1639, 9, 13, 10, 1, -9}, - /* 0xB6 */ {1654, 8, 16, 10, 2, -12}, - /* 0xB7 */ {1670, 3, 1, 5, 1, -4}, - /* 0xB8 */ {1671, 5, 4, 6, 1, 1}, - /* 0xB9 */ {1674, 10, 14, 10, 1, -9}, - /* 0xBA */ {1692, 8, 14, 9, 1, -9}, - /* 0xBB */ {1706, 7, 6, 9, 1, -7}, - /* 0xBC */ {1712, 8, 13, 10, 1, -12}, - /* 0xBD */ {1725, 6, 3, 6, 0, -12}, - /* 0xBE */ {1728, 5, 13, 7, 1, -12}, - /* 0xBF */ {1737, 7, 12, 9, 1, -11}, - /* 0xC0 */ {1748, 12, 15, 13, 1, -14}, - /* 0xC1 */ {1771, 10, 14, 12, 1, -13}, - /* 0xC2 */ {1789, 10, 14, 12, 1, -13}, - /* 0xC3 */ {1807, 10, 14, 12, 1, -13}, - /* 0xC4 */ {1825, 10, 14, 12, 1, -13}, - /* 0xC5 */ {1843, 8, 14, 10, 1, -13}, - /* 0xC6 */ {1857, 11, 15, 13, 1, -14}, - /* 0xC7 */ {1878, 11, 17, 13, 1, -12}, - /* 0xC8 */ {1902, 11, 15, 13, 1, -14}, - /* 0xC9 */ {1923, 9, 14, 11, 1, -13}, - /* 0xCA */ {1939, 11, 17, 12, 1, -12}, - /* 0xCB */ {1963, 9, 14, 11, 1, -13}, - /* 0xCC */ {1979, 9, 15, 11, 1, -14}, - /* 0xCD */ {1996, 3, 14, 5, 1, -13}, - /* 0xCE */ {2002, 5, 14, 5, 0, -13}, - /* 0xCF */ {2011, 10, 15, 13, 2, -14}, - /* 0xD0 */ {2030, 11, 13, 13, 1, -12}, - /* 0xD1 */ {2048, 11, 14, 13, 1, -13}, - /* 0xD2 */ {2068, 11, 14, 13, 1, -13}, - /* 0xD3 */ {2088, 12, 15, 13, 1, -14}, - /* 0xD4 */ {2111, 12, 15, 13, 1, -14}, - /* 0xD5 */ {2134, 12, 15, 13, 1, -14}, - /* 0xD6 */ {2157, 12, 15, 13, 1, -14}, - /* 0xD7 */ {2180, 7, 7, 11, 2, -7}, - /* 0xD8 */ {2187, 12, 15, 13, 1, -14}, - /* 0xD9 */ {2210, 11, 14, 13, 1, -13}, - /* 0xDA */ {2230, 11, 14, 13, 1, -13}, - /* 0xDB */ {2250, 11, 14, 13, 1, -13}, - /* 0xDC */ {2270, 11, 14, 13, 1, -13}, - /* 0xDD */ {2290, 12, 14, 12, 0, -13}, - /* 0xDE */ {2311, 9, 17, 11, 1, -12}, - /* 0xDF */ {2331, 9, 13, 11, 1, -12}, - /* 0xE0 */ {2346, 5, 13, 6, 1, -12}, - /* 0xE1 */ {2355, 9, 13, 10, 1, -12}, - /* 0xE2 */ {2370, 9, 13, 10, 1, -12}, - /* 0xE3 */ {2385, 9, 13, 10, 1, -12}, - /* 0xE4 */ {2400, 9, 12, 10, 1, -11}, - /* 0xE5 */ {2414, 3, 15, 4, 0, -14}, - /* 0xE6 */ {2420, 8, 13, 9, 1, -12}, - /* 0xE7 */ {2433, 8, 14, 9, 1, -9}, - /* 0xE8 */ {2447, 8, 13, 9, 1, -12}, - /* 0xE9 */ {2460, 8, 13, 10, 1, -12}, - /* 0xEA */ {2473, 8, 14, 10, 1, -9}, - /* 0xEB */ {2487, 8, 12, 10, 1, -11}, - /* 0xEC */ {2499, 8, 13, 10, 1, -12}, - /* 0xED */ {2512, 3, 13, 4, 1, -12}, - /* 0xEE */ {2517, 4, 13, 5, 0, -12}, - /* 0xEF */ {2524, 12, 13, 12, 1, -12}, - /* 0xF0 */ {2544, 9, 13, 10, 1, -12}, - /* 0xF1 */ {2559, 8, 13, 10, 1, -12}, - /* 0xF2 */ {2572, 8, 13, 10, 1, -12}, - /* 0xF3 */ {2585, 8, 13, 10, 1, -12}, - /* 0xF4 */ {2598, 8, 13, 10, 1, -12}, - /* 0xF5 */ {2611, 8, 13, 10, 1, -12}, - /* 0xF6 */ {2624, 8, 12, 10, 1, -11}, - /* 0xF7 */ {2636, 9, 8, 11, 1, -7}, - /* 0xF8 */ {2645, 5, 13, 6, 1, -12}, - /* 0xF9 */ {2654, 8, 13, 10, 1, -12}, - /* 0xFA */ {2667, 8, 13, 10, 1, -12}, - /* 0xFB */ {2680, 8, 13, 10, 1, -12}, - /* 0xFC */ {2693, 8, 12, 10, 1, -11}, - /* 0xFD */ {2705, 8, 17, 9, 0, -12}, - /* 0xFE */ {2722, 5, 16, 5, 1, -11}, - /* 0xFF */ {2732, 2, 1, 6, 2, -11}, +/* 0x01 */ { 0, 15, 15, 17, 1, -13 }, +/* 0x02 */ { 29, 15, 15, 17, 1, -13 }, +/* 0x03 */ { 58, 15, 16, 17, 1, -14 }, +/* 0x04 */ { 88, 15, 16, 17, 1, -14 }, +/* 0x05 */ { 118, 16, 15, 18, 1, -13 }, +/* 0x06 */ { 148, 15, 15, 17, 1, -13 }, +/* 0x07 */ { 177, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 177, 17, 16, 19, 1, -14 }, +/* 0x09 */ { 211, 17, 12, 19, 1, -12 }, +/* 0x0A */ { 237, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 237, 17, 16, 19, 1, -14 }, +/* 0x0C */ { 271, 15, 14, 17, 1, -12 }, +/* 0x0D */ { 298, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 298, 15, 16, 17, 1, -14 }, +/* 0x0F */ { 328, 15, 15, 17, 1, -13 }, +/* 0x10 */ { 357, 15, 15, 17, 1, -13 }, +/* 0x11 */ { 386, 15, 16, 17, 1, -14 }, +/* 0x12 */ { 416, 17, 17, 19, 1, -15 }, +/* 0x13 */ { 453, 15, 16, 17, 1, -14 }, +/* 0x14 */ { 483, 15, 16, 17, 1, -14 }, +/* 0x15 */ { 513, 15, 16, 17, 1, -14 }, +/* 0x16 */ { 543, 11, 16, 13, 1, -14 }, +/* 0x17 */ { 565, 15, 16, 17, 1, -14 }, +/* 0x18 */ { 595, 18, 15, 20, 1, -13 }, +/* 0x19 */ { 629, 15, 16, 17, 1, -14 }, +/* 0x1A */ { 659, 13, 14, 15, 1, -12 }, +/* 0x1B */ { 682, 17, 16, 19, 1, -14 }, +/* 0x1C */ { 716, 15, 16, 17, 1, -14 }, +/* 0x1D */ { 746, 15, 15, 17, 1, -13 }, +/* 0x1E */ { 775, 17, 16, 19, 1, -14 }, +/* 0x1F */ { 809, 11, 16, 13, 1, -14 }, +/* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, +/* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, +/* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, +/* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, +/* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, +/* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, +/* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, +/* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, +/* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, +/* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, +/* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, +/* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, +/* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, +/* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, +/* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, +/* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, +/* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, +/* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, +/* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, +/* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, +/* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, +/* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, +/* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, +/* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, +/* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, +/* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, +/* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, +/* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, +/* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, +/* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, +/* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, +/* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, +/* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, +/* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, +/* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, +/* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, +/* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, +/* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, +/* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, +/* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, +/* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, +/* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, +/* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, +/* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, +/* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, +/* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, +/* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, +/* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, +/* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, +/* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, +/* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, +/* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, +/* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, +/* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, +/* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, +/* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, +/* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, +/* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, +/* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, +/* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, +/* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, +/* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, +/* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, +/* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, +/* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, +/* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, +/* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, +/* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, +/* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, +/* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, +/* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, +/* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, +/* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, +/* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, +/* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, +/* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, +/* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, +/* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, +/* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, +/* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, +/* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, +/* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, +/* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, +/* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, +/* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, +/* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, +/* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, +/* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, +/* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, +/* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, +/* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, +/* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, +/* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, +/* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, +/* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, +/* 0x7F */ { 1967, 13, 14, 15, 1, -12 }, +/* 0x80 */ { 1990, 10, 13, 12, 1, -12 }, +/* 0x81 */ { 2007, 0, 0, 0, 0, 0 }, +/* 0x82 */ { 2007, 2, 3, 5, 1, 0 }, +/* 0x83 */ { 2008, 0, 0, 0, 0, 0 }, +/* 0x84 */ { 2008, 5, 3, 7, 1, 0 }, +/* 0x85 */ { 2010, 10, 1, 12, 1, 0 }, +/* 0x86 */ { 2012, 8, 16, 10, 1, -12 }, +/* 0x87 */ { 2028, 8, 16, 10, 1, -12 }, +/* 0x88 */ { 2044, 0, 0, 0, 0, 0 }, +/* 0x89 */ { 2044, 18, 13, 18, 0, -12 }, +/* 0x8A */ { 2074, 10, 15, 12, 1, -14 }, +/* 0x8B */ { 2093, 2, 4, 4, 1, -6 }, +/* 0x8C */ { 2094, 10, 15, 12, 1, -14 }, +/* 0x8D */ { 2113, 9, 15, 11, 1, -14 }, +/* 0x8E */ { 2130, 10, 15, 11, 1, -14 }, +/* 0x8F */ { 2149, 10, 15, 11, 1, -14 }, +/* 0x90 */ { 2168, 0, 0, 0, 0, 0 }, +/* 0x91 */ { 2168, 2, 4, 4, 2, -12 }, +/* 0x92 */ { 2169, 2, 4, 4, 1, -12 }, +/* 0x93 */ { 2170, 5, 4, 7, 2, -12 }, +/* 0x94 */ { 2173, 5, 4, 7, 1, -12 }, +/* 0x95 */ { 2176, 4, 5, 7, 1, -8 }, +/* 0x96 */ { 2179, 7, 1, 9, 1, -4 }, +/* 0x97 */ { 2180, 16, 1, 18, 1, -4 }, +/* 0x98 */ { 2182, 0, 0, 0, 0, 0 }, +/* 0x99 */ { 2182, 18, 10, 18, 1, -13 }, +/* 0x9A */ { 2205, 8, 13, 9, 1, -12 }, +/* 0x9B */ { 2218, 2, 4, 5, 2, -6 }, +/* 0x9C */ { 2219, 8, 13, 9, 1, -12 }, +/* 0x9D */ { 2232, 6, 13, 8, 1, -12 }, +/* 0x9E */ { 2242, 7, 13, 9, 1, -12 }, +/* 0x9F */ { 2254, 7, 13, 9, 1, -12 }, +/* 0xA0 */ { 2266, 0, 0, 5, 0, 0 }, +/* 0xA1 */ { 2266, 5, 3, 6, 0, -12 }, +/* 0xA2 */ { 2268, 6, 2, 6, 0, -12 }, +/* 0xA3 */ { 2270, 9, 13, 11, 1, -12 }, +/* 0xA4 */ { 2285, 7, 6, 10, 2, -8 }, +/* 0xA5 */ { 2291, 12, 17, 12, 1, -12 }, +/* 0xA6 */ { 2317, 2, 17, 5, 2, -12 }, +/* 0xA7 */ { 2322, 9, 17, 10, 1, -12 }, +/* 0xA8 */ { 2342, 6, 1, 6, 0, -11 }, +/* 0xA9 */ { 2343, 14, 13, 14, 1, -12 }, +/* 0xAA */ { 2366, 10, 17, 12, 1, -12 }, +/* 0xAB */ { 2388, 7, 6, 9, 1, -7 }, +/* 0xAC */ { 2394, 9, 5, 11, 2, -5 }, +/* 0xAD */ { 2400, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 2400, 14, 13, 14, 1, -12 }, +/* 0xAF */ { 2423, 10, 15, 11, 1, -14 }, +/* 0xB0 */ { 2442, 5, 5, 11, 3, -11 }, +/* 0xB1 */ { 2446, 9, 11, 11, 1, -10 }, +/* 0xB2 */ { 2459, 4, 4, 6, 1, 1 }, +/* 0xB3 */ { 2461, 4, 13, 5, 1, -12 }, +/* 0xB4 */ { 2468, 4, 3, 6, 2, -12 }, +/* 0xB5 */ { 2470, 9, 13, 10, 1, -9 }, +/* 0xB6 */ { 2485, 8, 16, 10, 2, -12 }, +/* 0xB7 */ { 2501, 3, 1, 5, 1, -4 }, +/* 0xB8 */ { 2502, 5, 4, 6, 1, 1 }, +/* 0xB9 */ { 2505, 10, 14, 10, 1, -9 }, +/* 0xBA */ { 2523, 8, 14, 9, 1, -9 }, +/* 0xBB */ { 2537, 7, 6, 9, 1, -7 }, +/* 0xBC */ { 2543, 8, 13, 10, 1, -12 }, +/* 0xBD */ { 2556, 6, 3, 6, 0, -12 }, +/* 0xBE */ { 2559, 5, 13, 7, 1, -12 }, +/* 0xBF */ { 2568, 7, 12, 9, 1, -11 }, +/* 0xC0 */ { 2579, 12, 15, 13, 1, -14 }, +/* 0xC1 */ { 2602, 10, 14, 12, 1, -13 }, +/* 0xC2 */ { 2620, 10, 14, 12, 1, -13 }, +/* 0xC3 */ { 2638, 10, 14, 12, 1, -13 }, +/* 0xC4 */ { 2656, 10, 14, 12, 1, -13 }, +/* 0xC5 */ { 2674, 8, 14, 10, 1, -13 }, +/* 0xC6 */ { 2688, 11, 15, 13, 1, -14 }, +/* 0xC7 */ { 2709, 11, 17, 13, 1, -12 }, +/* 0xC8 */ { 2733, 11, 15, 13, 1, -14 }, +/* 0xC9 */ { 2754, 9, 14, 11, 1, -13 }, +/* 0xCA */ { 2770, 11, 17, 12, 1, -12 }, +/* 0xCB */ { 2794, 9, 14, 11, 1, -13 }, +/* 0xCC */ { 2810, 9, 15, 11, 1, -14 }, +/* 0xCD */ { 2827, 3, 14, 5, 1, -13 }, +/* 0xCE */ { 2833, 5, 14, 5, 0, -13 }, +/* 0xCF */ { 2842, 10, 15, 13, 2, -14 }, +/* 0xD0 */ { 2861, 11, 13, 13, 1, -12 }, +/* 0xD1 */ { 2879, 11, 14, 13, 1, -13 }, +/* 0xD2 */ { 2899, 11, 14, 13, 1, -13 }, +/* 0xD3 */ { 2919, 12, 15, 13, 1, -14 }, +/* 0xD4 */ { 2942, 12, 15, 13, 1, -14 }, +/* 0xD5 */ { 2965, 12, 15, 13, 1, -14 }, +/* 0xD6 */ { 2988, 12, 15, 13, 1, -14 }, +/* 0xD7 */ { 3011, 7, 7, 11, 2, -7 }, +/* 0xD8 */ { 3018, 12, 15, 13, 1, -14 }, +/* 0xD9 */ { 3041, 11, 14, 13, 1, -13 }, +/* 0xDA */ { 3061, 11, 14, 13, 1, -13 }, +/* 0xDB */ { 3081, 11, 14, 13, 1, -13 }, +/* 0xDC */ { 3101, 11, 14, 13, 1, -13 }, +/* 0xDD */ { 3121, 12, 14, 12, 0, -13 }, +/* 0xDE */ { 3142, 9, 17, 11, 1, -12 }, +/* 0xDF */ { 3162, 9, 13, 11, 1, -12 }, +/* 0xE0 */ { 3177, 5, 13, 6, 1, -12 }, +/* 0xE1 */ { 3186, 9, 13, 10, 1, -12 }, +/* 0xE2 */ { 3201, 9, 13, 10, 1, -12 }, +/* 0xE3 */ { 3216, 9, 13, 10, 1, -12 }, +/* 0xE4 */ { 3231, 9, 12, 10, 1, -11 }, +/* 0xE5 */ { 3245, 3, 15, 4, 0, -14 }, +/* 0xE6 */ { 3251, 8, 13, 9, 1, -12 }, +/* 0xE7 */ { 3264, 8, 14, 9, 1, -9 }, +/* 0xE8 */ { 3278, 8, 13, 9, 1, -12 }, +/* 0xE9 */ { 3291, 8, 13, 10, 1, -12 }, +/* 0xEA */ { 3304, 8, 14, 10, 1, -9 }, +/* 0xEB */ { 3318, 8, 12, 10, 1, -11 }, +/* 0xEC */ { 3330, 8, 13, 10, 1, -12 }, +/* 0xED */ { 3343, 3, 13, 4, 1, -12 }, +/* 0xEE */ { 3348, 4, 13, 5, 0, -12 }, +/* 0xEF */ { 3355, 12, 13, 12, 1, -12 }, +/* 0xF0 */ { 3375, 9, 13, 10, 1, -12 }, +/* 0xF1 */ { 3390, 8, 13, 10, 1, -12 }, +/* 0xF2 */ { 3403, 8, 13, 10, 1, -12 }, +/* 0xF3 */ { 3416, 8, 13, 10, 1, -12 }, +/* 0xF4 */ { 3429, 8, 13, 10, 1, -12 }, +/* 0xF5 */ { 3442, 8, 13, 10, 1, -12 }, +/* 0xF6 */ { 3455, 8, 12, 10, 1, -11 }, +/* 0xF7 */ { 3467, 9, 8, 11, 1, -7 }, +/* 0xF8 */ { 3476, 5, 13, 6, 1, -12 }, +/* 0xF9 */ { 3485, 8, 13, 10, 1, -12 }, +/* 0xFA */ { 3498, 8, 13, 10, 1, -12 }, +/* 0xFB */ { 3511, 8, 13, 10, 1, -12 }, +/* 0xFC */ { 3524, 8, 12, 10, 1, -11 }, +/* 0xFD */ { 3536, 8, 17, 9, 0, -12 }, +/* 0xFE */ { 3553, 5, 16, 5, 1, -11 }, +/* 0xFF */ { 3563, 2, 1, 6, 2, -11 }, }; -const GFXfont FreeSans9pt_Win1250 PROGMEM = {(uint8_t *)FreeSans9pt_Win1250Bitmaps, (GFXglyph *)FreeSans9pt_Win1250Glyphs, 0x20, - 0xFF, 21}; +const GFXfont FreeSans9pt_Win1250 PROGMEM = { +(uint8_t*)FreeSans9pt_Win1250Bitmaps, +(GFXglyph*)FreeSans9pt_Win1250Glyphs, +0x01, 0xFF, 21 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h index 82857cb91..b1511d996 100644 --- a/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h @@ -1,493 +1,527 @@ +// trunk-ignore-all(clang-format) #pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1251 +*/ const uint8_t FreeSans9pt_Win1251Bitmaps[] PROGMEM = { - /* ' ' 0x20 */ - 0xFF, 0xFF, 0xF0, 0xC0, /* '!' 0x21 */ - 0xDE, 0xF7, 0x20, /* '"' 0x22 */ - 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '#' 0x23 */ - 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '$' 0x24 */ - 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, - 0x63, 0x04, 0x77, 0x08, 0x3C, /* '%' 0x25 */ - 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* '&' 0x26 */ - 0xFE, /* ''' 0x27 */ - 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* '(' 0x28 */ - 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* ')' 0x29 */ - 0x25, 0x7E, 0xA5, 0x00, /* '*' 0x2A */ - 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* '+' 0x2B */ - 0xD6, /* ',' 0x2C */ - 0xF0, /* '-' 0x2D */ - 0xC0, /* '.' 0x2E */ - 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '/' 0x2F */ - 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '0' 0x30 */ - 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '1' 0x31 */ - 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '2' 0x32 */ - 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '3' 0x33 */ - 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '4' 0x34 */ - 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '5' 0x35 */ - 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ - 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '7' 0x37 */ - 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '8' 0x38 */ - 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* '9' 0x39 */ - 0xC0, 0x00, 0x30, /* ':' 0x3A */ - 0xC0, 0x00, 0x00, 0x64, 0xA0, /* ';' 0x3B */ - 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '<' 0x3C */ - 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '=' 0x3D */ - 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '>' 0x3E */ - 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '?' 0x3F */ - 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, - 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* '@' 0x40 */ - 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, - 0x30, /* 'A' 0x41 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'B' 0x42 */ - 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'C' 0x43 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'D' 0x44 */ - 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'E' 0x45 */ - 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'F' 0x46 */ - 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, - 0x10, /* 'G' 0x47 */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'H' 0x48 */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 'I' 0x49 */ - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'J' 0x4A */ - 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'K' 0x4B */ - 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'L' 0x4C */ - 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, - 0x80, /* 'M' 0x4D */ - 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'N' 0x4E */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, - 0x00, /* 'O' 0x4F */ - 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'P' 0x50 */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, - 0x00, 0x08, /* 'Q' 0x51 */ - 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, - 0x70, /* 'R' 0x52 */ - 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'S' 0x53 */ - 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'T' 0x54 */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'U' 0x55 */ - 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'V' 0x56 */ - 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, - 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'W' 0x57 */ - 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'X' 0x58 */ - 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, - 0x00, /* 'Y' 0x59 */ - 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* 'Z' 0x5A */ - 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '[' 0x5B */ - 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* '\' 0x5C */ - 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* ']' 0x5D */ - 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '^' 0x5E */ - 0xFF, 0xC0, /* '_' 0x5F */ - 0xC6, 0x30, /* '`' 0x60 */ - 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'a' 0x61 */ - 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'b' 0x62 */ - 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'c' 0x63 */ - 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'd' 0x64 */ - 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'e' 0x65 */ - 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'f' 0x66 */ - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'g' 0x67 */ - 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'h' 0x68 */ - 0xC3, 0xFF, 0xFF, 0xC0, /* 'i' 0x69 */ - 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'j' 0x6A */ - 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'k' 0x6B */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 'l' 0x6C */ - 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'm' 0x6D */ - 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'n' 0x6E */ - 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'o' 0x6F */ - 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'p' 0x70 */ - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'q' 0x71 */ - 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 'r' 0x72 */ - 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 's' 0x73 */ - 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 't' 0x74 */ - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'u' 0x75 */ - 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'v' 0x76 */ - 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'w' 0x77 */ - 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'x' 0x78 */ - 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'y' 0x79 */ - 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* 'z' 0x7A */ - 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '{' 0x7B */ - 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '|' 0x7C */ - 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '}' 0x7D */ - 0x61, 0x24, 0x38, /* '~' 0x7E */ - 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, - 0xFF, 0xFC, /* 0x7F */ - 0xFF, 0x01, 0x80, 0x18, 0x01, 0x80, 0x18, 0x01, 0xFE, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x30, 0x03, - 0x00, 0x30, 0x0E, /* 0x80 */ - 0x0C, 0x18, 0x00, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 0x81 */ - 0xDC, /* 0x82 */ - 0x18, 0x89, 0xFC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0x83 */ - 0xDA, 0x76, /* 0x84 */ - 0xCC, 0xC0, /* 0x85 */ - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x86 */ - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ - 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x88 */ - 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, - 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x89 */ - 0x3F, 0x80, 0x18, 0xC0, 0x0C, 0x60, 0x06, 0x30, 0x03, 0x18, 0x01, 0x8C, 0x00, 0xC7, 0xF8, 0x63, 0x06, 0x31, 0x81, 0x90, 0xC0, - 0xD8, 0x60, 0x6C, 0x30, 0x6C, 0x1F, 0xE0, /* 0x8A */ - 0x69, /* 0x8B */ - 0xC0, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0xFF, 0xF9, 0x81, 0x86, 0xC0, 0xC1, 0xE0, 0x60, - 0xF0, 0x30, 0x78, 0x18, 0x6C, 0x0F, 0xE0, /* 0x8C */ - 0x0C, 0x06, 0x0C, 0x1B, 0x0C, 0xC6, 0x33, 0x0D, 0x83, 0xC0, 0xF0, 0x3E, 0x0D, 0xC3, 0x38, 0xC7, 0x30, 0xEC, 0x1C, /* 0x8D */ - 0xFF, 0x01, 0x80, 0x18, 0x01, 0x80, 0x18, 0x01, 0xFE, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, - 0x30, /* 0x8E */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3F, 0xFE, 0x0C, 0x01, - 0x80, /* 0x8F */ - 0x60, 0x7C, 0x18, 0x0D, 0xE7, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x18, 0x08, 0x08, /* 0x90 */ - 0x6B, /* 0x91 */ - 0xD6, /* 0x92 */ - 0x4C, 0xA5, 0xB0, /* 0x93 */ - 0xDA, 0x53, 0x20, /* 0x94 */ - 0x6F, 0xFF, 0x60, /* 0x95 */ - 0xFE, /* 0x96 */ - 0xFF, 0xFF, /* 0x97 */ - /* 0x98 */ - 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, - 0x33, 0x30, /* 0x99 */ - 0x7E, 0x03, 0x30, 0x19, 0x80, 0xCC, 0x06, 0x60, 0x33, 0xF9, 0x98, 0x6C, 0xC3, 0x46, 0x1E, 0x3F, 0x80, /* 0x9A */ - 0x96, /* 0x9B */ - 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xC3, 0x03, 0xFF, 0xEC, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0F, 0xE0, /* 0x9C */ - 0x0C, 0x30, 0x46, 0x3C, 0xDB, 0x34, 0x70, 0xF1, 0xB3, 0x36, 0x3C, 0x20, /* 0x9D */ - 0x60, 0x7C, 0x18, 0x0D, 0xE7, 0x3B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, /* 0x9E */ - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x18, 0x18, /* 0x9F */ - /* 0xA0 */ - 0x21, 0x07, 0x8C, 0x0F, 0x06, 0x61, 0x98, 0xC3, 0x30, 0xD8, 0x1E, 0x07, 0x00, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0x00, /* 0xA1 */ - 0x66, 0x18, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xA2 */ - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 0xA3 */ - 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA4 */ - 0x00, 0xC0, 0x3F, 0xFF, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, /* 0xA5 */ - 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA6 */ - 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, - 0x00, /* 0xA7 */ - 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFE, /* 0xA8 */ - 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, - 0x0F, 0xC0, /* 0xA9 */ - 0x1F, 0x86, 0x19, 0x81, 0xB0, 0x3C, 0x01, 0x80, 0x3F, 0xC6, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 0xAA */ - 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAB */ - 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAC */ - /* 0xAD */ - 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, - 0x0F, 0xC0, /* 0xAE */ - 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, /* 0xAF */ - 0x74, 0x63, 0x17, 0x00, /* 0xB0 */ - 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB1 */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 0xB2 */ - 0xC3, 0xFF, 0xFF, 0xC0, /* 0xB3 */ - 0x0C, 0x3F, 0xF0, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, /* 0xB4 */ - 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB5 */ - 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB6 */ - 0xE0, /* 0xB7 */ - 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xB8 */ - 0xC1, 0x81, 0x83, 0x03, 0x86, 0x05, 0x0C, 0xEB, 0x1A, 0x32, 0x34, 0x66, 0x68, 0xC4, 0xD1, 0x8D, 0xB3, 0x0B, 0x3A, 0x1E, 0x04, - 0x1C, 0x08, 0x1B, 0xC0, /* 0xB9 */ - 0x3C, 0x46, 0xC3, 0x80, 0xF8, 0x80, 0x80, 0xC3, 0x46, 0x3C, /* 0xBA */ - 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBB */ - 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 0xBC */ - 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 0xBD */ - 0x3E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0xBE */ - 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xBF */ - 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, - 0x30, /* 0xC0 */ - 0xFF, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x3F, 0xE6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 0xC1 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 0xC2 */ - 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xC3 */ - 0x1F, 0xF0, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x03, 0x0C, 0x0C, - 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0xF0, 0x03, /* 0xC4 */ - 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 0xC5 */ - 0x61, 0x86, 0x31, 0x8C, 0x19, 0x98, 0x19, 0x98, 0x0D, 0xB0, 0x07, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x0D, 0xB0, 0x19, 0x98, 0x31, - 0x8C, 0x61, 0x86, 0xC1, 0x83, /* 0xC6 */ - 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0x00, 0xC0, 0x60, 0xF0, 0x06, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 0xC7 */ - 0xC0, 0xF8, 0x1F, 0x07, 0xE0, 0xBC, 0x37, 0x8C, 0xF1, 0x1E, 0x63, 0xD8, 0x7A, 0x0F, 0xC1, 0xF0, 0x3E, 0x06, /* 0xC8 */ - 0x11, 0x03, 0xE0, 0x00, 0x60, 0x7C, 0x0F, 0x83, 0xF0, 0x5E, 0x1B, 0xC6, 0x78, 0x8F, 0x31, 0xEC, 0x3D, 0x07, 0xE0, 0xF8, 0x1F, - 0x03, /* 0xC9 */ - 0xC1, 0xB0, 0xCC, 0x63, 0x30, 0xD8, 0x3C, 0x0F, 0x03, 0xE0, 0xDC, 0x33, 0x8C, 0x73, 0x0E, 0xC1, 0xC0, /* 0xCA */ - 0x3F, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xC8, 0x36, 0x0D, 0x83, 0xC0, 0xC0, /* 0xCB */ - 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, - 0x80, /* 0xCC */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 0xCD */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, - 0x00, /* 0xCE */ - 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 0xCF */ - 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 0xD0 */ - 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 0xD1 */ - 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 0xD2 */ - 0xC0, 0xF0, 0x66, 0x19, 0x8C, 0x33, 0x0D, 0x81, 0xE0, 0x70, 0x0C, 0x06, 0x01, 0x80, 0xC0, 0x30, 0x00, /* 0xD3 */ - 0x03, 0x00, 0x0C, 0x01, 0xFE, 0x1C, 0xCE, 0xE3, 0x1F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xE3, 0x1D, 0xCC, 0xE3, 0xFF, 0x00, 0xC0, - 0x03, 0x00, /* 0xD4 */ - 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 0xD5 */ - 0xC0, 0x66, 0x03, 0x30, 0x19, 0x80, 0xCC, 0x06, 0x60, 0x33, 0x01, 0x98, 0x0C, 0xC0, 0x66, 0x03, 0x30, 0x19, 0x80, 0xCF, 0xFF, - 0x80, 0x0C, 0x00, 0x60, /* 0xD6 */ - 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x06, 0xFF, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xD7 */ - 0xC3, 0x1E, 0x18, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xF0, 0xC7, 0x86, 0x3F, 0xFF, - 0x80, /* 0xD8 */ - 0xC3, 0x19, 0x86, 0x33, 0x0C, 0x66, 0x18, 0xCC, 0x31, 0x98, 0x63, 0x30, 0xC6, 0x61, 0x8C, 0xC3, 0x19, 0x86, 0x33, 0x0C, 0x66, - 0x18, 0xCF, 0xFF, 0xE0, 0x00, 0xC0, 0x01, 0x80, /* 0xD9 */ - 0xF8, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0F, 0xF0, 0x60, 0xC3, 0x03, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, 0x61, 0xFE, - 0x00, /* 0xDA */ - 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0xFE, 0x3C, 0x0C, 0xF0, 0x1B, 0xC0, 0x6F, 0x01, 0xBC, 0x06, 0xF0, 0x33, - 0xFF, 0x8C, /* 0xDB */ - 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0xFF, 0x30, 0x36, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 0xDC */ - 0x3F, 0x0C, 0x33, 0x83, 0x60, 0x20, 0x06, 0x00, 0x47, 0xF8, 0x01, 0xC0, 0x78, 0x0D, 0x81, 0x30, 0xC1, 0xF0, /* 0xDD */ - 0xC0, 0xF8, 0x61, 0x83, 0x31, 0x80, 0xD8, 0xC0, 0x6C, 0xC0, 0x1E, 0x60, 0x0F, 0xF0, 0x07, 0x98, 0x03, 0xCC, 0x01, 0xE3, 0x01, - 0xB1, 0x80, 0xD8, 0x60, 0xCC, 0x0F, 0x80, /* 0xDE */ - 0x3F, 0xD8, 0x3C, 0x0F, 0x03, 0xC0, 0xD8, 0x33, 0xFC, 0x33, 0x18, 0xCC, 0x36, 0x0D, 0x83, 0xC0, 0xC0, /* 0xDF */ - 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 0xE0 */ - 0x03, 0x1F, 0x78, 0x40, 0xFC, 0xE6, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xE1 */ - 0xFD, 0x8F, 0x0E, 0x3F, 0xDF, 0xB1, 0xE1, 0xC7, 0xF8, /* 0xE2 */ - 0xFE, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 0xE3 */ - 0x1F, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x61, 0x8C, 0x31, 0x9F, 0xFF, 0x01, 0xE0, 0x30, /* 0xE4 */ - 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE5 */ - 0xC6, 0x36, 0x66, 0x36, 0xC1, 0xF8, 0x0F, 0x01, 0xF8, 0x36, 0xC6, 0x66, 0xC6, 0x38, 0x61, /* 0xE6 */ - 0x79, 0x8C, 0x18, 0x30, 0x43, 0x01, 0xE3, 0xC6, 0xF8, /* 0xE7 */ - 0xC7, 0xC7, 0xCF, 0xCB, 0xCB, 0xD3, 0xD3, 0xF3, 0xE3, 0xE3, /* 0xE8 */ - 0x66, 0x18, 0xC7, 0xC7, 0xCF, 0xCB, 0xCB, 0xD3, 0xD3, 0xF3, 0xE3, 0xE3, /* 0xE9 */ - 0xC7, 0x9B, 0x66, 0x8E, 0x1E, 0x36, 0x66, 0xC7, 0x84, /* 0xEA */ - 0x7E, 0xCD, 0x9B, 0x36, 0x6C, 0xD9, 0xA3, 0xC7, 0x0C, /* 0xEB */ - 0xE3, 0xF1, 0xF8, 0xFE, 0xFF, 0x7E, 0xAF, 0x77, 0x93, 0xC9, 0xE0, 0xC0, /* 0xEC */ - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xED */ - 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xEE */ - 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xEF */ - 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 0xF0 */ - 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xF1 */ - 0xFC, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xF2 */ - 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xF3 */ - 0x03, 0x00, 0x0C, 0x03, 0xB7, 0x19, 0xE6, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x36, 0x79, 0x8E, 0xDC, - 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, /* 0xF4 */ - 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 0xF5 */ - 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x3F, 0xF0, 0x0C, 0x03, /* 0xF6 */ - 0xC7, 0x8F, 0x1E, 0x3C, 0x6F, 0xC1, 0x83, 0x06, 0x0C, /* 0xF7 */ - 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xFF, 0xF0, /* 0xF8 */ - 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xFF, 0x00, 0x30, 0x03, /* 0xF9 */ - 0xF0, 0x18, 0x0C, 0x06, 0x03, 0xF1, 0x8C, 0xC6, 0x63, 0x31, 0x9F, 0x80, /* 0xFA */ - 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xFE, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xFF, 0xB0, /* 0xFB */ - 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC3, 0xC3, 0xC3, 0xC3, 0xFE, /* 0xFC */ - 0x3C, 0x62, 0xC3, 0x01, 0x1F, 0x01, 0x01, 0xC3, 0x62, 0x3C, /* 0xFD */ - 0xC7, 0xCC, 0xC6, 0xD8, 0x3D, 0x83, 0xF8, 0x3D, 0x83, 0xD8, 0x3C, 0xC2, 0xCC, 0x6C, 0x7C, /* 0xFE */ - 0x7F, 0xC3, 0xC3, 0xC3, 0x7F, 0x13, 0x33, 0x63, 0xC3, 0x83, /* 0xFF */ +/* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, +/* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, +/* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, +/* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, +/* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, +/* 0x0A */ +/* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, +/* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, +/* 0x0D */ +/* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, +/* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, +/* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, +/* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, +/* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, +/* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, +/* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, +/* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, +/* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, +/* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, +/* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, +/* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, +/* '"' 0x22 */ 0xDE, 0xF7, 0x20, +/* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, +/* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, +/* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, +/* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, +/* ''' 0x27 */ 0xFE, +/* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, +/* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, +/* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, +/* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, +/* ',' 0x2C */ 0xD6, +/* '-' 0x2D */ 0xF0, +/* '.' 0x2E */ 0xC0, +/* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, +/* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, +/* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, +/* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, +/* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, +/* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, +/* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, +/* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, +/* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, +/* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, +/* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, +/* ':' 0x3A */ 0xC0, 0x00, 0x30, +/* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, +/* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, +/* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, +/* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, +/* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, +/* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, +/* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, +/* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, +/* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, +/* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, +/* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, +/* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, +/* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, +/* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, +/* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, +/* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, +/* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, +/* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, +/* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, +/* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, +/* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, +/* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, +/* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, +/* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, +/* '_' 0x5F */ 0xFF, 0xC0, +/* '`' 0x60 */ 0xC6, 0x30, +/* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, +/* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, +/* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, +/* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, +/* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, +/* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, +/* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, +/* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, +/* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, +/* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, +/* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, +/* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, +/* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, +/* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, +/* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, +/* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, +/* '~' 0x7E */ 0x61, 0x24, 0x38, +/* 0x7F */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, 0xFF, 0xFC, +/* 0x80 */ 0xFF, 0x01, 0x80, 0x18, 0x01, 0x80, 0x18, 0x01, 0xFE, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x30, 0x03, 0x00, 0x30, 0x0E, +/* 0x81 */ 0x0C, 0x18, 0x00, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0x82 */ 0xDC, +/* 0x83 */ 0x18, 0x89, 0xFC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, +/* 0x84 */ 0xDA, 0x76, +/* 0x85 */ 0xCC, 0xC0, +/* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, +/* 0x88 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, +/* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, +/* 0x8A */ 0x3F, 0x80, 0x18, 0xC0, 0x0C, 0x60, 0x06, 0x30, 0x03, 0x18, 0x01, 0x8C, 0x00, 0xC7, 0xF8, 0x63, 0x06, 0x31, 0x81, 0x90, 0xC0, 0xD8, 0x60, 0x6C, 0x30, 0x6C, 0x1F, 0xE0, +/* 0x8B */ 0x69, +/* 0x8C */ 0xC0, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0xFF, 0xF9, 0x81, 0x86, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x6C, 0x0F, 0xE0, +/* 0x8D */ 0x0C, 0x06, 0x0C, 0x1B, 0x0C, 0xC6, 0x33, 0x0D, 0x83, 0xC0, 0xF0, 0x3E, 0x0D, 0xC3, 0x38, 0xC7, 0x30, 0xEC, 0x1C, +/* 0x8E */ 0xFF, 0x01, 0x80, 0x18, 0x01, 0x80, 0x18, 0x01, 0xFE, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x30, +/* 0x8F */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3F, 0xFE, 0x0C, 0x01, 0x80, +/* 0x90 */ 0x60, 0x7C, 0x18, 0x0D, 0xE7, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x18, 0x08, 0x08, +/* 0x91 */ 0x6B, +/* 0x92 */ 0xD6, +/* 0x93 */ 0x4C, 0xA5, 0xB0, +/* 0x94 */ 0xDA, 0x53, 0x20, +/* 0x95 */ 0x6F, 0xFF, 0x60, +/* 0x96 */ 0xFE, +/* 0x97 */ 0xFF, 0xFF, +/* 0x98 */ +/* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, +/* 0x9A */ 0x7E, 0x03, 0x30, 0x19, 0x80, 0xCC, 0x06, 0x60, 0x33, 0xF9, 0x98, 0x6C, 0xC3, 0x46, 0x1E, 0x3F, 0x80, +/* 0x9B */ 0x96, +/* 0x9C */ 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xC3, 0x03, 0xFF, 0xEC, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0F, 0xE0, +/* 0x9D */ 0x0C, 0x30, 0x46, 0x3C, 0xDB, 0x34, 0x70, 0xF1, 0xB3, 0x36, 0x3C, 0x20, +/* 0x9E */ 0x60, 0x7C, 0x18, 0x0D, 0xE7, 0x3B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, +/* 0x9F */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x18, 0x18, +/* 0xA0 */ +/* 0xA1 */ 0x21, 0x07, 0x8C, 0x0F, 0x06, 0x61, 0x98, 0xC3, 0x30, 0xD8, 0x1E, 0x07, 0x00, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0x00, +/* 0xA2 */ 0x66, 0x18, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 0xA3 */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, +/* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, +/* 0xA5 */ 0x00, 0xC0, 0x3F, 0xFF, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, +/* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, +/* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, +/* 0xA8 */ 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFE, +/* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAA */ 0x1F, 0x86, 0x19, 0x81, 0xB0, 0x3C, 0x01, 0x80, 0x3F, 0xC6, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, +/* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xAD */ +/* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, +/* 0xB0 */ 0x74, 0x63, 0x17, 0x00, +/* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, +/* 0xB2 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xB3 */ 0xC3, 0xFF, 0xFF, 0xC0, +/* 0xB4 */ 0x0C, 0x3F, 0xF0, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, +/* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, +/* 0xB7 */ 0xE0, +/* 0xB8 */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xB9 */ 0xC1, 0x81, 0x83, 0x03, 0x86, 0x05, 0x0C, 0xEB, 0x1A, 0x32, 0x34, 0x66, 0x68, 0xC4, 0xD1, 0x8D, 0xB3, 0x0B, 0x3A, 0x1E, 0x04, 0x1C, 0x08, 0x1B, 0xC0, +/* 0xBA */ 0x3C, 0x46, 0xC3, 0x80, 0xF8, 0x80, 0x80, 0xC3, 0x46, 0x3C, +/* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, +/* 0xBC */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, +/* 0xBD */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 0xBE */ 0x3E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 0xBF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xC0 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, +/* 0xC1 */ 0xFF, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x3F, 0xE6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 0xC2 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 0xC3 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xC4 */ 0x1F, 0xF0, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x03, 0x0C, 0x0C, 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0xF0, 0x03, +/* 0xC5 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, +/* 0xC6 */ 0x61, 0x86, 0x31, 0x8C, 0x19, 0x98, 0x19, 0x98, 0x0D, 0xB0, 0x07, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x0D, 0xB0, 0x19, 0x98, 0x31, 0x8C, 0x61, 0x86, 0xC1, 0x83, +/* 0xC7 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0x00, 0xC0, 0x60, 0xF0, 0x06, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 0xC8 */ 0xC0, 0xF8, 0x1F, 0x07, 0xE0, 0xBC, 0x37, 0x8C, 0xF1, 0x1E, 0x63, 0xD8, 0x7A, 0x0F, 0xC1, 0xF0, 0x3E, 0x06, +/* 0xC9 */ 0x11, 0x03, 0xE0, 0x00, 0x60, 0x7C, 0x0F, 0x83, 0xF0, 0x5E, 0x1B, 0xC6, 0x78, 0x8F, 0x31, 0xEC, 0x3D, 0x07, 0xE0, 0xF8, 0x1F, 0x03, +/* 0xCA */ 0xC1, 0xB0, 0xCC, 0x63, 0x30, 0xD8, 0x3C, 0x0F, 0x03, 0xE0, 0xDC, 0x33, 0x8C, 0x73, 0x0E, 0xC1, 0xC0, +/* 0xCB */ 0x3F, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xC8, 0x36, 0x0D, 0x83, 0xC0, 0xC0, +/* 0xCC */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, +/* 0xCD */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 0xCE */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, +/* 0xCF */ 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 0xD0 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 0xD1 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 0xD2 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, +/* 0xD3 */ 0xC0, 0xF0, 0x66, 0x19, 0x8C, 0x33, 0x0D, 0x81, 0xE0, 0x70, 0x0C, 0x06, 0x01, 0x80, 0xC0, 0x30, 0x00, +/* 0xD4 */ 0x03, 0x00, 0x0C, 0x01, 0xFE, 0x1C, 0xCE, 0xE3, 0x1F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xE3, 0x1D, 0xCC, 0xE3, 0xFF, 0x00, 0xC0, 0x03, 0x00, +/* 0xD5 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, +/* 0xD6 */ 0xC0, 0x66, 0x03, 0x30, 0x19, 0x80, 0xCC, 0x06, 0x60, 0x33, 0x01, 0x98, 0x0C, 0xC0, 0x66, 0x03, 0x30, 0x19, 0x80, 0xCF, 0xFF, 0x80, 0x0C, 0x00, 0x60, +/* 0xD7 */ 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x06, 0xFF, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xD8 */ 0xC3, 0x1E, 0x18, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xF0, 0xC7, 0x86, 0x3F, 0xFF, 0x80, +/* 0xD9 */ 0xC3, 0x19, 0x86, 0x33, 0x0C, 0x66, 0x18, 0xCC, 0x31, 0x98, 0x63, 0x30, 0xC6, 0x61, 0x8C, 0xC3, 0x19, 0x86, 0x33, 0x0C, 0x66, 0x18, 0xCF, 0xFF, 0xE0, 0x00, 0xC0, 0x01, 0x80, +/* 0xDA */ 0xF8, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0F, 0xF0, 0x60, 0xC3, 0x03, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, 0x61, 0xFE, 0x00, +/* 0xDB */ 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0xFE, 0x3C, 0x0C, 0xF0, 0x1B, 0xC0, 0x6F, 0x01, 0xBC, 0x06, 0xF0, 0x33, 0xFF, 0x8C, +/* 0xDC */ 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0xFF, 0x30, 0x36, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 0xDD */ 0x3F, 0x0C, 0x33, 0x83, 0x60, 0x20, 0x06, 0x00, 0x47, 0xF8, 0x01, 0xC0, 0x78, 0x0D, 0x81, 0x30, 0xC1, 0xF0, +/* 0xDE */ 0xC0, 0xF8, 0x61, 0x83, 0x31, 0x80, 0xD8, 0xC0, 0x6C, 0xC0, 0x1E, 0x60, 0x0F, 0xF0, 0x07, 0x98, 0x03, 0xCC, 0x01, 0xE3, 0x01, 0xB1, 0x80, 0xD8, 0x60, 0xCC, 0x0F, 0x80, +/* 0xDF */ 0x3F, 0xD8, 0x3C, 0x0F, 0x03, 0xC0, 0xD8, 0x33, 0xFC, 0x33, 0x18, 0xCC, 0x36, 0x0D, 0x83, 0xC0, 0xC0, +/* 0xE0 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, +/* 0xE1 */ 0x03, 0x1F, 0x78, 0x40, 0xFC, 0xE6, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xE2 */ 0xFD, 0x8F, 0x0E, 0x3F, 0xDF, 0xB1, 0xE1, 0xC7, 0xF8, +/* 0xE3 */ 0xFE, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, +/* 0xE4 */ 0x1F, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x61, 0x8C, 0x31, 0x9F, 0xFF, 0x01, 0xE0, 0x30, +/* 0xE5 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xE6 */ 0xC6, 0x36, 0x66, 0x36, 0xC1, 0xF8, 0x0F, 0x01, 0xF8, 0x36, 0xC6, 0x66, 0xC6, 0x38, 0x61, +/* 0xE7 */ 0x79, 0x8C, 0x18, 0x30, 0x43, 0x01, 0xE3, 0xC6, 0xF8, +/* 0xE8 */ 0xC7, 0xC7, 0xCF, 0xCB, 0xCB, 0xD3, 0xD3, 0xF3, 0xE3, 0xE3, +/* 0xE9 */ 0x66, 0x18, 0xC7, 0xC7, 0xCF, 0xCB, 0xCB, 0xD3, 0xD3, 0xF3, 0xE3, 0xE3, +/* 0xEA */ 0xC7, 0x9B, 0x66, 0x8E, 0x1E, 0x36, 0x66, 0xC7, 0x84, +/* 0xEB */ 0x7E, 0xCD, 0x9B, 0x36, 0x6C, 0xD9, 0xA3, 0xC7, 0x0C, +/* 0xEC */ 0xE3, 0xF1, 0xF8, 0xFE, 0xFF, 0x7E, 0xAF, 0x77, 0x93, 0xC9, 0xE0, 0xC0, +/* 0xED */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, +/* 0xEE */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xEF */ 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 0xF0 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, +/* 0xF1 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xF2 */ 0xFC, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xF3 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 0xF4 */ 0x03, 0x00, 0x0C, 0x03, 0xB7, 0x19, 0xE6, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x36, 0x79, 0x8E, 0xDC, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, +/* 0xF5 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, +/* 0xF6 */ 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x3F, 0xF0, 0x0C, 0x03, +/* 0xF7 */ 0xC7, 0x8F, 0x1E, 0x3C, 0x6F, 0xC1, 0x83, 0x06, 0x0C, +/* 0xF8 */ 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xFF, 0xF0, +/* 0xF9 */ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xFF, 0x00, 0x30, 0x03, +/* 0xFA */ 0xF0, 0x18, 0x0C, 0x06, 0x03, 0xF1, 0x8C, 0xC6, 0x63, 0x31, 0x9F, 0x80, +/* 0xFB */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xFE, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xFF, 0xB0, +/* 0xFC */ 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC3, 0xC3, 0xC3, 0xC3, 0xFE, +/* 0xFD */ 0x3C, 0x62, 0xC3, 0x01, 0x1F, 0x01, 0x01, 0xC3, 0x62, 0x3C, +/* 0xFE */ 0xC7, 0xCC, 0xC6, 0xD8, 0x3D, 0x83, 0xF8, 0x3D, 0x83, 0xD8, 0x3C, 0xC2, 0xCC, 0x6C, 0x7C, +/* 0xFF */ 0x7F, 0xC3, 0xC3, 0xC3, 0x7F, 0x13, 0x33, 0x63, 0xC3, 0x83, }; const GFXglyph FreeSans9pt_Win1251Glyphs[] PROGMEM = { - /* ' ' 0x20 */ {0, 0, 0, 5, 0, 0}, - /* '!' 0x21 */ {0, 2, 13, 6, 2, -12}, - /* '"' 0x22 */ {4, 5, 4, 6, 1, -12}, - /* '#' 0x23 */ {7, 10, 12, 10, 0, -11}, - /* '$' 0x24 */ {22, 9, 16, 10, 1, -13}, - /* '%' 0x25 */ {40, 16, 13, 16, 1, -12}, - /* '&' 0x26 */ {66, 10, 13, 12, 1, -12}, - /* ''' 0x27 */ {83, 2, 4, 4, 1, -12}, - /* '(' 0x28 */ {84, 4, 17, 6, 1, -12}, - /* ')' 0x29 */ {93, 4, 17, 6, 1, -12}, - /* '*' 0x2A */ {102, 5, 5, 7, 1, -12}, - /* '+' 0x2B */ {106, 6, 8, 11, 3, -7}, - /* ',' 0x2C */ {112, 2, 4, 5, 2, 0}, - /* '-' 0x2D */ {113, 4, 1, 6, 1, -4}, - /* '.' 0x2E */ {114, 2, 1, 5, 1, 0}, - /* '/' 0x2F */ {115, 5, 13, 5, 0, -12}, - /* '0' 0x30 */ {124, 8, 13, 10, 1, -12}, - /* '1' 0x31 */ {137, 4, 13, 10, 3, -12}, - /* '2' 0x32 */ {144, 9, 13, 10, 1, -12}, - /* '3' 0x33 */ {159, 8, 13, 10, 1, -12}, - /* '4' 0x34 */ {172, 7, 13, 10, 2, -12}, - /* '5' 0x35 */ {184, 9, 13, 10, 1, -12}, - /* '6' 0x36 */ {199, 9, 13, 10, 1, -12}, - /* '7' 0x37 */ {214, 8, 13, 10, 0, -12}, - /* '8' 0x38 */ {227, 9, 13, 10, 1, -12}, - /* '9' 0x39 */ {242, 8, 13, 10, 1, -12}, - /* ':' 0x3A */ {255, 2, 10, 5, 1, -9}, - /* ';' 0x3B */ {258, 3, 12, 5, 1, -8}, - /* '<' 0x3C */ {263, 9, 9, 11, 1, -8}, - /* '=' 0x3D */ {274, 9, 4, 11, 1, -5}, - /* '>' 0x3E */ {279, 9, 8, 11, 1, -7}, - /* '?' 0x3F */ {288, 9, 13, 10, 1, -12}, - /* '@' 0x40 */ {303, 17, 16, 18, 1, -12}, - /* 'A' 0x41 */ {337, 12, 13, 12, 0, -12}, - /* 'B' 0x42 */ {357, 11, 13, 12, 1, -12}, - /* 'C' 0x43 */ {375, 11, 13, 13, 1, -12}, - /* 'D' 0x44 */ {393, 11, 13, 13, 1, -12}, - /* 'E' 0x45 */ {411, 9, 13, 11, 1, -12}, - /* 'F' 0x46 */ {426, 8, 13, 11, 1, -12}, - /* 'G' 0x47 */ {439, 12, 13, 14, 1, -12}, - /* 'H' 0x48 */ {459, 11, 13, 13, 1, -12}, - /* 'I' 0x49 */ {477, 2, 13, 5, 2, -12}, - /* 'J' 0x4A */ {481, 7, 13, 10, 1, -12}, - /* 'K' 0x4B */ {493, 10, 13, 12, 1, -12}, - /* 'L' 0x4C */ {510, 8, 13, 10, 1, -12}, - /* 'M' 0x4D */ {523, 13, 13, 15, 1, -12}, - /* 'N' 0x4E */ {545, 11, 13, 13, 1, -12}, - /* 'O' 0x4F */ {563, 13, 13, 14, 1, -12}, - /* 'P' 0x50 */ {585, 10, 13, 12, 1, -12}, - /* 'Q' 0x51 */ {602, 13, 14, 14, 1, -12}, - /* 'R' 0x52 */ {625, 12, 13, 13, 1, -12}, - /* 'S' 0x53 */ {645, 10, 13, 12, 1, -12}, - /* 'T' 0x54 */ {662, 9, 13, 11, 1, -12}, - /* 'U' 0x55 */ {677, 11, 13, 13, 1, -12}, - /* 'V' 0x56 */ {695, 11, 13, 11, 0, -12}, - /* 'W' 0x57 */ {713, 16, 13, 17, 0, -12}, - /* 'X' 0x58 */ {739, 10, 13, 12, 1, -12}, - /* 'Y' 0x59 */ {756, 12, 13, 12, 0, -12}, - /* 'Z' 0x5A */ {776, 10, 13, 11, 1, -12}, - /* '[' 0x5B */ {793, 3, 17, 5, 1, -12}, - /* '\' 0x5C */ {800, 5, 13, 5, 0, -12}, - /* ']' 0x5D */ {809, 3, 17, 5, 0, -12}, - /* '^' 0x5E */ {816, 7, 7, 8, 1, -12}, - /* '_' 0x5F */ {823, 10, 1, 10, 0, 3}, - /* '`' 0x60 */ {825, 4, 3, 5, 0, -12}, - /* 'a' 0x61 */ {827, 9, 10, 10, 1, -9}, - /* 'b' 0x62 */ {839, 9, 13, 10, 1, -12}, - /* 'c' 0x63 */ {854, 8, 10, 9, 1, -9}, - /* 'd' 0x64 */ {864, 8, 13, 10, 1, -12}, - /* 'e' 0x65 */ {877, 8, 10, 10, 1, -9}, - /* 'f' 0x66 */ {887, 4, 13, 5, 1, -12}, - /* 'g' 0x67 */ {894, 8, 14, 10, 1, -9}, - /* 'h' 0x68 */ {908, 8, 13, 10, 1, -12}, - /* 'i' 0x69 */ {921, 2, 13, 4, 1, -12}, - /* 'j' 0x6A */ {925, 4, 17, 4, 0, -12}, - /* 'k' 0x6B */ {934, 8, 13, 9, 1, -12}, - /* 'l' 0x6C */ {947, 2, 13, 4, 1, -12}, - /* 'm' 0x6D */ {951, 13, 10, 15, 1, -9}, - /* 'n' 0x6E */ {968, 8, 10, 10, 1, -9}, - /* 'o' 0x6F */ {978, 8, 10, 10, 1, -9}, - /* 'p' 0x70 */ {988, 9, 13, 10, 1, -9}, - /* 'q' 0x71 */ {1003, 8, 13, 10, 1, -9}, - /* 'r' 0x72 */ {1016, 5, 10, 6, 1, -9}, - /* 's' 0x73 */ {1023, 8, 10, 9, 1, -9}, - /* 't' 0x74 */ {1033, 4, 12, 5, 1, -11}, - /* 'u' 0x75 */ {1039, 8, 10, 10, 1, -9}, - /* 'v' 0x76 */ {1049, 9, 10, 9, 0, -9}, - /* 'w' 0x77 */ {1061, 13, 10, 13, 0, -9}, - /* 'x' 0x78 */ {1078, 7, 10, 9, 1, -9}, - /* 'y' 0x79 */ {1087, 8, 14, 9, 0, -9}, - /* 'z' 0x7A */ {1101, 7, 10, 9, 1, -9}, - /* '{' 0x7B */ {1110, 4, 17, 6, 1, -12}, - /* '|' 0x7C */ {1119, 2, 17, 4, 2, -12}, - /* '}' 0x7D */ {1124, 4, 17, 6, 1, -12}, - /* '~' 0x7E */ {1133, 7, 3, 9, 1, -7}, - /* 0x7F */ {1136, 13, 14, 15, 1, -12}, - /* 0x80 */ {1159, 12, 16, 14, 1, -12}, - /* 0x81 */ {1183, 8, 15, 11, 1, -14}, - /* 0x82 */ {1198, 2, 3, 5, 1, 0}, - /* 0x83 */ {1199, 5, 13, 7, 1, -12}, - /* 0x84 */ {1208, 5, 3, 7, 1, 0}, - /* 0x85 */ {1210, 10, 1, 12, 1, 0}, - /* 0x86 */ {1212, 8, 16, 10, 1, -12}, - /* 0x87 */ {1228, 8, 16, 10, 1, -12}, - /* 0x88 */ {1244, 10, 13, 12, 1, -12}, - /* 0x89 */ {1261, 18, 13, 18, 0, -12}, - /* 0x8A */ {1291, 17, 13, 18, 1, -12}, - /* 0x8B */ {1319, 2, 4, 4, 1, -6}, - /* 0x8C */ {1320, 17, 13, 18, 1, -12}, - /* 0x8D */ {1348, 10, 15, 11, 1, -14}, - /* 0x8E */ {1367, 12, 13, 14, 1, -12}, - /* 0x8F */ {1387, 11, 15, 13, 1, -12}, - /* 0x90 */ {1408, 9, 16, 10, 1, -12}, - /* 0x91 */ {1426, 2, 4, 4, 2, -12}, - /* 0x92 */ {1427, 2, 4, 4, 1, -12}, - /* 0x93 */ {1428, 5, 4, 7, 2, -12}, - /* 0x94 */ {1431, 5, 4, 7, 1, -12}, - /* 0x95 */ {1434, 4, 5, 7, 1, -8}, - /* 0x96 */ {1437, 7, 1, 9, 1, -4}, - /* 0x97 */ {1438, 16, 1, 18, 1, -4}, - /* 0x98 */ {1440, 0, 0, 0, 0, 0}, - /* 0x99 */ {1440, 18, 10, 18, 1, -13}, - /* 0x9A */ {1463, 13, 10, 14, 1, -9}, - /* 0x9B */ {1480, 2, 4, 5, 2, -6}, - /* 0x9C */ {1481, 14, 10, 15, 1, -9}, - /* 0x9D */ {1499, 7, 13, 9, 1, -12}, - /* 0x9E */ {1511, 9, 13, 10, 1, -12}, - /* 0x9F */ {1526, 8, 12, 10, 1, -9}, - /* 0xA0 */ {1538, 0, 0, 5, 0, 0}, - /* 0xA1 */ {1538, 10, 15, 11, 1, -14}, - /* 0xA2 */ {1557, 8, 16, 9, 0, -11}, - /* 0xA3 */ {1573, 7, 13, 10, 1, -12}, - /* 0xA4 */ {1585, 7, 6, 10, 2, -8}, - /* 0xA5 */ {1591, 10, 14, 11, 1, -13}, - /* 0xA6 */ {1609, 2, 17, 5, 2, -12}, - /* 0xA7 */ {1614, 9, 17, 10, 1, -12}, - /* 0xA8 */ {1634, 9, 15, 12, 1, -14}, - /* 0xA9 */ {1651, 14, 13, 14, 1, -12}, - /* 0xAA */ {1674, 11, 13, 13, 1, -12}, - /* 0xAB */ {1692, 7, 6, 9, 1, -7}, - /* 0xAC */ {1698, 9, 5, 11, 2, -5}, - /* 0xAD */ {1704, 0, 0, 0, 0, 0}, - /* 0xAE */ {1704, 14, 13, 14, 1, -12}, - /* 0xAF */ {1727, 6, 15, 5, 0, -14}, - /* 0xB0 */ {1739, 5, 5, 11, 3, -11}, - /* 0xB1 */ {1743, 9, 11, 11, 1, -10}, - /* 0xB2 */ {1756, 2, 13, 4, 1, -12}, - /* 0xB3 */ {1760, 2, 13, 4, 1, -12}, - /* 0xB4 */ {1764, 6, 12, 7, 1, -11}, - /* 0xB5 */ {1773, 9, 13, 10, 1, -9}, - /* 0xB6 */ {1788, 8, 16, 10, 2, -12}, - /* 0xB7 */ {1804, 3, 1, 5, 1, -4}, - /* 0xB8 */ {1805, 8, 12, 10, 1, -11}, - /* 0xB9 */ {1817, 15, 13, 17, 1, -12}, - /* 0xBA */ {1842, 8, 10, 9, 1, -9}, - /* 0xBB */ {1852, 7, 6, 9, 1, -7}, - /* 0xBC */ {1858, 4, 17, 4, 0, -12}, - /* 0xBD */ {1867, 10, 13, 12, 1, -12}, - /* 0xBE */ {1884, 8, 10, 9, 1, -9}, - /* 0xBF */ {1894, 6, 12, 5, -1, -11}, - /* 0xC0 */ {1903, 12, 13, 12, 0, -12}, - /* 0xC1 */ {1923, 11, 13, 12, 1, -12}, - /* 0xC2 */ {1941, 11, 13, 12, 1, -12}, - /* 0xC3 */ {1959, 8, 13, 8, 1, -12}, - /* 0xC4 */ {1972, 14, 16, 15, 1, -12}, - /* 0xC5 */ {2000, 9, 13, 12, 1, -12}, - /* 0xC6 */ {2015, 16, 13, 16, 0, -12}, - /* 0xC7 */ {2041, 10, 13, 12, 1, -12}, - /* 0xC8 */ {2058, 11, 13, 13, 1, -12}, - /* 0xC9 */ {2076, 11, 16, 13, 1, -15}, - /* 0xCA */ {2098, 10, 13, 11, 1, -12}, - /* 0xCB */ {2115, 10, 13, 12, 1, -12}, - /* 0xCC */ {2132, 13, 13, 15, 1, -12}, - /* 0xCD */ {2154, 11, 13, 13, 1, -12}, - /* 0xCE */ {2172, 13, 13, 14, 1, -12}, - /* 0xCF */ {2194, 11, 13, 13, 1, -12}, - /* 0xD0 */ {2212, 10, 13, 12, 1, -12}, - /* 0xD1 */ {2229, 11, 13, 13, 1, -12}, - /* 0xD2 */ {2247, 9, 13, 11, 1, -12}, - /* 0xD3 */ {2262, 10, 13, 11, 1, -12}, - /* 0xD4 */ {2279, 14, 13, 15, 1, -12}, - /* 0xD5 */ {2302, 10, 13, 12, 1, -12}, - /* 0xD6 */ {2319, 13, 15, 13, 1, -12}, - /* 0xD7 */ {2344, 9, 13, 11, 1, -12}, - /* 0xD8 */ {2359, 13, 13, 15, 1, -12}, - /* 0xD9 */ {2381, 15, 15, 15, 1, -12}, - /* 0xDA */ {2410, 13, 13, 15, 2, -12}, - /* 0xDB */ {2432, 14, 13, 16, 1, -12}, - /* 0xDC */ {2455, 11, 13, 12, 1, -12}, - /* 0xDD */ {2473, 11, 13, 13, 1, -12}, - /* 0xDE */ {2491, 17, 13, 18, 1, -12}, - /* 0xDF */ {2519, 10, 13, 12, 1, -12}, - /* 0xE0 */ {2536, 9, 10, 10, 1, -9}, - /* 0xE1 */ {2548, 8, 14, 10, 1, -13}, - /* 0xE2 */ {2562, 7, 10, 9, 1, -9}, - /* 0xE3 */ {2571, 5, 10, 7, 1, -9}, - /* 0xE4 */ {2578, 11, 12, 10, 0, -9}, - /* 0xE5 */ {2595, 8, 10, 10, 1, -9}, - /* 0xE6 */ {2605, 12, 10, 14, 1, -9}, - /* 0xE7 */ {2620, 7, 10, 9, 1, -9}, - /* 0xE8 */ {2629, 8, 10, 10, 1, -9}, - /* 0xE9 */ {2639, 8, 12, 10, 1, -11}, - /* 0xEA */ {2651, 7, 10, 9, 1, -9}, - /* 0xEB */ {2660, 7, 10, 8, 0, -9}, - /* 0xEC */ {2669, 9, 10, 11, 1, -9}, - /* 0xED */ {2681, 8, 10, 10, 1, -9}, - /* 0xEE */ {2691, 8, 10, 10, 1, -9}, - /* 0xEF */ {2701, 8, 10, 10, 1, -9}, - /* 0xF0 */ {2711, 9, 13, 10, 1, -9}, - /* 0xF1 */ {2726, 8, 10, 9, 1, -9}, - /* 0xF2 */ {2736, 6, 10, 7, 1, -9}, - /* 0xF3 */ {2744, 8, 14, 9, 0, -9}, - /* 0xF4 */ {2758, 14, 15, 15, 1, -11}, - /* 0xF5 */ {2785, 7, 10, 9, 1, -9}, - /* 0xF6 */ {2794, 10, 12, 10, 1, -9}, - /* 0xF7 */ {2809, 7, 10, 9, 1, -9}, - /* 0xF8 */ {2818, 10, 10, 12, 1, -9}, - /* 0xF9 */ {2831, 12, 12, 13, 1, -9}, - /* 0xFA */ {2849, 9, 10, 12, 2, -9}, - /* 0xFB */ {2861, 10, 10, 12, 1, -9}, - /* 0xFC */ {2874, 8, 10, 9, 1, -9}, - /* 0xFD */ {2884, 8, 10, 9, 1, -9}, - /* 0xFE */ {2894, 12, 10, 13, 1, -9}, - /* 0xFF */ {2909, 8, 10, 10, 1, -9}, +/* 0x01 */ { 0, 15, 15, 17, 1, -13 }, +/* 0x02 */ { 29, 15, 15, 17, 1, -13 }, +/* 0x03 */ { 58, 15, 16, 17, 1, -14 }, +/* 0x04 */ { 88, 15, 16, 17, 1, -14 }, +/* 0x05 */ { 118, 16, 15, 18, 1, -13 }, +/* 0x06 */ { 148, 15, 15, 17, 1, -13 }, +/* 0x07 */ { 177, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 177, 17, 16, 19, 1, -14 }, +/* 0x09 */ { 211, 17, 12, 19, 1, -12 }, +/* 0x0A */ { 237, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 237, 17, 16, 19, 1, -14 }, +/* 0x0C */ { 271, 15, 14, 17, 1, -12 }, +/* 0x0D */ { 298, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 298, 15, 16, 17, 1, -14 }, +/* 0x0F */ { 328, 15, 15, 17, 1, -13 }, +/* 0x10 */ { 357, 15, 15, 17, 1, -13 }, +/* 0x11 */ { 386, 15, 16, 17, 1, -14 }, +/* 0x12 */ { 416, 17, 17, 19, 1, -15 }, +/* 0x13 */ { 453, 15, 16, 17, 1, -14 }, +/* 0x14 */ { 483, 15, 16, 17, 1, -14 }, +/* 0x15 */ { 513, 15, 16, 17, 1, -14 }, +/* 0x16 */ { 543, 11, 16, 13, 1, -14 }, +/* 0x17 */ { 565, 15, 16, 17, 1, -14 }, +/* 0x18 */ { 595, 18, 15, 20, 1, -13 }, +/* 0x19 */ { 629, 15, 16, 17, 1, -14 }, +/* 0x1A */ { 659, 13, 14, 15, 1, -12 }, +/* 0x1B */ { 682, 17, 16, 19, 1, -14 }, +/* 0x1C */ { 716, 15, 16, 17, 1, -14 }, +/* 0x1D */ { 746, 15, 15, 17, 1, -13 }, +/* 0x1E */ { 775, 17, 16, 19, 1, -14 }, +/* 0x1F */ { 809, 11, 16, 13, 1, -14 }, +/* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, +/* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, +/* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, +/* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, +/* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, +/* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, +/* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, +/* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, +/* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, +/* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, +/* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, +/* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, +/* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, +/* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, +/* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, +/* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, +/* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, +/* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, +/* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, +/* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, +/* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, +/* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, +/* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, +/* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, +/* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, +/* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, +/* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, +/* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, +/* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, +/* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, +/* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, +/* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, +/* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, +/* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, +/* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, +/* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, +/* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, +/* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, +/* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, +/* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, +/* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, +/* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, +/* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, +/* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, +/* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, +/* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, +/* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, +/* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, +/* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, +/* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, +/* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, +/* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, +/* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, +/* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, +/* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, +/* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, +/* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, +/* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, +/* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, +/* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, +/* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, +/* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, +/* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, +/* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, +/* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, +/* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, +/* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, +/* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, +/* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, +/* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, +/* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, +/* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, +/* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, +/* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, +/* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, +/* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, +/* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, +/* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, +/* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, +/* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, +/* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, +/* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, +/* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, +/* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, +/* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, +/* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, +/* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, +/* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, +/* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, +/* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, +/* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, +/* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, +/* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, +/* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, +/* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, +/* 0x7F */ { 1967, 13, 14, 15, 1, -12 }, +/* 0x80 */ { 1990, 12, 16, 14, 1, -12 }, +/* 0x81 */ { 2014, 8, 15, 11, 1, -14 }, +/* 0x82 */ { 2029, 2, 3, 5, 1, 0 }, +/* 0x83 */ { 2030, 5, 13, 7, 1, -12 }, +/* 0x84 */ { 2039, 5, 3, 7, 1, 0 }, +/* 0x85 */ { 2041, 10, 1, 12, 1, 0 }, +/* 0x86 */ { 2043, 8, 16, 10, 1, -12 }, +/* 0x87 */ { 2059, 8, 16, 10, 1, -12 }, +/* 0x88 */ { 2075, 10, 13, 12, 1, -12 }, +/* 0x89 */ { 2092, 18, 13, 18, 0, -12 }, +/* 0x8A */ { 2122, 17, 13, 18, 1, -12 }, +/* 0x8B */ { 2150, 2, 4, 4, 1, -6 }, +/* 0x8C */ { 2151, 17, 13, 18, 1, -12 }, +/* 0x8D */ { 2179, 10, 15, 11, 1, -14 }, +/* 0x8E */ { 2198, 12, 13, 14, 1, -12 }, +/* 0x8F */ { 2218, 11, 15, 13, 1, -12 }, +/* 0x90 */ { 2239, 9, 16, 10, 1, -12 }, +/* 0x91 */ { 2257, 2, 4, 4, 2, -12 }, +/* 0x92 */ { 2258, 2, 4, 4, 1, -12 }, +/* 0x93 */ { 2259, 5, 4, 7, 2, -12 }, +/* 0x94 */ { 2262, 5, 4, 7, 1, -12 }, +/* 0x95 */ { 2265, 4, 5, 7, 1, -8 }, +/* 0x96 */ { 2268, 7, 1, 9, 1, -4 }, +/* 0x97 */ { 2269, 16, 1, 18, 1, -4 }, +/* 0x98 */ { 2271, 0, 0, 0, 0, 0 }, +/* 0x99 */ { 2271, 18, 10, 18, 1, -13 }, +/* 0x9A */ { 2294, 13, 10, 14, 1, -9 }, +/* 0x9B */ { 2311, 2, 4, 5, 2, -6 }, +/* 0x9C */ { 2312, 14, 10, 15, 1, -9 }, +/* 0x9D */ { 2330, 7, 13, 9, 1, -12 }, +/* 0x9E */ { 2342, 9, 13, 10, 1, -12 }, +/* 0x9F */ { 2357, 8, 12, 10, 1, -9 }, +/* 0xA0 */ { 2369, 0, 0, 5, 0, 0 }, +/* 0xA1 */ { 2369, 10, 15, 11, 1, -14 }, +/* 0xA2 */ { 2388, 8, 16, 9, 0, -11 }, +/* 0xA3 */ { 2404, 7, 13, 10, 1, -12 }, +/* 0xA4 */ { 2416, 7, 6, 10, 2, -8 }, +/* 0xA5 */ { 2422, 10, 14, 11, 1, -13 }, +/* 0xA6 */ { 2440, 2, 17, 5, 2, -12 }, +/* 0xA7 */ { 2445, 9, 17, 10, 1, -12 }, +/* 0xA8 */ { 2465, 9, 15, 12, 1, -14 }, +/* 0xA9 */ { 2482, 14, 13, 14, 1, -12 }, +/* 0xAA */ { 2505, 11, 13, 13, 1, -12 }, +/* 0xAB */ { 2523, 7, 6, 9, 1, -7 }, +/* 0xAC */ { 2529, 9, 5, 11, 2, -5 }, +/* 0xAD */ { 2535, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 2535, 14, 13, 14, 1, -12 }, +/* 0xAF */ { 2558, 6, 15, 5, 0, -14 }, +/* 0xB0 */ { 2570, 5, 5, 11, 3, -11 }, +/* 0xB1 */ { 2574, 9, 11, 11, 1, -10 }, +/* 0xB2 */ { 2587, 2, 13, 4, 1, -12 }, +/* 0xB3 */ { 2591, 2, 13, 4, 1, -12 }, +/* 0xB4 */ { 2595, 6, 12, 7, 1, -11 }, +/* 0xB5 */ { 2604, 9, 13, 10, 1, -9 }, +/* 0xB6 */ { 2619, 8, 16, 10, 2, -12 }, +/* 0xB7 */ { 2635, 3, 1, 5, 1, -4 }, +/* 0xB8 */ { 2636, 8, 12, 10, 1, -11 }, +/* 0xB9 */ { 2648, 15, 13, 17, 1, -12 }, +/* 0xBA */ { 2673, 8, 10, 9, 1, -9 }, +/* 0xBB */ { 2683, 7, 6, 9, 1, -7 }, +/* 0xBC */ { 2689, 4, 17, 4, 0, -12 }, +/* 0xBD */ { 2698, 10, 13, 12, 1, -12 }, +/* 0xBE */ { 2715, 8, 10, 9, 1, -9 }, +/* 0xBF */ { 2725, 6, 12, 5, -1, -11 }, +/* 0xC0 */ { 2734, 12, 13, 12, 0, -12 }, +/* 0xC1 */ { 2754, 11, 13, 12, 1, -12 }, +/* 0xC2 */ { 2772, 11, 13, 12, 1, -12 }, +/* 0xC3 */ { 2790, 8, 13, 8, 1, -12 }, +/* 0xC4 */ { 2803, 14, 16, 15, 1, -12 }, +/* 0xC5 */ { 2831, 9, 13, 12, 1, -12 }, +/* 0xC6 */ { 2846, 16, 13, 16, 0, -12 }, +/* 0xC7 */ { 2872, 10, 13, 12, 1, -12 }, +/* 0xC8 */ { 2889, 11, 13, 13, 1, -12 }, +/* 0xC9 */ { 2907, 11, 16, 13, 1, -15 }, +/* 0xCA */ { 2929, 10, 13, 11, 1, -12 }, +/* 0xCB */ { 2946, 10, 13, 12, 1, -12 }, +/* 0xCC */ { 2963, 13, 13, 15, 1, -12 }, +/* 0xCD */ { 2985, 11, 13, 13, 1, -12 }, +/* 0xCE */ { 3003, 13, 13, 14, 1, -12 }, +/* 0xCF */ { 3025, 11, 13, 13, 1, -12 }, +/* 0xD0 */ { 3043, 10, 13, 12, 1, -12 }, +/* 0xD1 */ { 3060, 11, 13, 13, 1, -12 }, +/* 0xD2 */ { 3078, 9, 13, 11, 1, -12 }, +/* 0xD3 */ { 3093, 10, 13, 11, 1, -12 }, +/* 0xD4 */ { 3110, 14, 13, 15, 1, -12 }, +/* 0xD5 */ { 3133, 10, 13, 12, 1, -12 }, +/* 0xD6 */ { 3150, 13, 15, 13, 1, -12 }, +/* 0xD7 */ { 3175, 9, 13, 11, 1, -12 }, +/* 0xD8 */ { 3190, 13, 13, 15, 1, -12 }, +/* 0xD9 */ { 3212, 15, 15, 15, 1, -12 }, +/* 0xDA */ { 3241, 13, 13, 15, 2, -12 }, +/* 0xDB */ { 3263, 14, 13, 16, 1, -12 }, +/* 0xDC */ { 3286, 11, 13, 12, 1, -12 }, +/* 0xDD */ { 3304, 11, 13, 13, 1, -12 }, +/* 0xDE */ { 3322, 17, 13, 18, 1, -12 }, +/* 0xDF */ { 3350, 10, 13, 12, 1, -12 }, +/* 0xE0 */ { 3367, 9, 10, 10, 1, -9 }, +/* 0xE1 */ { 3379, 8, 14, 10, 1, -13 }, +/* 0xE2 */ { 3393, 7, 10, 9, 1, -9 }, +/* 0xE3 */ { 3402, 5, 10, 7, 1, -9 }, +/* 0xE4 */ { 3409, 11, 12, 10, 0, -9 }, +/* 0xE5 */ { 3426, 8, 10, 10, 1, -9 }, +/* 0xE6 */ { 3436, 12, 10, 14, 1, -9 }, +/* 0xE7 */ { 3451, 7, 10, 9, 1, -9 }, +/* 0xE8 */ { 3460, 8, 10, 10, 1, -9 }, +/* 0xE9 */ { 3470, 8, 12, 10, 1, -11 }, +/* 0xEA */ { 3482, 7, 10, 9, 1, -9 }, +/* 0xEB */ { 3491, 7, 10, 8, 0, -9 }, +/* 0xEC */ { 3500, 9, 10, 11, 1, -9 }, +/* 0xED */ { 3512, 8, 10, 10, 1, -9 }, +/* 0xEE */ { 3522, 8, 10, 10, 1, -9 }, +/* 0xEF */ { 3532, 8, 10, 10, 1, -9 }, +/* 0xF0 */ { 3542, 9, 13, 10, 1, -9 }, +/* 0xF1 */ { 3557, 8, 10, 9, 1, -9 }, +/* 0xF2 */ { 3567, 6, 10, 7, 1, -9 }, +/* 0xF3 */ { 3575, 8, 14, 9, 0, -9 }, +/* 0xF4 */ { 3589, 14, 15, 15, 1, -11 }, +/* 0xF5 */ { 3616, 7, 10, 9, 1, -9 }, +/* 0xF6 */ { 3625, 10, 12, 10, 1, -9 }, +/* 0xF7 */ { 3640, 7, 10, 9, 1, -9 }, +/* 0xF8 */ { 3649, 10, 10, 12, 1, -9 }, +/* 0xF9 */ { 3662, 12, 12, 13, 1, -9 }, +/* 0xFA */ { 3680, 9, 10, 12, 2, -9 }, +/* 0xFB */ { 3692, 10, 10, 12, 1, -9 }, +/* 0xFC */ { 3705, 8, 10, 9, 1, -9 }, +/* 0xFD */ { 3715, 8, 10, 9, 1, -9 }, +/* 0xFE */ { 3725, 12, 10, 13, 1, -9 }, +/* 0xFF */ { 3740, 8, 10, 10, 1, -9 }, }; -const GFXfont FreeSans9pt_Win1251 PROGMEM = {(uint8_t *)FreeSans9pt_Win1251Bitmaps, (GFXglyph *)FreeSans9pt_Win1251Glyphs, 0x20, - 0xFF, 21}; +const GFXfont FreeSans9pt_Win1251 PROGMEM = { +(uint8_t*)FreeSans9pt_Win1251Bitmaps, +(GFXglyph*)FreeSans9pt_Win1251Glyphs, +0x01, 0xFF, 21 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h index 20f2ddc2f..8bc656632 100644 --- a/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h @@ -1,494 +1,527 @@ +// trunk-ignore-all(clang-format) #pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1252 +*/ const uint8_t FreeSans9pt_Win1252Bitmaps[] PROGMEM = { - /* ' ' 0x20 */ - 0xFF, 0xFF, 0xF0, 0xC0, /* '!' 0x21 */ - 0xDE, 0xF7, 0x20, /* '"' 0x22 */ - 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '#' 0x23 */ - 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '$' 0x24 */ - 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, - 0x63, 0x04, 0x77, 0x08, 0x3C, /* '%' 0x25 */ - 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* '&' 0x26 */ - 0xFE, /* ''' 0x27 */ - 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* '(' 0x28 */ - 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* ')' 0x29 */ - 0x25, 0x7E, 0xA5, 0x00, /* '*' 0x2A */ - 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* '+' 0x2B */ - 0xD6, /* ',' 0x2C */ - 0xF0, /* '-' 0x2D */ - 0xC0, /* '.' 0x2E */ - 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '/' 0x2F */ - 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '0' 0x30 */ - 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '1' 0x31 */ - 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '2' 0x32 */ - 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '3' 0x33 */ - 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '4' 0x34 */ - 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '5' 0x35 */ - 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ - 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '7' 0x37 */ - 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '8' 0x38 */ - 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* '9' 0x39 */ - 0xC0, 0x00, 0x30, /* ':' 0x3A */ - 0xC0, 0x00, 0x00, 0x64, 0xA0, /* ';' 0x3B */ - 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '<' 0x3C */ - 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '=' 0x3D */ - 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '>' 0x3E */ - 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '?' 0x3F */ - 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, - 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* '@' 0x40 */ - 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, - 0x30, /* 'A' 0x41 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'B' 0x42 */ - 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'C' 0x43 */ - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'D' 0x44 */ - 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'E' 0x45 */ - 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'F' 0x46 */ - 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, - 0x10, /* 'G' 0x47 */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'H' 0x48 */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 'I' 0x49 */ - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'J' 0x4A */ - 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'K' 0x4B */ - 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'L' 0x4C */ - 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, - 0x80, /* 'M' 0x4D */ - 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'N' 0x4E */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, - 0x00, /* 'O' 0x4F */ - 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'P' 0x50 */ - 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, - 0x00, 0x08, /* 'Q' 0x51 */ - 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, - 0x70, /* 'R' 0x52 */ - 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'S' 0x53 */ - 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'T' 0x54 */ - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'U' 0x55 */ - 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'V' 0x56 */ - 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, - 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'W' 0x57 */ - 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'X' 0x58 */ - 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, - 0x00, /* 'Y' 0x59 */ - 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* 'Z' 0x5A */ - 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '[' 0x5B */ - 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* '\' 0x5C */ - 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* ']' 0x5D */ - 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '^' 0x5E */ - 0xFF, 0xC0, /* '_' 0x5F */ - 0xC6, 0x30, /* '`' 0x60 */ - 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'a' 0x61 */ - 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'b' 0x62 */ - 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'c' 0x63 */ - 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'd' 0x64 */ - 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'e' 0x65 */ - 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'f' 0x66 */ - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'g' 0x67 */ - 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'h' 0x68 */ - 0xC3, 0xFF, 0xFF, 0xC0, /* 'i' 0x69 */ - 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'j' 0x6A */ - 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'k' 0x6B */ - 0xFF, 0xFF, 0xFF, 0xC0, /* 'l' 0x6C */ - 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'm' 0x6D */ - 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'n' 0x6E */ - 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'o' 0x6F */ - 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'p' 0x70 */ - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'q' 0x71 */ - 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 'r' 0x72 */ - 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 's' 0x73 */ - 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 't' 0x74 */ - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'u' 0x75 */ - 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'v' 0x76 */ - 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'w' 0x77 */ - 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'x' 0x78 */ - 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'y' 0x79 */ - 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* 'z' 0x7A */ - 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '{' 0x7B */ - 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '|' 0x7C */ - 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '}' 0x7D */ - 0x61, 0x24, 0x38, /* '~' 0x7E */ - 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, - 0xFF, 0xFC, /* 0x7F */ - 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x80 */ - /* 0x81 */ - 0xDC, /* 0x82 */ - 0x19, 0x8C, 0xF3, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0xE0, /* 0x83 */ - 0xDA, 0x76, /* 0x84 */ - 0xCC, 0xC0, /* 0x85 */ - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x86 */ - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ - 0x72, 0xA2, /* 0x88 */ - 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, - 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x89 */ - 0x1B, 0x03, 0x80, 0x00, 0xFC, 0x61, 0xB0, 0x3C, 0x0F, 0x00, 0x78, 0x07, 0xC0, 0x38, 0x03, 0xC0, 0xF0, 0x36, 0x18, - 0xFC, /* 0x8A */ - 0x69, /* 0x8B */ - 0x1E, 0xFE, 0x43, 0x81, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x3F, 0xE0, 0x60, 0xC0, 0xC1, 0x81, 0x81, 0x83, 0x01, - 0x8E, 0x01, 0xEF, 0xE0, /* 0x8C */ - /* 0x8D */ - 0x1B, 0x03, 0x80, 0x03, 0xFF, 0x01, 0x80, 0xC0, 0x30, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x18, 0x0C, 0x03, - 0xFF, /* 0x8E */ - /* 0x8F */ - /* 0x90 */ - 0x6B, /* 0x91 */ - 0xD6, /* 0x92 */ - 0x4C, 0xA5, 0xB0, /* 0x93 */ - 0xDA, 0x53, 0x20, /* 0x94 */ - 0x6F, 0xFF, 0x60, /* 0x95 */ - 0xFE, /* 0x96 */ - 0xFF, 0xFF, /* 0x97 */ - 0x4D, 0xC0, /* 0x98 */ - 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, - 0x33, 0x30, /* 0x99 */ - 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9A */ - 0x96, /* 0x9B */ - 0x3C, 0xF8, 0xCF, 0x1B, 0x0C, 0x1E, 0x18, 0x3C, 0x3F, 0xF8, 0x60, 0x30, 0xC0, 0x61, 0x83, 0x67, 0x8C, 0x79, 0xF0, /* 0x9C */ - /* 0x9D */ - 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0x9E */ - 0x19, 0x80, 0x00, 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, - 0x60, /* 0x9F */ - /* 0xA0 */ - 0xCF, 0xFF, 0xFF, 0xC0, /* 0xA1 */ - 0x08, 0x04, 0x0F, 0x8D, 0x6C, 0x9E, 0x43, 0x21, 0x90, 0xC8, 0x64, 0xDA, 0xC7, 0xC0, 0x80, 0x40, /* 0xA2 */ - 0x1F, 0x0C, 0x66, 0x0D, 0x83, 0x60, 0x0C, 0x0F, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x01, 0xF1, 0x43, 0xC0, /* 0xA3 */ - 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA4 */ - 0xC3, 0x42, 0x42, 0x24, 0x24, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x18, /* 0xA5 */ - 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA6 */ - 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, - 0x00, /* 0xA7 */ - 0xCC, /* 0xA8 */ - 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, - 0x0F, 0xC0, /* 0xA9 */ - 0x74, 0x8D, 0xA9, 0x7C, 0x1F, /* 0xAA */ - 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAB */ - 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAC */ - /* 0xAD */ - 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, - 0x0F, 0xC0, /* 0xAE */ - 0xF8, /* 0xAF */ - 0x74, 0x63, 0x17, 0x00, /* 0xB0 */ - 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB1 */ - 0x7B, 0x30, 0xC3, 0x11, 0x84, 0x3F, /* 0xB2 */ - 0x7D, 0x8C, 0x18, 0xC0, 0x60, 0xF1, 0xBE, /* 0xB3 */ - 0x36, 0xC0, /* 0xB4 */ - 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB5 */ - 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB6 */ - 0xE0, /* 0xB7 */ - 0x21, 0xC7, 0xE0, /* 0xB8 */ - 0x3D, 0xB6, 0xD8, /* 0xB9 */ - 0x74, 0x63, 0x18, 0xB8, 0x1F, /* 0xBA */ - 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBB */ - 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x84, 0x06, 0x21, 0x80, 0x86, 0x04, 0x78, 0x32, 0x60, 0x87, 0xC4, 0x06, - 0x10, 0x18, /* 0xBC */ - 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x8D, 0xE6, 0x2C, 0xC1, 0x03, 0x0C, 0x0C, 0x20, 0x41, 0x86, 0x0C, 0x30, - 0x20, 0xFC, /* 0xBD */ - 0x78, 0x11, 0x98, 0x40, 0x31, 0x00, 0x82, 0x00, 0xC8, 0x01, 0x90, 0x33, 0x43, 0x3D, 0x06, 0x02, 0x3C, 0x08, 0x98, 0x10, 0xF8, - 0x40, 0x61, 0x00, 0xC0, /* 0xBE */ - 0x0C, 0x00, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0xE0, 0xC0, 0xC0, 0x60, 0xF0, 0x6C, 0x63, 0xE0, /* 0xBF */ - 0x18, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC0 */ - 0x06, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC1 */ - 0x0C, 0x04, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC2 */ - 0x19, 0x09, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC3 */ - 0x33, 0x00, 0x00, 0xC0, 0x78, 0x1E, 0x04, 0x83, 0x30, 0xCC, 0x33, 0x1F, 0xE6, 0x19, 0x02, 0xC0, 0xF0, 0x30, /* 0xC4 */ - 0x0C, 0x04, 0x81, 0x20, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC5 */ - 0x07, 0xFF, 0x04, 0xC0, 0x0C, 0xC0, 0x08, 0xC0, 0x18, 0xC0, 0x18, 0xC0, 0x30, 0xFF, 0x30, 0xC0, 0x3F, 0xC0, 0x60, 0xC0, 0x60, - 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 0xC6 */ - 0x1F, 0x06, 0x19, 0x83, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0xE1, 0xF0, 0x08, 0x01, 0xC0, - 0x18, 0x0E, 0x00, /* 0xC7 */ - 0x18, 0x06, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xC8 */ - 0x0C, 0x0C, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xC9 */ - 0x1C, 0x1B, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCA */ - 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0xFE, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCB */ - 0xCC, 0x36, 0xDB, 0x6D, 0xB6, 0xD8, /* 0xCC */ - 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xC0, /* 0xCD */ - 0x76, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* 0xCE */ - 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xCF */ - 0x7F, 0x0C, 0x31, 0x83, 0x30, 0x36, 0x06, 0xC0, 0xFE, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x30, 0xE7, 0xF0, /* 0xD0 */ - 0x19, 0x02, 0xC3, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, - 0xC0, /* 0xD1 */ - 0x0C, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD2 */ - 0x03, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD3 */ - 0x0F, 0x01, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD4 */ - 0x1C, 0x81, 0x38, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD5 */ - 0x19, 0x81, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, - 0x0F, 0x00, /* 0xD6 */ - 0x83, 0x89, 0xA1, 0x83, 0x89, 0xA1, 0x80, /* 0xD7 */ - 0x0F, 0xD9, 0x83, 0x18, 0x1C, 0xC1, 0xEC, 0x19, 0xE0, 0x8F, 0x08, 0x78, 0x83, 0xC8, 0x1B, 0x81, 0x98, 0x0C, 0xE0, 0xC8, 0xF8, - 0x00, /* 0xD8 */ - 0x0C, 0x00, 0xC3, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xD9 */ - 0x06, 0x01, 0x83, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xDA */ - 0x0E, 0x03, 0x63, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xDB */ - 0x1B, 0x00, 0x03, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, - 0x00, /* 0xDC */ - 0x03, 0x0C, 0x63, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x1D, 0x80, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, - 0x60, /* 0xDD */ - 0xC0, 0x30, 0x0F, 0xF3, 0x06, 0xC0, 0xF0, 0x3C, 0x0F, 0x06, 0xFF, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 0xDE */ - 0x3C, 0x33, 0x30, 0xD8, 0x6C, 0x36, 0x33, 0x39, 0x86, 0xC1, 0xE0, 0xF0, 0x78, 0x6D, 0xE0, /* 0xDF */ - 0x60, 0x18, 0x06, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE0 */ - 0x0C, 0x04, 0x04, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE1 */ - 0x10, 0x14, 0x1B, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE2 */ - 0x24, 0x2E, 0x00, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE3 */ - 0x66, 0x00, 0x1F, 0x9C, 0x6C, 0x30, 0x18, 0x3C, 0xF6, 0xC3, 0x61, 0xB1, 0xCF, 0x70, /* 0xE4 */ - 0x1C, 0x1B, 0x0D, 0x83, 0x87, 0xE7, 0x1B, 0x0C, 0x06, 0x0F, 0x3D, 0xB0, 0xD8, 0x6C, 0x73, 0xDC, /* 0xE5 */ - 0x7E, 0xF9, 0xC7, 0x1B, 0x0C, 0x18, 0x18, 0x33, 0xFF, 0xFC, 0x60, 0x30, 0xC0, 0x61, 0x83, 0xC7, 0x8C, 0xF1, 0xF0, /* 0xE6 */ - 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x10, 0x1C, 0x0C, 0x38, /* 0xE7 */ - 0x60, 0x30, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE8 */ - 0x0C, 0x08, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE9 */ - 0x10, 0x28, 0x6C, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEA */ - 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEB */ - 0xCC, 0xB6, 0xDB, 0x6D, 0xB6, /* 0xEC */ - 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, /* 0xED */ - 0x6E, 0x96, 0x66, 0x66, 0x66, 0x66, 0x60, /* 0xEE */ - 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xEF */ - 0x34, 0x0C, 0x16, 0x03, 0x3F, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF0 */ - 0x24, 0x5C, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF1 */ - 0x30, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF2 */ - 0x0C, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF3 */ - 0x18, 0x24, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF4 */ - 0x34, 0x2C, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF5 */ - 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF6 */ - 0x18, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x30, /* 0xF7 */ - 0x3D, 0x66, 0xC7, 0xCB, 0xCB, 0xD3, 0xD3, 0xE3, 0x66, 0xBC, /* 0xF8 */ - 0x60, 0x30, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xF9 */ - 0x06, 0x0C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFA */ - 0x3C, 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFB */ - 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFC */ - 0x06, 0x04, 0x08, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xFD */ - 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE6, 0x03, 0x01, 0x80, /* 0xFE */ - 0x33, 0x00, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xFF */ +/* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, +/* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, +/* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, +/* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, +/* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, +/* 0x0A */ +/* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, +/* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, +/* 0x0D */ +/* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, +/* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, +/* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, +/* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, +/* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, +/* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, +/* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, +/* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, +/* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, +/* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, +/* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, +/* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, +/* '"' 0x22 */ 0xDE, 0xF7, 0x20, +/* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, +/* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, +/* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, +/* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, +/* ''' 0x27 */ 0xFE, +/* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, +/* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, +/* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, +/* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, +/* ',' 0x2C */ 0xD6, +/* '-' 0x2D */ 0xF0, +/* '.' 0x2E */ 0xC0, +/* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, +/* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, +/* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, +/* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, +/* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, +/* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, +/* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, +/* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, +/* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, +/* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, +/* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, +/* ':' 0x3A */ 0xC0, 0x00, 0x30, +/* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, +/* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, +/* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, +/* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, +/* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, +/* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, +/* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, +/* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, +/* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, +/* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, +/* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, +/* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, +/* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, +/* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, +/* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, +/* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, +/* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, +/* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, +/* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, +/* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, +/* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, +/* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, +/* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, +/* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, +/* '_' 0x5F */ 0xFF, 0xC0, +/* '`' 0x60 */ 0xC6, 0x30, +/* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, +/* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, +/* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, +/* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, +/* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, +/* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, +/* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, +/* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, +/* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, +/* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, +/* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, +/* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, +/* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, +/* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, +/* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, +/* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, +/* '~' 0x7E */ 0x61, 0x24, 0x38, +/* 0x7F */ +/* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, +/* 0x81 */ +/* 0x82 */ 0xDC, +/* 0x83 */ 0x19, 0x8C, 0xF3, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0xE0, +/* 0x84 */ 0xDA, 0x76, +/* 0x85 */ 0xCC, 0xC0, +/* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, +/* 0x88 */ 0x72, 0xA2, +/* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, +/* 0x8A */ 0x1B, 0x03, 0x80, 0x00, 0xFC, 0x61, 0xB0, 0x3C, 0x0F, 0x00, 0x78, 0x07, 0xC0, 0x38, 0x03, 0xC0, 0xF0, 0x36, 0x18, 0xFC, +/* 0x8B */ 0x69, +/* 0x8C */ 0x1E, 0xFE, 0x43, 0x81, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x3F, 0xE0, 0x60, 0xC0, 0xC1, 0x81, 0x81, 0x83, 0x01, 0x8E, 0x01, 0xEF, 0xE0, +/* 0x8D */ +/* 0x8E */ 0x1B, 0x03, 0x80, 0x03, 0xFF, 0x01, 0x80, 0xC0, 0x30, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0xFF, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0x6B, +/* 0x92 */ 0xD6, +/* 0x93 */ 0x4C, 0xA5, 0xB0, +/* 0x94 */ 0xDA, 0x53, 0x20, +/* 0x95 */ 0x6F, 0xFF, 0x60, +/* 0x96 */ 0xFE, +/* 0x97 */ 0xFF, 0xFF, +/* 0x98 */ 0x4D, 0xC0, +/* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, +/* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 0x9B */ 0x96, +/* 0x9C */ 0x3C, 0xF8, 0xCF, 0x1B, 0x0C, 0x1E, 0x18, 0x3C, 0x3F, 0xF8, 0x60, 0x30, 0xC0, 0x61, 0x83, 0x67, 0x8C, 0x79, 0xF0, +/* 0x9D */ +/* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, +/* 0x9F */ 0x19, 0x80, 0x00, 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xA0 */ +/* 0xA1 */ 0xCF, 0xFF, 0xFF, 0xC0, +/* 0xA2 */ 0x08, 0x04, 0x0F, 0x8D, 0x6C, 0x9E, 0x43, 0x21, 0x90, 0xC8, 0x64, 0xDA, 0xC7, 0xC0, 0x80, 0x40, +/* 0xA3 */ 0x1F, 0x0C, 0x66, 0x0D, 0x83, 0x60, 0x0C, 0x0F, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x01, 0xF1, 0x43, 0xC0, +/* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, +/* 0xA5 */ 0xC3, 0x42, 0x42, 0x24, 0x24, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x18, +/* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, +/* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, +/* 0xA8 */ 0xCC, +/* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAA */ 0x74, 0x8D, 0xA9, 0x7C, 0x1F, +/* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, +/* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xAD */ +/* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAF */ 0xF8, +/* 0xB0 */ 0x74, 0x63, 0x17, 0x00, +/* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, +/* 0xB2 */ 0x7B, 0x30, 0xC3, 0x11, 0x84, 0x3F, +/* 0xB3 */ 0x7D, 0x8C, 0x18, 0xC0, 0x60, 0xF1, 0xBE, +/* 0xB4 */ 0x36, 0xC0, +/* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, +/* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, +/* 0xB7 */ 0xE0, +/* 0xB8 */ 0x21, 0xC7, 0xE0, +/* 0xB9 */ 0x3D, 0xB6, 0xD8, +/* 0xBA */ 0x74, 0x63, 0x18, 0xB8, 0x1F, +/* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, +/* 0xBC */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x84, 0x06, 0x21, 0x80, 0x86, 0x04, 0x78, 0x32, 0x60, 0x87, 0xC4, 0x06, 0x10, 0x18, +/* 0xBD */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x8D, 0xE6, 0x2C, 0xC1, 0x03, 0x0C, 0x0C, 0x20, 0x41, 0x86, 0x0C, 0x30, 0x20, 0xFC, +/* 0xBE */ 0x78, 0x11, 0x98, 0x40, 0x31, 0x00, 0x82, 0x00, 0xC8, 0x01, 0x90, 0x33, 0x43, 0x3D, 0x06, 0x02, 0x3C, 0x08, 0x98, 0x10, 0xF8, 0x40, 0x61, 0x00, 0xC0, +/* 0xBF */ 0x0C, 0x00, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0xE0, 0xC0, 0xC0, 0x60, 0xF0, 0x6C, 0x63, 0xE0, +/* 0xC0 */ 0x18, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC1 */ 0x06, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC2 */ 0x0C, 0x04, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC3 */ 0x19, 0x09, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC4 */ 0x33, 0x00, 0x00, 0xC0, 0x78, 0x1E, 0x04, 0x83, 0x30, 0xCC, 0x33, 0x1F, 0xE6, 0x19, 0x02, 0xC0, 0xF0, 0x30, +/* 0xC5 */ 0x0C, 0x04, 0x81, 0x20, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, +/* 0xC6 */ 0x07, 0xFF, 0x04, 0xC0, 0x0C, 0xC0, 0x08, 0xC0, 0x18, 0xC0, 0x18, 0xC0, 0x30, 0xFF, 0x30, 0xC0, 0x3F, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 0xC7 */ 0x1F, 0x06, 0x19, 0x83, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0xE1, 0xF0, 0x08, 0x01, 0xC0, 0x18, 0x0E, 0x00, +/* 0xC8 */ 0x18, 0x06, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, +/* 0xC9 */ 0x0C, 0x0C, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, +/* 0xCA */ 0x1C, 0x1B, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, +/* 0xCB */ 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0xFE, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, +/* 0xCC */ 0xCC, 0x36, 0xDB, 0x6D, 0xB6, 0xD8, +/* 0xCD */ 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xC0, +/* 0xCE */ 0x76, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* 0xCF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xD0 */ 0x7F, 0x0C, 0x31, 0x83, 0x30, 0x36, 0x06, 0xC0, 0xFE, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x30, 0xE7, 0xF0, +/* 0xD1 */ 0x19, 0x02, 0xC3, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, 0xC0, +/* 0xD2 */ 0x0C, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD3 */ 0x03, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD4 */ 0x0F, 0x01, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD5 */ 0x1C, 0x81, 0x38, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD6 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, +/* 0xD7 */ 0x83, 0x89, 0xA1, 0x83, 0x89, 0xA1, 0x80, +/* 0xD8 */ 0x0F, 0xD9, 0x83, 0x18, 0x1C, 0xC1, 0xEC, 0x19, 0xE0, 0x8F, 0x08, 0x78, 0x83, 0xC8, 0x1B, 0x81, 0x98, 0x0C, 0xE0, 0xC8, 0xF8, 0x00, +/* 0xD9 */ 0x0C, 0x00, 0xC3, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDA */ 0x06, 0x01, 0x83, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDB */ 0x0E, 0x03, 0x63, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDC */ 0x1B, 0x00, 0x03, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, +/* 0xDD */ 0x03, 0x0C, 0x63, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x1D, 0x80, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xDE */ 0xC0, 0x30, 0x0F, 0xF3, 0x06, 0xC0, 0xF0, 0x3C, 0x0F, 0x06, 0xFF, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 0xDF */ 0x3C, 0x33, 0x30, 0xD8, 0x6C, 0x36, 0x33, 0x39, 0x86, 0xC1, 0xE0, 0xF0, 0x78, 0x6D, 0xE0, +/* 0xE0 */ 0x60, 0x18, 0x06, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE1 */ 0x0C, 0x04, 0x04, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE2 */ 0x10, 0x14, 0x1B, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE3 */ 0x24, 0x2E, 0x00, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, +/* 0xE4 */ 0x66, 0x00, 0x1F, 0x9C, 0x6C, 0x30, 0x18, 0x3C, 0xF6, 0xC3, 0x61, 0xB1, 0xCF, 0x70, +/* 0xE5 */ 0x1C, 0x1B, 0x0D, 0x83, 0x87, 0xE7, 0x1B, 0x0C, 0x06, 0x0F, 0x3D, 0xB0, 0xD8, 0x6C, 0x73, 0xDC, +/* 0xE6 */ 0x7E, 0xF9, 0xC7, 0x1B, 0x0C, 0x18, 0x18, 0x33, 0xFF, 0xFC, 0x60, 0x30, 0xC0, 0x61, 0x83, 0xC7, 0x8C, 0xF1, 0xF0, +/* 0xE7 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x10, 0x1C, 0x0C, 0x38, +/* 0xE8 */ 0x60, 0x30, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xE9 */ 0x0C, 0x08, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xEA */ 0x10, 0x28, 0x6C, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xEB */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 0xEC */ 0xCC, 0xB6, 0xDB, 0x6D, 0xB6, +/* 0xED */ 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, +/* 0xEE */ 0x6E, 0x96, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 0xEF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xF0 */ 0x34, 0x0C, 0x16, 0x03, 0x3F, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF1 */ 0x24, 0x5C, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 0xF2 */ 0x30, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF3 */ 0x0C, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF4 */ 0x18, 0x24, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF5 */ 0x34, 0x2C, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF6 */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 0xF7 */ 0x18, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x30, +/* 0xF8 */ 0x3D, 0x66, 0xC7, 0xCB, 0xCB, 0xD3, 0xD3, 0xE3, 0x66, 0xBC, +/* 0xF9 */ 0x60, 0x30, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFA */ 0x06, 0x0C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFB */ 0x3C, 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFC */ 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 0xFD */ 0x06, 0x04, 0x08, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 0xFE */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE6, 0x03, 0x01, 0x80, +/* 0xFF */ 0x33, 0x00, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, }; const GFXglyph FreeSans9pt_Win1252Glyphs[] PROGMEM = { - /* ' ' 0x20 */ {0, 0, 0, 5, 0, 0}, - /* '!' 0x21 */ {0, 2, 13, 6, 2, -12}, - /* '"' 0x22 */ {4, 5, 4, 6, 1, -12}, - /* '#' 0x23 */ {7, 10, 12, 10, 0, -11}, - /* '$' 0x24 */ {22, 9, 16, 10, 1, -13}, - /* '%' 0x25 */ {40, 16, 13, 16, 1, -12}, - /* '&' 0x26 */ {66, 10, 13, 12, 1, -12}, - /* ''' 0x27 */ {83, 2, 4, 4, 1, -12}, - /* '(' 0x28 */ {84, 4, 17, 6, 1, -12}, - /* ')' 0x29 */ {93, 4, 17, 6, 1, -12}, - /* '*' 0x2A */ {102, 5, 5, 7, 1, -12}, - /* '+' 0x2B */ {106, 6, 8, 11, 3, -7}, - /* ',' 0x2C */ {112, 2, 4, 5, 2, 0}, - /* '-' 0x2D */ {113, 4, 1, 6, 1, -4}, - /* '.' 0x2E */ {114, 2, 1, 5, 1, 0}, - /* '/' 0x2F */ {115, 5, 13, 5, 0, -12}, - /* '0' 0x30 */ {124, 8, 13, 10, 1, -12}, - /* '1' 0x31 */ {137, 4, 13, 10, 3, -12}, - /* '2' 0x32 */ {144, 9, 13, 10, 1, -12}, - /* '3' 0x33 */ {159, 8, 13, 10, 1, -12}, - /* '4' 0x34 */ {172, 7, 13, 10, 2, -12}, - /* '5' 0x35 */ {184, 9, 13, 10, 1, -12}, - /* '6' 0x36 */ {199, 9, 13, 10, 1, -12}, - /* '7' 0x37 */ {214, 8, 13, 10, 0, -12}, - /* '8' 0x38 */ {227, 9, 13, 10, 1, -12}, - /* '9' 0x39 */ {242, 8, 13, 10, 1, -12}, - /* ':' 0x3A */ {255, 2, 10, 5, 1, -9}, - /* ';' 0x3B */ {258, 3, 12, 5, 1, -8}, - /* '<' 0x3C */ {263, 9, 9, 11, 1, -8}, - /* '=' 0x3D */ {274, 9, 4, 11, 1, -5}, - /* '>' 0x3E */ {279, 9, 8, 11, 1, -7}, - /* '?' 0x3F */ {288, 9, 13, 10, 1, -12}, - /* '@' 0x40 */ {303, 17, 16, 18, 1, -12}, - /* 'A' 0x41 */ {337, 12, 13, 12, 0, -12}, - /* 'B' 0x42 */ {357, 11, 13, 12, 1, -12}, - /* 'C' 0x43 */ {375, 11, 13, 13, 1, -12}, - /* 'D' 0x44 */ {393, 11, 13, 13, 1, -12}, - /* 'E' 0x45 */ {411, 9, 13, 11, 1, -12}, - /* 'F' 0x46 */ {426, 8, 13, 11, 1, -12}, - /* 'G' 0x47 */ {439, 12, 13, 14, 1, -12}, - /* 'H' 0x48 */ {459, 11, 13, 13, 1, -12}, - /* 'I' 0x49 */ {477, 2, 13, 5, 2, -12}, - /* 'J' 0x4A */ {481, 7, 13, 10, 1, -12}, - /* 'K' 0x4B */ {493, 10, 13, 12, 1, -12}, - /* 'L' 0x4C */ {510, 8, 13, 10, 1, -12}, - /* 'M' 0x4D */ {523, 13, 13, 15, 1, -12}, - /* 'N' 0x4E */ {545, 11, 13, 13, 1, -12}, - /* 'O' 0x4F */ {563, 13, 13, 14, 1, -12}, - /* 'P' 0x50 */ {585, 10, 13, 12, 1, -12}, - /* 'Q' 0x51 */ {602, 13, 14, 14, 1, -12}, - /* 'R' 0x52 */ {625, 12, 13, 13, 1, -12}, - /* 'S' 0x53 */ {645, 10, 13, 12, 1, -12}, - /* 'T' 0x54 */ {662, 9, 13, 11, 1, -12}, - /* 'U' 0x55 */ {677, 11, 13, 13, 1, -12}, - /* 'V' 0x56 */ {695, 11, 13, 11, 0, -12}, - /* 'W' 0x57 */ {713, 16, 13, 17, 0, -12}, - /* 'X' 0x58 */ {739, 10, 13, 12, 1, -12}, - /* 'Y' 0x59 */ {756, 12, 13, 12, 0, -12}, - /* 'Z' 0x5A */ {776, 10, 13, 11, 1, -12}, - /* '[' 0x5B */ {793, 3, 17, 5, 1, -12}, - /* '\' 0x5C */ {800, 5, 13, 5, 0, -12}, - /* ']' 0x5D */ {809, 3, 17, 5, 0, -12}, - /* '^' 0x5E */ {816, 7, 7, 8, 1, -12}, - /* '_' 0x5F */ {823, 10, 1, 10, 0, 3}, - /* '`' 0x60 */ {825, 4, 3, 5, 0, -12}, - /* 'a' 0x61 */ {827, 9, 10, 10, 1, -9}, - /* 'b' 0x62 */ {839, 9, 13, 10, 1, -12}, - /* 'c' 0x63 */ {854, 8, 10, 9, 1, -9}, - /* 'd' 0x64 */ {864, 8, 13, 10, 1, -12}, - /* 'e' 0x65 */ {877, 8, 10, 10, 1, -9}, - /* 'f' 0x66 */ {887, 4, 13, 5, 1, -12}, - /* 'g' 0x67 */ {894, 8, 14, 10, 1, -9}, - /* 'h' 0x68 */ {908, 8, 13, 10, 1, -12}, - /* 'i' 0x69 */ {921, 2, 13, 4, 1, -12}, - /* 'j' 0x6A */ {925, 4, 17, 4, 0, -12}, - /* 'k' 0x6B */ {934, 8, 13, 9, 1, -12}, - /* 'l' 0x6C */ {947, 2, 13, 4, 1, -12}, - /* 'm' 0x6D */ {951, 13, 10, 15, 1, -9}, - /* 'n' 0x6E */ {968, 8, 10, 10, 1, -9}, - /* 'o' 0x6F */ {978, 8, 10, 10, 1, -9}, - /* 'p' 0x70 */ {988, 9, 13, 10, 1, -9}, - /* 'q' 0x71 */ {1003, 8, 13, 10, 1, -9}, - /* 'r' 0x72 */ {1016, 5, 10, 6, 1, -9}, - /* 's' 0x73 */ {1023, 8, 10, 9, 1, -9}, - /* 't' 0x74 */ {1033, 4, 12, 5, 1, -11}, - /* 'u' 0x75 */ {1039, 8, 10, 10, 1, -9}, - /* 'v' 0x76 */ {1049, 9, 10, 9, 0, -9}, - /* 'w' 0x77 */ {1061, 13, 10, 13, 0, -9}, - /* 'x' 0x78 */ {1078, 7, 10, 9, 1, -9}, - /* 'y' 0x79 */ {1087, 8, 14, 9, 0, -9}, - /* 'z' 0x7A */ {1101, 7, 10, 9, 1, -9}, - /* '{' 0x7B */ {1110, 4, 17, 6, 1, -12}, - /* '|' 0x7C */ {1119, 2, 17, 4, 2, -12}, - /* '}' 0x7D */ {1124, 4, 17, 6, 1, -12}, - /* '~' 0x7E */ {1133, 7, 3, 9, 1, -7}, - /* 0x7F */ {1136, 13, 14, 15, 1, -12}, - /* 0x80 */ {1159, 10, 13, 12, 1, -12}, - /* 0x81 */ {1176, 0, 0, 8, 0, 0}, - /* 0x82 */ {1176, 2, 3, 5, 1, 0}, - /* 0x83 */ {1177, 5, 17, 5, 0, -12}, - /* 0x84 */ {1188, 5, 3, 7, 1, 0}, - /* 0x85 */ {1190, 10, 1, 12, 1, 0}, - /* 0x86 */ {1192, 8, 16, 10, 1, -12}, - /* 0x87 */ {1208, 8, 16, 10, 1, -12}, - /* 0x88 */ {1224, 5, 3, 6, 0, -12}, - /* 0x89 */ {1226, 18, 13, 18, 0, -12}, - /* 0x8A */ {1256, 10, 16, 12, 1, -15}, - /* 0x8B */ {1276, 2, 4, 4, 1, -6}, - /* 0x8C */ {1277, 15, 13, 18, 1, -12}, - /* 0x8D */ {1302, 0, 0, 8, 0, 0}, - /* 0x8E */ {1302, 10, 16, 11, 1, -15}, - /* 0x8F */ {1322, 0, 0, 8, 0, 0}, - /* 0x90 */ {1322, 0, 0, 8, 0, 0}, - /* 0x91 */ {1322, 2, 4, 4, 2, -12}, - /* 0x92 */ {1323, 2, 4, 4, 1, -12}, - /* 0x93 */ {1324, 5, 4, 7, 2, -12}, - /* 0x94 */ {1327, 5, 4, 7, 1, -12}, - /* 0x95 */ {1330, 4, 5, 7, 1, -8}, - /* 0x96 */ {1333, 7, 1, 9, 1, -4}, - /* 0x97 */ {1334, 16, 1, 18, 1, -4}, - /* 0x98 */ {1336, 5, 2, 6, 0, -12}, - /* 0x99 */ {1338, 18, 10, 18, 1, -13}, - /* 0x9A */ {1361, 8, 13, 9, 1, -12}, - /* 0x9B */ {1374, 2, 4, 5, 2, -6}, - /* 0x9C */ {1375, 15, 10, 17, 1, -9}, - /* 0x9D */ {1394, 0, 0, 8, 0, 0}, - /* 0x9E */ {1394, 7, 13, 9, 1, -12}, - /* 0x9F */ {1406, 12, 14, 12, 0, -13}, - /* 0xA0 */ {1427, 0, 0, 5, 0, 0}, - /* 0xA1 */ {1427, 2, 13, 6, 2, -8}, - /* 0xA2 */ {1431, 9, 14, 10, 1, -11}, - /* 0xA3 */ {1447, 10, 13, 10, 0, -12}, - /* 0xA4 */ {1464, 7, 6, 10, 2, -8}, - /* 0xA5 */ {1470, 8, 13, 10, 1, -12}, - /* 0xA6 */ {1483, 2, 17, 5, 2, -12}, - /* 0xA7 */ {1488, 9, 17, 10, 1, -12}, - /* 0xA8 */ {1508, 6, 1, 6, 0, -11}, - /* 0xA9 */ {1509, 14, 13, 14, 1, -12}, - /* 0xAA */ {1532, 5, 8, 7, 1, -12}, - /* 0xAB */ {1537, 7, 6, 9, 1, -7}, - /* 0xAC */ {1543, 9, 5, 11, 2, -5}, - /* 0xAD */ {1549, 0, 0, 0, 0, 0}, - /* 0xAE */ {1549, 14, 13, 14, 1, -12}, - /* 0xAF */ {1572, 5, 1, 6, 0, -12}, - /* 0xB0 */ {1573, 5, 5, 11, 3, -11}, - /* 0xB1 */ {1577, 9, 11, 11, 1, -10}, - /* 0xB2 */ {1590, 6, 8, 6, 1, -13}, - /* 0xB3 */ {1596, 7, 8, 6, 0, -13}, - /* 0xB4 */ {1603, 4, 3, 6, 2, -12}, - /* 0xB5 */ {1605, 9, 13, 10, 1, -9}, - /* 0xB6 */ {1620, 8, 16, 10, 2, -12}, - /* 0xB7 */ {1636, 3, 1, 5, 1, -4}, - /* 0xB8 */ {1637, 5, 4, 6, 1, 1}, - /* 0xB9 */ {1640, 3, 7, 6, 2, -13}, - /* 0xBA */ {1643, 5, 8, 7, 1, -12}, - /* 0xBB */ {1648, 7, 6, 9, 1, -7}, - /* 0xBC */ {1654, 14, 13, 16, 2, -12}, - /* 0xBD */ {1677, 14, 13, 16, 2, -12}, - /* 0xBE */ {1700, 15, 13, 16, 1, -12}, - /* 0xBF */ {1725, 9, 13, 10, 1, -8}, - /* 0xC0 */ {1740, 10, 14, 12, 1, -13}, - /* 0xC1 */ {1758, 10, 14, 12, 1, -13}, - /* 0xC2 */ {1776, 10, 14, 12, 1, -13}, - /* 0xC3 */ {1794, 10, 14, 12, 1, -13}, - /* 0xC4 */ {1812, 10, 14, 12, 1, -13}, - /* 0xC5 */ {1830, 10, 14, 12, 1, -13}, - /* 0xC6 */ {1848, 16, 13, 18, 1, -12}, - /* 0xC7 */ {1874, 11, 17, 13, 1, -12}, - /* 0xC8 */ {1898, 9, 14, 11, 1, -13}, - /* 0xC9 */ {1914, 9, 14, 11, 1, -13}, - /* 0xCA */ {1930, 9, 14, 11, 1, -13}, - /* 0xCB */ {1946, 9, 14, 11, 1, -13}, - /* 0xCC */ {1962, 3, 15, 5, 1, -13}, - /* 0xCD */ {1968, 3, 14, 5, 1, -13}, - /* 0xCE */ {1974, 5, 14, 5, 0, -13}, - /* 0xCF */ {1983, 6, 14, 5, 0, -13}, - /* 0xD0 */ {1994, 11, 13, 13, 1, -12}, - /* 0xD1 */ {2012, 11, 14, 13, 1, -13}, - /* 0xD2 */ {2032, 12, 15, 13, 1, -14}, - /* 0xD3 */ {2055, 12, 15, 13, 1, -14}, - /* 0xD4 */ {2078, 12, 15, 13, 1, -14}, - /* 0xD5 */ {2101, 12, 15, 13, 1, -14}, - /* 0xD6 */ {2124, 12, 15, 13, 1, -14}, - /* 0xD7 */ {2147, 7, 7, 11, 2, -7}, - /* 0xD8 */ {2154, 13, 13, 14, 1, -12}, - /* 0xD9 */ {2176, 11, 14, 13, 1, -13}, - /* 0xDA */ {2196, 11, 14, 13, 1, -13}, - /* 0xDB */ {2216, 11, 14, 13, 1, -13}, - /* 0xDC */ {2236, 11, 14, 13, 1, -13}, - /* 0xDD */ {2256, 12, 14, 12, 0, -13}, - /* 0xDE */ {2277, 10, 13, 12, 1, -12}, - /* 0xDF */ {2294, 9, 13, 11, 1, -12}, - /* 0xE0 */ {2309, 9, 13, 10, 1, -12}, - /* 0xE1 */ {2324, 9, 13, 10, 1, -12}, - /* 0xE2 */ {2339, 9, 13, 10, 1, -12}, - /* 0xE3 */ {2354, 9, 13, 10, 1, -12}, - /* 0xE4 */ {2369, 9, 12, 10, 1, -11}, - /* 0xE5 */ {2383, 9, 14, 10, 1, -13}, - /* 0xE6 */ {2399, 15, 10, 16, 1, -9}, - /* 0xE7 */ {2418, 8, 14, 9, 1, -9}, - /* 0xE8 */ {2432, 8, 13, 10, 1, -12}, - /* 0xE9 */ {2445, 8, 13, 10, 1, -12}, - /* 0xEA */ {2458, 8, 13, 10, 1, -12}, - /* 0xEB */ {2471, 8, 12, 10, 1, -11}, - /* 0xEC */ {2483, 3, 13, 4, 0, -12}, - /* 0xED */ {2488, 3, 13, 4, 1, -12}, - /* 0xEE */ {2493, 4, 13, 5, 0, -12}, - /* 0xEF */ {2500, 6, 12, 5, -1, -11}, - /* 0xF0 */ {2509, 8, 13, 10, 1, -12}, - /* 0xF1 */ {2522, 8, 13, 10, 1, -12}, - /* 0xF2 */ {2535, 8, 13, 10, 1, -12}, - /* 0xF3 */ {2548, 8, 13, 10, 1, -12}, - /* 0xF4 */ {2561, 8, 13, 10, 1, -12}, - /* 0xF5 */ {2574, 8, 13, 10, 1, -12}, - /* 0xF6 */ {2587, 8, 12, 10, 1, -11}, - /* 0xF7 */ {2599, 9, 8, 11, 1, -7}, - /* 0xF8 */ {2608, 8, 10, 10, 1, -9}, - /* 0xF9 */ {2618, 8, 13, 10, 1, -12}, - /* 0xFA */ {2631, 8, 13, 10, 1, -12}, - /* 0xFB */ {2644, 8, 13, 10, 1, -12}, - /* 0xFC */ {2657, 8, 12, 10, 1, -11}, - /* 0xFD */ {2669, 8, 17, 9, 0, -12}, - /* 0xFE */ {2686, 9, 16, 10, 1, -12}, - /* 0xFF */ {2704, 8, 16, 9, 0, -11}, +/* 0x01 */ { 0, 15, 15, 17, 1, -13 }, +/* 0x02 */ { 29, 15, 15, 17, 1, -13 }, +/* 0x03 */ { 58, 15, 16, 17, 1, -14 }, +/* 0x04 */ { 88, 15, 16, 17, 1, -14 }, +/* 0x05 */ { 118, 16, 15, 18, 1, -13 }, +/* 0x06 */ { 148, 15, 15, 17, 1, -13 }, +/* 0x07 */ { 177, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 177, 17, 16, 19, 1, -14 }, +/* 0x09 */ { 211, 17, 12, 19, 1, -12 }, +/* 0x0A */ { 237, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 237, 17, 16, 19, 1, -14 }, +/* 0x0C */ { 271, 15, 14, 17, 1, -12 }, +/* 0x0D */ { 298, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 298, 15, 16, 17, 1, -14 }, +/* 0x0F */ { 328, 15, 15, 17, 1, -13 }, +/* 0x10 */ { 357, 15, 15, 17, 1, -13 }, +/* 0x11 */ { 386, 15, 16, 17, 1, -14 }, +/* 0x12 */ { 416, 17, 17, 19, 1, -15 }, +/* 0x13 */ { 453, 15, 16, 17, 1, -14 }, +/* 0x14 */ { 483, 15, 16, 17, 1, -14 }, +/* 0x15 */ { 513, 15, 16, 17, 1, -14 }, +/* 0x16 */ { 543, 11, 16, 13, 1, -14 }, +/* 0x17 */ { 565, 15, 16, 17, 1, -14 }, +/* 0x18 */ { 595, 18, 15, 20, 1, -13 }, +/* 0x19 */ { 629, 15, 16, 17, 1, -14 }, +/* 0x1A */ { 659, 13, 14, 15, 1, -12 }, +/* 0x1B */ { 682, 17, 16, 19, 1, -14 }, +/* 0x1C */ { 716, 15, 16, 17, 1, -14 }, +/* 0x1D */ { 746, 15, 15, 17, 1, -13 }, +/* 0x1E */ { 775, 17, 16, 19, 1, -14 }, +/* 0x1F */ { 809, 11, 16, 13, 1, -14 }, +/* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, +/* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, +/* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, +/* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, +/* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, +/* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, +/* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, +/* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, +/* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, +/* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, +/* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, +/* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, +/* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, +/* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, +/* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, +/* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, +/* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, +/* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, +/* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, +/* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, +/* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, +/* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, +/* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, +/* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, +/* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, +/* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, +/* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, +/* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, +/* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, +/* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, +/* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, +/* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, +/* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, +/* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, +/* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, +/* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, +/* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, +/* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, +/* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, +/* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, +/* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, +/* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, +/* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, +/* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, +/* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, +/* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, +/* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, +/* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, +/* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, +/* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, +/* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, +/* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, +/* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, +/* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, +/* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, +/* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, +/* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, +/* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, +/* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, +/* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, +/* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, +/* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, +/* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, +/* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, +/* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, +/* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, +/* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, +/* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, +/* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, +/* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, +/* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, +/* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, +/* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, +/* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, +/* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, +/* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, +/* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, +/* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, +/* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, +/* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, +/* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, +/* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, +/* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, +/* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, +/* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, +/* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, +/* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, +/* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, +/* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, +/* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, +/* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, +/* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, +/* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, +/* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, +/* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, +/* 0x7F */ { 1967, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 1967, 10, 13, 12, 1, -12 }, +/* 0x81 */ { 1984, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 1984, 2, 3, 5, 1, 0 }, +/* 0x83 */ { 1985, 5, 17, 5, 0, -12 }, +/* 0x84 */ { 1996, 5, 3, 7, 1, 0 }, +/* 0x85 */ { 1998, 10, 1, 12, 1, 0 }, +/* 0x86 */ { 2000, 8, 16, 10, 1, -12 }, +/* 0x87 */ { 2016, 8, 16, 10, 1, -12 }, +/* 0x88 */ { 2032, 5, 3, 6, 0, -12 }, +/* 0x89 */ { 2034, 18, 13, 18, 0, -12 }, +/* 0x8A */ { 2064, 10, 16, 12, 1, -15 }, +/* 0x8B */ { 2084, 2, 4, 4, 1, -6 }, +/* 0x8C */ { 2085, 15, 13, 18, 1, -12 }, +/* 0x8D */ { 2110, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 2110, 10, 16, 11, 1, -15 }, +/* 0x8F */ { 2130, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 2130, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 2130, 2, 4, 4, 2, -12 }, +/* 0x92 */ { 2131, 2, 4, 4, 1, -12 }, +/* 0x93 */ { 2132, 5, 4, 7, 2, -12 }, +/* 0x94 */ { 2135, 5, 4, 7, 1, -12 }, +/* 0x95 */ { 2138, 4, 5, 7, 1, -8 }, +/* 0x96 */ { 2141, 7, 1, 9, 1, -4 }, +/* 0x97 */ { 2142, 16, 1, 18, 1, -4 }, +/* 0x98 */ { 2144, 5, 2, 6, 0, -12 }, +/* 0x99 */ { 2146, 18, 10, 18, 1, -13 }, +/* 0x9A */ { 2169, 8, 13, 9, 1, -12 }, +/* 0x9B */ { 2182, 2, 4, 5, 2, -6 }, +/* 0x9C */ { 2183, 15, 10, 17, 1, -9 }, +/* 0x9D */ { 2202, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 2202, 7, 13, 9, 1, -12 }, +/* 0x9F */ { 2214, 12, 14, 12, 0, -13 }, +/* 0xA0 */ { 2235, 0, 0, 5, 0, 0 }, +/* 0xA1 */ { 2235, 2, 13, 6, 2, -8 }, +/* 0xA2 */ { 2239, 9, 14, 10, 1, -11 }, +/* 0xA3 */ { 2255, 10, 13, 10, 0, -12 }, +/* 0xA4 */ { 2272, 7, 6, 10, 2, -8 }, +/* 0xA5 */ { 2278, 8, 13, 10, 1, -12 }, +/* 0xA6 */ { 2291, 2, 17, 5, 2, -12 }, +/* 0xA7 */ { 2296, 9, 17, 10, 1, -12 }, +/* 0xA8 */ { 2316, 6, 1, 6, 0, -11 }, +/* 0xA9 */ { 2317, 14, 13, 14, 1, -12 }, +/* 0xAA */ { 2340, 5, 8, 7, 1, -12 }, +/* 0xAB */ { 2345, 7, 6, 9, 1, -7 }, +/* 0xAC */ { 2351, 9, 5, 11, 2, -5 }, +/* 0xAD */ { 2357, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 2357, 14, 13, 14, 1, -12 }, +/* 0xAF */ { 2380, 5, 1, 6, 0, -12 }, +/* 0xB0 */ { 2381, 5, 5, 11, 3, -11 }, +/* 0xB1 */ { 2385, 9, 11, 11, 1, -10 }, +/* 0xB2 */ { 2398, 6, 8, 6, 1, -13 }, +/* 0xB3 */ { 2404, 7, 8, 6, 0, -13 }, +/* 0xB4 */ { 2411, 4, 3, 6, 2, -12 }, +/* 0xB5 */ { 2413, 9, 13, 10, 1, -9 }, +/* 0xB6 */ { 2428, 8, 16, 10, 2, -12 }, +/* 0xB7 */ { 2444, 3, 1, 5, 1, -4 }, +/* 0xB8 */ { 2445, 5, 4, 6, 1, 1 }, +/* 0xB9 */ { 2448, 3, 7, 6, 2, -13 }, +/* 0xBA */ { 2451, 5, 8, 7, 1, -12 }, +/* 0xBB */ { 2456, 7, 6, 9, 1, -7 }, +/* 0xBC */ { 2462, 14, 13, 16, 2, -12 }, +/* 0xBD */ { 2485, 14, 13, 16, 2, -12 }, +/* 0xBE */ { 2508, 15, 13, 16, 1, -12 }, +/* 0xBF */ { 2533, 9, 13, 10, 1, -8 }, +/* 0xC0 */ { 2548, 10, 14, 12, 1, -13 }, +/* 0xC1 */ { 2566, 10, 14, 12, 1, -13 }, +/* 0xC2 */ { 2584, 10, 14, 12, 1, -13 }, +/* 0xC3 */ { 2602, 10, 14, 12, 1, -13 }, +/* 0xC4 */ { 2620, 10, 14, 12, 1, -13 }, +/* 0xC5 */ { 2638, 10, 14, 12, 1, -13 }, +/* 0xC6 */ { 2656, 16, 13, 18, 1, -12 }, +/* 0xC7 */ { 2682, 11, 17, 13, 1, -12 }, +/* 0xC8 */ { 2706, 9, 14, 11, 1, -13 }, +/* 0xC9 */ { 2722, 9, 14, 11, 1, -13 }, +/* 0xCA */ { 2738, 9, 14, 11, 1, -13 }, +/* 0xCB */ { 2754, 9, 14, 11, 1, -13 }, +/* 0xCC */ { 2770, 3, 15, 5, 1, -13 }, +/* 0xCD */ { 2776, 3, 14, 5, 1, -13 }, +/* 0xCE */ { 2782, 5, 14, 5, 0, -13 }, +/* 0xCF */ { 2791, 6, 14, 5, 0, -13 }, +/* 0xD0 */ { 2802, 11, 13, 13, 1, -12 }, +/* 0xD1 */ { 2820, 11, 14, 13, 1, -13 }, +/* 0xD2 */ { 2840, 12, 15, 13, 1, -14 }, +/* 0xD3 */ { 2863, 12, 15, 13, 1, -14 }, +/* 0xD4 */ { 2886, 12, 15, 13, 1, -14 }, +/* 0xD5 */ { 2909, 12, 15, 13, 1, -14 }, +/* 0xD6 */ { 2932, 12, 15, 13, 1, -14 }, +/* 0xD7 */ { 2955, 7, 7, 11, 2, -7 }, +/* 0xD8 */ { 2962, 13, 13, 14, 1, -12 }, +/* 0xD9 */ { 2984, 11, 14, 13, 1, -13 }, +/* 0xDA */ { 3004, 11, 14, 13, 1, -13 }, +/* 0xDB */ { 3024, 11, 14, 13, 1, -13 }, +/* 0xDC */ { 3044, 11, 14, 13, 1, -13 }, +/* 0xDD */ { 3064, 12, 14, 12, 0, -13 }, +/* 0xDE */ { 3085, 10, 13, 12, 1, -12 }, +/* 0xDF */ { 3102, 9, 13, 11, 1, -12 }, +/* 0xE0 */ { 3117, 9, 13, 10, 1, -12 }, +/* 0xE1 */ { 3132, 9, 13, 10, 1, -12 }, +/* 0xE2 */ { 3147, 9, 13, 10, 1, -12 }, +/* 0xE3 */ { 3162, 9, 13, 10, 1, -12 }, +/* 0xE4 */ { 3177, 9, 12, 10, 1, -11 }, +/* 0xE5 */ { 3191, 9, 14, 10, 1, -13 }, +/* 0xE6 */ { 3207, 15, 10, 16, 1, -9 }, +/* 0xE7 */ { 3226, 8, 14, 9, 1, -9 }, +/* 0xE8 */ { 3240, 8, 13, 10, 1, -12 }, +/* 0xE9 */ { 3253, 8, 13, 10, 1, -12 }, +/* 0xEA */ { 3266, 8, 13, 10, 1, -12 }, +/* 0xEB */ { 3279, 8, 12, 10, 1, -11 }, +/* 0xEC */ { 3291, 3, 13, 4, 0, -12 }, +/* 0xED */ { 3296, 3, 13, 4, 1, -12 }, +/* 0xEE */ { 3301, 4, 13, 5, 0, -12 }, +/* 0xEF */ { 3308, 6, 12, 5, -1, -11 }, +/* 0xF0 */ { 3317, 8, 13, 10, 1, -12 }, +/* 0xF1 */ { 3330, 8, 13, 10, 1, -12 }, +/* 0xF2 */ { 3343, 8, 13, 10, 1, -12 }, +/* 0xF3 */ { 3356, 8, 13, 10, 1, -12 }, +/* 0xF4 */ { 3369, 8, 13, 10, 1, -12 }, +/* 0xF5 */ { 3382, 8, 13, 10, 1, -12 }, +/* 0xF6 */ { 3395, 8, 12, 10, 1, -11 }, +/* 0xF7 */ { 3407, 9, 8, 11, 1, -7 }, +/* 0xF8 */ { 3416, 8, 10, 10, 1, -9 }, +/* 0xF9 */ { 3426, 8, 13, 10, 1, -12 }, +/* 0xFA */ { 3439, 8, 13, 10, 1, -12 }, +/* 0xFB */ { 3452, 8, 13, 10, 1, -12 }, +/* 0xFC */ { 3465, 8, 12, 10, 1, -11 }, +/* 0xFD */ { 3477, 8, 17, 9, 0, -12 }, +/* 0xFE */ { 3494, 9, 16, 10, 1, -12 }, +/* 0xFF */ { 3512, 8, 16, 9, 0, -11 }, }; -const GFXfont FreeSans9pt_Win1252 PROGMEM = {(uint8_t *)FreeSans9pt_Win1252Bitmaps, (GFXglyph *)FreeSans9pt_Win1252Glyphs, 0x20, - 0xFF, 21}; +const GFXfont FreeSans9pt_Win1252 PROGMEM = { +(uint8_t*)FreeSans9pt_Win1252Bitmaps, +(GFXglyph*)FreeSans9pt_Win1252Glyphs, +0x01, 0xFF, 16 +}; diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index f63bd4bbe..1e89ebe1b 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -8,8 +8,9 @@ using namespace NicheGraphics; -InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose font. Set by setDefaultFonts -InkHUD::AppletFont InkHUD::Applet::fontSmall; // General purpose font. Set by setDefaultFonts +InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose fonts. Set in nicheGraphics.h +InkHUD::AppletFont InkHUD::Applet::fontMedium; +InkHUD::AppletFont InkHUD::Applet::fontSmall; constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo InkHUD::Applet::Applet() : GFX(0, 0) @@ -353,10 +354,9 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) // Determine if all characters of a string are printable using the current font bool InkHUD::Applet::isPrintable(std::string text) { - // Scan for DEL (0x7F), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled - // Todo: move this to from DEL to SUB, once the fonts have been changed for this + // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled for (char &c : text) { - if (c == '\x7F') + if (c == '\x1A') return false; } diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index c6a8a8aad..802186e6e 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -95,7 +95,7 @@ class Applet : public GFX static uint16_t getHeaderHeight(); // How tall the "standard" applet header is - static AppletFont fontSmall, fontLarge; // The general purpose fonts, used by all applets + static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 88fb4054b..db7097f3f 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -616,9 +616,116 @@ char InkHUD::AppletFont::applyEncoding(std::string utf8) } } - // If not handled, return DEL - // Todo: swap this to SUB, and modify the fonts - return '\x7F'; + else /*ASCII or Unhandled*/ { + if (utf8.length() == 1) + return utf8.at(0); + } + + // All single-byte (ASCII) characters should have been handled by now + // Only unhandled multi-byte UTF8 characters should remain + assert(utf8.length() > 1); + + // Parse emoji + // Strip emoji modifiers + switch (toUtf32(utf8)) { + REMAP(0x1F44D, 0x01) // ๐Ÿ‘ Thumbs Up + REMAP(0x1F44E, 0x02) // ๐Ÿ‘Ž Thumbs Down + + REMAP(0x1F60A, 0x03) // ๐Ÿ˜Š Smiling Face with Smiling Eyes + REMAP(0x1F642, 0x03) // ๐Ÿ™‚ Slightly Smiling Face + REMAP(0x1F601, 0x03) // ๐Ÿ˜ Grinning Face with Smiling Eye + + REMAP(0x1F602, 0x04) // ๐Ÿ˜‚ Face with Tears of Joy + REMAP(0x1F923, 0x04) // ๐Ÿคฃ Rolling on the Floor Laughing + REMAP(0x1F606, 0x04) // ๐Ÿ˜† Smiling with Open Mouth and Closed Eyes + + REMAP(0x1F44B, 0x05) // ๐Ÿ‘‹ Waving Hand + + REMAP(0x02600, 0x06) // โ˜€ Sun + REMAP(0x1F31E, 0x06) // ๐ŸŒž Sun with Face + + // 0x07 - Bell character (unused) + REMAP(0x1F327, 0x08) // ๐ŸŒง๏ธ Cloud with Rain + + REMAP(0x02601, 0x09) // โ˜๏ธ Cloud + REMAP(0x1F32B, 0x09) // Fog + + REMAP(0x1F9E1, 0x0B) // ๐Ÿงก Orange Heart + REMAP(0x02763, 0x0B) // โฃ Heart Exclamation + REMAP(0x02764, 0x0B) // โค Heart + REMAP(0x1F495, 0x0B) // ๐Ÿ’• Two Hearts + REMAP(0x1F496, 0x0B) // ๐Ÿ’– Sparkling Heart + REMAP(0x1F497, 0x0B) // ๐Ÿ’— Growing Heart + REMAP(0x1F498, 0x0B) // ๐Ÿ’˜ Heart with Arrow + + REMAP(0x1F4A9, 0x0C) // ๐Ÿ’ฉ Pile of Poo + // 0x0D - Carriage return (unused) + REMAP(0x1F514, 0x0E) // ๐Ÿ”” Bell + + REMAP(0x1F62D, 0x0F) // ๐Ÿ˜ญ Loudly Crying Face + REMAP(0x1F622, 0x0F) // ๐Ÿ˜ข Crying Face + + REMAP(0x1F64F, 0x10) // ๐Ÿ™ Person with Folded Hands + REMAP(0x1F618, 0x11) // ๐Ÿ˜˜ Face Throwing a Kiss + REMAP(0x1F389, 0x12) // ๐ŸŽ‰ Party Popper + + REMAP(0x1F600, 0x13) // ๐Ÿ˜€ Grinning Face + REMAP(0x1F603, 0x13) // ๐Ÿ˜ƒ Smiling Face with Open Mouth + REMAP(0x1F604, 0x13) // ๐Ÿ˜„ Smiling Face with Open Mouth and Smiling Eyes + + REMAP(0x1F97A, 0x14) // ๐Ÿฅบ Face with Pleading Eyes + REMAP(0x1F605, 0x15) // ๐Ÿ˜… Smiling with Sweat + REMAP(0x1F525, 0x16) // ๐Ÿ”ฅ Fire + REMAP(0x1F926, 0x17) // ๐Ÿคฆ Face Palm + REMAP(0x1F937, 0x18) // ๐Ÿคท Shrug + REMAP(0x1F644, 0x19) // ๐Ÿ™„ Face with Rolling Eyes + // 0x1A Substitution (unused) + REMAP(0x1F917, 0x1B) // ๐Ÿค— Hugging Face + + REMAP(0x1F609, 0x1C) // ๐Ÿ˜‰ Winking Face + REMAP(0x1F61C, 0x1C) // ๐Ÿ˜œ Face with Stuck-Out Tongue and Winking Eye + REMAP(0x1F60F, 0x1C) // ๐Ÿ˜ Smirking Face + + REMAP(0x1F914, 0x1D) // ๐Ÿค” Thinking Face + REMAP(0x1FAE1, 0x1E) // ๐Ÿซก Saluting Face + REMAP(0x1F44C, 0x1F) // ๐Ÿ‘Œ OK Hand Sign + + REMAP(0x02755, '!') // โ• + REMAP(0x02757, '!') // โ— + REMAP(0x0203C, '!') // โ€ผ + REMAP(0x02753, '?') // โ“ + REMAP(0x02754, '?') // โ” + REMAP(0x02049, '?') // โ‰ + + // Modifiers (deleted) + REMAP(0x02640, 0x7F) // Gender + REMAP(0x02642, 0x7F) + REMAP(0x1F3FB, 0x7F) // Skin Tones + REMAP(0x1F3FC, 0x7F) + REMAP(0x1F3FD, 0x7F) + REMAP(0x1F3FE, 0x7F) + REMAP(0x1F3FF, 0x7F) + REMAP(0x0FE00, 0x7F) // Variation Selectors + REMAP(0x0FE01, 0x7F) + REMAP(0x0FE02, 0x7F) + REMAP(0x0FE03, 0x7F) + REMAP(0x0FE04, 0x7F) + REMAP(0x0FE05, 0x7F) + REMAP(0x0FE06, 0x7F) + REMAP(0x0FE07, 0x7F) + REMAP(0x0FE08, 0x7F) + REMAP(0x0FE09, 0x7F) + REMAP(0x0FE0A, 0x7F) + REMAP(0x0FE0B, 0x7F) + REMAP(0x0FE0C, 0x7F) + REMAP(0x0FE0D, 0x7F) + REMAP(0x0FE0E, 0x7F) + REMAP(0x0FE0F, 0x7F) + REMAP(0x0200D, 0x7F) // Zero Width Joiner + } + + // If not handled, return SUB + return '\x1A'; // Sweep up the syntactic sugar // Don't want ants in the house diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 67348b8d3..e1fe37974 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -61,20 +61,26 @@ class AppletFont // Line padding has been adjusted manually, to compensate for a few *extra tall* diacritics // Central European +#include "graphics/niche/Fonts/FreeSans12pt_Win1250.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1250.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1250.h" +#define FREESANS_12PT_WIN1250 InkHUD::AppletFont(FreeSans12pt_Win1250, InkHUD::AppletFont::WINDOWS_1250, -3, 1) #define FREESANS_9PT_WIN1250 InkHUD::AppletFont(FreeSans9pt_Win1250, InkHUD::AppletFont::WINDOWS_1250, -1, -1) #define FREESANS_6PT_WIN1250 InkHUD::AppletFont(FreeSans6pt_Win1250, InkHUD::AppletFont::WINDOWS_1250, -1, -2) // Cyrillic +#include "graphics/niche/Fonts/FreeSans12pt_Win1251.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1251.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1251.h" +#define FREESANS_12PT_WIN1251 InkHUD::AppletFont(FreeSans12pt_Win1251, InkHUD::AppletFont::WINDOWS_1251, -3, 1) #define FREESANS_9PT_WIN1251 InkHUD::AppletFont(FreeSans9pt_Win1251, InkHUD::AppletFont::WINDOWS_1251, -2, -1) #define FREESANS_6PT_WIN1251 InkHUD::AppletFont(FreeSans6pt_Win1251, InkHUD::AppletFont::WINDOWS_1251, -1, -2) // Western European +#include "graphics/niche/Fonts/FreeSans12pt_Win1252.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1252.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1252.h" +#define FREESANS_12PT_WIN1252 InkHUD::AppletFont(FreeSans12pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -3, 1) #define FREESANS_9PT_WIN1252 InkHUD::AppletFont(FreeSans9pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -2, -1) #define FREESANS_6PT_WIN1252 InkHUD::AppletFont(FreeSans6pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -1, -2) diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 7fa31b244..1b0bfa9d0 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -168,11 +168,11 @@ void InkHUD::NodeListApplet::onRender() // Define two lines of text for the card // We will center our text on these lines - uint16_t lineAY = cardTopY + (fontLarge.lineHeight() / 2); - uint16_t lineBY = cardTopY + fontLarge.lineHeight() + (fontSmall.lineHeight() / 2); + uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2); + uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2); // Print the short name - setFont(fontLarge); + setFont(fontMedium); printAt(0, lineAY, shortName, LEFT, MIDDLE); // Print the distance @@ -182,8 +182,8 @@ void InkHUD::NodeListApplet::onRender() // If we have a direct connection to the node, draw the signal indicator if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label - uint16_t signalH = fontLarge.lineHeight() * 0.75; - int16_t signalY = lineAY + (fontLarge.lineHeight() / 2) - (fontLarge.lineHeight() * 0.75); + uint16_t signalH = fontMedium.lineHeight() * 0.75; + int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); int16_t signalX = width() - signalW; drawSignalIndicator(signalX, signalY, signalW, signalH, signal); } diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index 0abcad824..c2340027b 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -65,8 +65,8 @@ class NodeListApplet : public Applet, public MeshModule // Card Dimensions // - for rendering and for maxCards calc - const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards - const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card + uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards + uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index fa85deab3..ecaa7cea3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -14,9 +14,10 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") // During onboarding, show the default short name as well as the version string // This behavior assists manufacturers during mass production, and should not be modified without good reason if (!settings->tips.safeShutdownSeen) { - fontTitle = fontLarge; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + fontTitle = fontMedium; textLeft = xstr(APP_VERSION_SHORT); - textRight = owner.short_name; + textRight = parseShortName(ourNode); textTitle = "Meshtastic"; } else { fontTitle = fontSmall; @@ -51,6 +52,40 @@ void InkHUD::LogoApplet::onRender() setTextColor(WHITE); } +#ifdef USERPREFS_OEM_IMAGE_DATA // Custom boot screen, if defined in userPrefs.jsonc + + // Only show the custom screen at startup + // This allows us to draw the usual Meshtastic logo at shutdown + // The effect is similar to the two-stage userPrefs boot screen used by BaseUI + if (millis() < 10 * 1000UL) { + + // Draw the custom logo + const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; + drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left + logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top + logo, // XBM data + USERPREFS_OEM_IMAGE_WIDTH, // Width + USERPREFS_OEM_IMAGE_HEIGHT, // Height + inverted ? WHITE : BLACK // Color + ); + + // Select the largest font which will still comfortably fit the custom text + setFont(fontLarge); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontMedium); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontSmall); + + // Draw custom text below logo + int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo + printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); + + // Don't draw the normal boot screen, we've already drawn our custom version + return; + } + +#endif + drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); if (!textLeft.empty()) { @@ -115,7 +150,7 @@ void InkHUD::LogoApplet::onShutdown() textLeft = ""; textRight = ""; textTitle = parseShortName(ourNode); - fontTitle = fontLarge; + fontTitle = fontMedium; // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete } diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index f162aa385..c84ee09e0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD enum MenuAction { NO_ACTION, SEND_PING, + STORE_CANNEDMESSAGE_SELECTION, + SEND_CANNEDMESSAGE, SHUTDOWN, NEXT_TILE, TOGGLE_BACKLIGHT, @@ -31,6 +33,7 @@ enum MenuAction { LAYOUT, TOGGLE_BATTERY_ICON, TOGGLE_NOTIFICATIONS, + TOGGLE_INVERT_COLOR, TOGGLE_12H_CLOCK, }; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 9fdfad8ee..a1f79a28f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -5,6 +5,7 @@ #include "RTC.h" #include "MeshService.h" +#include "Router.h" #include "airtime.h" #include "main.h" #include "power.h" @@ -31,6 +32,12 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") if (settings->optionalMenuItems.backlight) { backlight = Drivers::LatchingBacklight::getInstance(); } + + // Initialize the Canned Message store + // This is a shared nicheGraphics component + // - handles loading & parsing the canned messages + // - handles setting / getting of canned messages via apps (Client API Admin Messages) + cm.store = CannedMessageStore::getInstance(); } void InkHUD::MenuApplet::onForeground() @@ -65,6 +72,10 @@ void InkHUD::MenuApplet::onForeground() void InkHUD::MenuApplet::onBackground() { + // Discard any data we generated while selecting a canned message + // Frees heap mem + freeCannedMessageResources(); + // If device has a backlight which isn't controlled by aux button: // Item in options submenu allows keeping backlight on after menu is closed // If this item is deselected we will turn backlight off again, now that menu is closing @@ -153,6 +164,16 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); break; + case STORE_CANNEDMESSAGE_SELECTION: + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + break; + + case SEND_CANNEDMESSAGE: + cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here + break; + case ROTATE: inkhud->rotate(); break; @@ -184,6 +205,15 @@ void InkHUD::MenuApplet::execute(MenuItem item) settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; break; + case TOGGLE_INVERT_COLOR: + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + else + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; + case SET_RECENTS: // Set value of settings.recentlyActiveSeconds // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) @@ -260,9 +290,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case SEND: - items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); - // Todo: canned messages - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + populateSendPage(); + break; + + case CANNEDMESSAGE_RECIPIENT: + populateRecipientPage(); break; case OPTIONS: @@ -293,6 +325,10 @@ void InkHUD::MenuApplet::showPage(MenuPage page) &settings->optionalFeatures.notifications)); items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings->optionalFeatures.batteryIcon)); + + invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); + items.push_back( MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); @@ -497,6 +533,8 @@ void InkHUD::MenuApplet::populateAutoshowPage() } } +// Create MenuItem entries to select our definition of "Recent" +// Controls how long data will remain in any "Recents" flavored applets void InkHUD::MenuApplet::populateRecentsPage() { // How many values are shown for use to choose from @@ -510,6 +548,112 @@ void InkHUD::MenuApplet::populateRecentsPage() } } +// MenuItem entries for the "send" page +// Dynamically creates menu items based on available canned messages +void InkHUD::MenuApplet::populateSendPage() +{ + // Position / NodeInfo packet + items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); + + // One menu item for each canned message + uint8_t count = cm.store->size(); + for (uint8_t i = 0; i < count; i++) { + // Gather the information for this item + CannedMessages::MessageItem messageItem; + messageItem.rawText = cm.store->at(i); + messageItem.label = parse(messageItem.rawText); + + // Store the item (until the menu closes) + cm.messageItems.push_back(messageItem); + + // Create a menu item + const char *itemText = cm.messageItems.back().label.c_str(); + items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); +} + +// Dynamically create MenuItem entries for possible canned message destinations +// All available channels are shown +// Favorite nodes are shown, provided we don't have an *excessive* amount +void InkHUD::MenuApplet::populateRecipientPage() +{ + // Create recipient data (and menu items) for any channels + // -------------------------------------------------------- + + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + // Get the channel, and check if it's enabled + meshtastic_Channel &channel = channels.getByIndex(i); + if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) + continue; + + CannedMessages::RecipientItem r; + + // Set index + r.channelIndex = channel.index; + + // Set a label for the menu item + r.label = "Ch " + to_string(i) + ": "; + if (channel.role == meshtastic_Channel_Role_PRIMARY) + r.label += "Primary"; + else + r.label += parse(channel.settings.name); + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + + // Create recipient data (and menu items) for favorite nodes + // --------------------------------------------------------- + + uint32_t nodeCount = nodeDB->getNumMeshNodes(); + uint32_t favoriteCount = 0; + + // Count favorites + for (uint32_t i = 0; i < nodeCount; i++) { + if (nodeDB->getMeshNodeByIndex(i)->is_favorite) + favoriteCount++; + } + + // Only add favorites if the number is reasonable + // Don't want some monstrous list that takes 100 clicks to reach exit + if (favoriteCount < 20) { + for (uint32_t i = 0; i < nodeCount; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip node if not a favorite + if (!node->is_favorite) + continue; + + CannedMessages::RecipientItem r; + + r.dest = node->num; + r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) + + // Set a label for the menu item + r.label = "DM: "; + if (node->has_user) + r.label += parse(node->user.long_name); + else + r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); +} + // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. @@ -523,11 +667,11 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t // ==================== std::string clockString = getTimeString(); if (clockString.length() > 0) { - setFont(fontLarge); + setFont(fontMedium); printAt(width / 2, top, clockString, CENTER, TOP); - height += fontLarge.lineHeight(); - height += fontLarge.lineHeight() * 0.1; // Padding below clock + height += fontMedium.lineHeight(); + height += fontMedium.lineHeight() * 0.1; // Padding below clock } // Stats @@ -619,4 +763,37 @@ uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() return height; } +// Send a text message to the mesh +// Used to send our canned messages +void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) +{ + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + // Tack on a bell character if requested + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator + p->decoded.payload.size++; + } + + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + + service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone +} + +// Free up any heap mmemory we'd used while selecting / sending canned messages +void InkHUD::MenuApplet::freeCannedMessageResources() +{ + cm.selectedMessageItem = nullptr; + cm.selectedRecipientItem = nullptr; + cm.messageItems.clear(); + cm.recipientItems.clear(); +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index d9297c8ed..8f9280e6f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -6,10 +6,12 @@ #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/InkHUD/Persistence.h" #include "graphics/niche/InkHUD/SystemApplet.h" +#include "graphics/niche/Utils/CannedMessageStore.h" #include "./MenuItem.h" #include "./MenuPage.h" +#include "Channels.h" #include "concurrency/OSThread.h" namespace NicheGraphics::InkHUD @@ -36,12 +38,18 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any void showPage(MenuPage page); // Load and display a MenuPage + + void populateSendPage(); // Dynamically create MenuItems including canned messages + void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + uint16_t getSystemInfoPanelHeight(); void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, - uint16_t *height = nullptr); // Info panel at top of root menu + uint16_t *height = nullptr); // Info panel at top of root menu + void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh + void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data MenuPage currentPage = MenuPage::ROOT; uint8_t cursor = 0; // Which menu item is currently highlighted @@ -51,7 +59,40 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread std::vector items; // MenuItems for the current page. Filled by ShowPage + // Data for selecting and sending canned messages via the menu + // Placed into a sub-class for organization only + class CannedMessages + { + public: + // Share NicheGraphics component + // Handles loading, getting, setting + CannedMessageStore *store; + + // One canned message + // Links the menu item to the true message text + struct MessageItem { + std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed + std::string rawText; // The message which will be sent, if this item is selected + } *selectedMessageItem; + + // One possible destination for a canned message + // Links the menu item to the intended recipient + // May represent either broadcast or DM + struct RecipientItem { + std::string label; // Shown in menu + NodeNum dest = NODENUM_BROADCAST; + uint8_t channelIndex = 0; + } *selectedRecipientItem; + + // These lists are generated when the menu page is populated + // Cleared onBackground (when MenuApplet closes) + std::vector messageItems; + std::vector recipientItems; + } cm; + Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu + + bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index d2314e83b..389e411c3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -18,6 +18,7 @@ namespace NicheGraphics::InkHUD enum MenuPage : uint8_t { ROOT, // Initial menu page SEND, + CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message OPTIONS, APPLETS, AUTOSHOW, diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index 3f51c7f88..09931f109 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -12,13 +12,13 @@ InkHUD::PairingApplet::PairingApplet() void InkHUD::PairingApplet::onRender() { // Header - setFont(fontLarge); + setFont(fontMedium); printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM); setFont(fontSmall); printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP); // Passkey - setFont(fontLarge); + setFont(fontMedium); printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2); // Device's bluetooth name, if it will fit diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index 82a196cb1..ade44ab65 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -50,11 +50,11 @@ void InkHUD::TipsApplet::onRender() break; case Tip::FINISH_SETUP: { - setFont(fontLarge); + setFont(fontMedium); printAt(0, 0, "Tip: Finish Setup"); setFont(fontSmall); - int16_t cursorY = fontLarge.lineHeight() * 1.5; + int16_t cursorY = fontMedium.lineHeight() * 1.5; printAt(0, cursorY, "- connect antenna"); cursorY += fontSmall.lineHeight() * 1.2; @@ -80,7 +80,7 @@ void InkHUD::TipsApplet::onRender() } break; case Tip::SAFE_SHUTDOWN: { - setFont(fontLarge); + setFont(fontMedium); printAt(0, 0, "Tip: Shutdown"); setFont(fontSmall); @@ -88,29 +88,29 @@ void InkHUD::TipsApplet::onRender() shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; shutdown += "\n"; shutdown += "This ensures data is saved."; - printWrapped(0, fontLarge.lineHeight() * 1.5, width(), shutdown); + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::CUSTOMIZATION: { - setFont(fontLarge); + setFont(fontMedium); printAt(0, 0, "Tip: Customization"); setFont(fontSmall); - printWrapped(0, fontLarge.lineHeight() * 1.5, width(), + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::BUTTONS: { - setFont(fontLarge); + setFont(fontMedium); printAt(0, 0, "Tip: Buttons"); setFont(fontSmall); - int16_t cursorY = fontLarge.lineHeight() * 1.5; + int16_t cursorY = fontMedium.lineHeight() * 1.5; printAt(0, cursorY, "User Button"); cursorY += fontSmall.lineHeight() * 1.2; @@ -123,11 +123,11 @@ void InkHUD::TipsApplet::onRender() } break; case Tip::ROTATION: { - setFont(fontLarge); + setFont(fontMedium); printAt(0, 0, "Tip: Rotation"); setFont(fontSmall); - printWrapped(0, fontLarge.lineHeight() * 1.5, width(), + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); @@ -155,7 +155,7 @@ void InkHUD::TipsApplet::renderWelcome() uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); // Title size - setFont(fontLarge); + setFont(fontMedium); std::string title; if (width() >= 200) // Future proofing: hide if *tiny* display title = "meshtastic.org"; diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 17d724aee..7c6232f3b 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -101,15 +101,25 @@ void InkHUD::AllMessageApplet::onRender() // Extra gap below the header int16_t textTop = headerDivY + padDivH; - // Determine size if printed large + // Attempt to print with fontLarge + uint32_t textHeight; setFont(fontLarge); - uint32_t textHeight = getWrappedTextHeight(0, width(), text); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } - // If too large, swap to small font - if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned) - setFont(fontSmall); + // Fallback (too large): attempt to print with fontMedium + setFont(fontMedium); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } - // Print text + // Fallback (too large): print with fontSmall + setFont(fontSmall); printWrapped(0, textTop, width(), text); } diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index dbf5c08fb..a3b9615a5 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -97,15 +97,25 @@ void InkHUD::DMApplet::onRender() // Extra gap below the header int16_t textTop = headerDivY + padDivH; - // Determine size if printed large + // Attempt to print with fontLarge + uint32_t textHeight; setFont(fontLarge); - uint32_t textHeight = getWrappedTextHeight(0, width(), text); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } - // If too large, swap to small font - if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned) - setFont(fontSmall); + // Fallback (too large): attempt to print with fontMedium + setFont(fontMedium); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } - // Print text + // Fallback (too large): print with fontSmall + setFont(fontSmall); printWrapped(0, textTop, width(), text); } diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index d5d7f77f8..fdb5a168d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -13,7 +13,8 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex) +InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) + : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { // Create the message store // Will shortly attempt to load messages from RAM, if applet is active @@ -69,9 +70,8 @@ void InkHUD::ThreadedMessageApplet::onRender() // Grab data for message MessageStore::Message &m = store->messages.at(i); - bool outgoing = (m.sender == 0); - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); - std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message + bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message + std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message // Cache bottom Y of message text // - Used when drawing vertical line alongside @@ -171,54 +171,54 @@ void InkHUD::ThreadedMessageApplet::onRender() void InkHUD::ThreadedMessageApplet::onActivate() { loadMessagesFromFlash(); - textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage + loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) } // Code which runs when the applet stop running -// This might be happen at shutdown, or if user disables the applet at run-time +// This might be at shutdown, or if the user disables the applet at run-time, via the menu void InkHUD::ThreadedMessageApplet::onDeactivate() { - textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage + loopbackOk = false; // Slightly reduce our impact if the applet is disabled } // Handle new text messages // These might be incoming, from the mesh, or outgoing from phone // Each instance of the ThreadMessageApplet will only listen on one specific channel -// Method should return 0, to indicate general success to TextMessageModule -int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets if (!isActive()) - return 0; + return ProcessMessage::CONTINUE; // Abort if wrong channel - if (p->channel != this->channelIndex) - return 0; + if (mp.channel != this->channelIndex) + return ProcessMessage::CONTINUE; // Abort if message was a DM - if (p->to != NODENUM_BROADCAST) - return 0; + if (mp.to != NODENUM_BROADCAST) + return ProcessMessage::CONTINUE; // Extract info into our slimmed-down "StoredMessage" type MessageStore::Message newMessage; newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - newMessage.sender = p->from; - newMessage.channelIndex = p->channel; - newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]); + newMessage.sender = mp.from; + newMessage.channelIndex = mp.channel; + newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); // Store newest message at front // These records are used when rendering, and also stored in flash at shutdown store->messages.push_front(newMessage); // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(p) != nodeDB->getNodeNum()) + if (getFrom(&mp) != nodeDB->getNodeNum()) requestAutoshow(); // Redraw the applet, perhaps. requestUpdate(); // Want to update display, if applet is foreground - return 0; + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages + return ProcessMessage::CONTINUE; } // Don't show notifications for text messages broadcast to our channel, when the applet is displayed diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 3e11a25f2..c986539b3 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -30,7 +30,7 @@ namespace NicheGraphics::InkHUD class Applet; -class ThreadedMessageApplet : public Applet +class ThreadedMessageApplet : public Applet, public SinglePortModule { public: explicit ThreadedMessageApplet(uint8_t channelIndex); @@ -41,16 +41,11 @@ class ThreadedMessageApplet : public Applet void onActivate() override; void onDeactivate() override; void onShutdown() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; bool approveNotification(Notification &n) override; // Which notifications to suppress protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, - &ThreadedMessageApplet::onReceiveTextMessage); - void saveMessagesToFlash(); void loadMessagesFromFlash(); diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index e8849b72e..7e1accafd 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -7,7 +7,12 @@ 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/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index ee6c04938..cdda1638d 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -3,13 +3,14 @@ #include "./Events.h" #include "RTC.h" -#include "modules/AdminModule.h" +#include "buzz.h" +#include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" #include "./Applet.h" #include "./SystemApplet.h" -#include "graphics/niche/FlashData.h" +#include "graphics/niche/Utils/FlashData.h" using namespace NicheGraphics; @@ -28,7 +29,7 @@ void InkHUD::Events::begin() rebootObserver.observe(¬ifyReboot); textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 lightSleepObserver.observe(¬ifyLightSleep); @@ -37,6 +38,13 @@ void InkHUD::Events::begin() void InkHUD::Events::onButtonShort() { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { @@ -49,12 +57,16 @@ void InkHUD::Events::onButtonShort() // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onButtonShortPress(); - else + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module inkhud->nextApplet(); } void InkHUD::Events::onButtonLong() { + // Audio feedback (via buzzer) + // Slightly longer than playChirp + playBoop(); + // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { @@ -102,6 +114,10 @@ int InkHUD::Events::beforeDeepSleep(void *unused) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); delay(1000); // Cooldown, before potentially yanking display power + // InkHUD shutdown complete + // Firmware shutdown continues for several seconds more; flash write still pending + playShutdownMelody(); + return 0; // We agree: deep sleep now } @@ -176,14 +192,15 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } -int InkHUD::Events::onAdminMessage(const meshtastic_AdminMessage *message) +int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { - switch (message->which_payload_variant) { + switch (data->request->which_payload_variant) { // Factory reset // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. case meshtastic_AdminMessage_factory_reset_device_tag: case meshtastic_AdminMessage_factory_reset_config_tag: eraseOnReboot = true; + *data->result = AdminMessageHandleResult::HANDLED; break; default: @@ -204,4 +221,24 @@ int InkHUD::Events::beforeLightSleep(void *unused) } #endif +// Silence all ongoing beeping, blinking, buzzing, coming from the external notification module +// Returns true if an external notification was active, and we dismissed it +// Button handling changes depending on our result +bool InkHUD::Events::dismissExternalNotification() +{ + // Abort if not using external notifications + if (!moduleConfig.external_notification.enabled) + return false; + + // Abort if nothing to dismiss + if (!externalNotificationModule->nagging()) + return false; + + // Stop the beep buzz blink + externalNotificationModule->stopNow(); + + // Inform that we did indeed dismiss an external notification + return true; +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 489135ea3..df68f368c 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -13,7 +13,7 @@ however this class handles general events which concern InkHUD as a whole, e.g. #include "configuration.h" -#include "Observer.h" +#include "modules/AdminModule.h" #include "./InkHUD.h" #include "./Persistence.h" @@ -33,7 +33,7 @@ class Events int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message - int onAdminMessage(const meshtastic_AdminMessage *message); // Handle incoming admin messages + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); // Prepare for light sleep #endif @@ -54,14 +54,17 @@ class Events CallbackObserver(this, &Events::onReceiveTextMessage); // Get notified of incoming admin messages, and handle any which are relevant to InkHUD - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Events::onAdminMessage); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Events::onAdminMessage); #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); #endif + // End any externalNotification beeping, buzzing, blinking etc + bool dismissExternalNotification(); + // If set, InkHUD's data will be erased during onReboot bool eraseOnReboot = false; }; diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index 40f1dd521..b85274c87 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -15,8 +15,8 @@ The save / load mechanism is a shared NicheGraphics feature. #include "configuration.h" #include "./InkHUD.h" -#include "graphics/niche/FlashData.h" #include "graphics/niche/InkHUD/MessageStore.h" +#include "graphics/niche/Utils/FlashData.h" namespace NicheGraphics::InkHUD { diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index cab0ea7bc..e5a0e67df 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -6,6 +6,7 @@ build_flags = -D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics -D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI) -D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class + -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D HAS_BUTTON=0 ; Suppress default ButtonThread lib_deps = https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index c058c4126..072e9dbd6 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -224,6 +224,13 @@ void InkHUD::Renderer::render(bool async) renderPlaceholders(); renderSystemApplets(); + // Invert Buffer if set by user + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) { + imageBuffer[i] = ~imageBuffer[i]; + } + } + // Tell display to begin process of drawing new image LOG_INFO("Updating display"); driver->update(imageBuffer, updateType); diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index b504d46c1..e7821299e 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -1,6 +1,6 @@ # InkHUD -This document is intended as a reference for maintainers. A haphazard collection of notes which _might_ be helpful. +A haphazard collection of notes which _might_ be helpful for developers. self deprecating meme @@ -109,7 +109,7 @@ The display image does not update "automatically". Individual applets are respon (animated diagram) -animated process diagram of InkHUD rendering +animated process diagram of InkHUD rendering An overview: @@ -312,18 +312,19 @@ As a general overview: ## Fonts -InkHUD uses AdafruitGFX fonts. The large and small font which are shared by all applets are set in nicheGraphics.h. +InkHUD uses AdafruitGFX fonts. Three shared fonts (small, medium, large) are available for use by all applets. These are set per-variant in nicheGraphics.h. ```cpp // Prepare fonts -InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; +InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; +InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Using a generic AdafruitGFX font instead: -// InkHUD::Applet::fontLarge = FreeSerif9pt7b; +// InkHUD::Applet::fontLarge = FreeSerif18pt7b; ``` -Any generic AdafruitGFX font may be used, but the fonts which are bundled with InkHUD have been customized with extended-ASCII character sets. +Any generic AdafruitGFX font may be used, but the fonts which are bundled with InkHUD have been customized with extended-ASCII character sets and emoji. ### Parsing Unicode Text @@ -338,6 +339,8 @@ std::string parsed = parse(greeting); This will re-encode the characters to match whichever extended-ASCII font InkHUD has been built with. +A limited set of emoji have been [wedged into unused code points within the font](#emoji). + ### Localization InkHUD is bundled with extended-ASCII fonts for: @@ -349,10 +352,12 @@ InkHUD is bundled with extended-ASCII fonts for: The default builds use Windows-1252 encoding. This can be changed in nicheGraphics.h. ```cpp -InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1250; +InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1250; +InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1250; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1250; -InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1251; +InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1251; +InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1251; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1251; ``` @@ -734,3 +739,36 @@ Some fonts may have a handful of especially tall characters, especially extended // -2 px of padding above, +1 px of padding below InkHUD::AppletFont(FreeSans9pt7b, ASCII, -2, 1); ``` + +#### Emoji + +AdafruitGFX fonts are limited to 255 characters. InkHUD supports a restricted set of emoji, which are stored in the unused code points of the ASCII control characters (`'\x01'`, `'\x02'`, etc). + +Standard AdafruitGFX fonts contain no glyphs below `'\x20'`, so will ignore these attempts to parse emoji. + +This mapping of emoji to control characters is fairly arbitrary. Selection was influenced by [PR #3940 Oled screen emojis](https://github.com/meshtastic/firmware/pull/3940) and [Emoji Frequency Spreadsheet](https://docs.google.com/spreadsheets/d/1Zs13WJYdZL1pNZP0dCIXkWau_tZOjK3mmJz0KNq4I30/). + +| Code Point | Emoji | +| ---------- | ---------------------------------------------- | +| ~~`0x00`~~ | (null term, unused) | +| `0x01` | ๐Ÿ‘ | +| `0x02` | ๐Ÿ‘Ž | +| `0x03` | ๐Ÿ™‚ | +| `0x04` | ๐Ÿ˜† | +| `0x05` | ๐Ÿ‘‹ | +| `0x06` | โ˜€ | +| ~~`0x07`~~ | (bell char, unused) | +| `0x08` | ๐ŸŒง | +| `0x09` | โ˜ | +| ~~`0x0A`~~ | (line feed, unused) | +| `0x0B` | โ™ฅ | +| `0x0C` | ๐Ÿ’ฉ | +| ~~`0x0D`~~ | (carriage return, unused) | +| `0x0E` | ๐Ÿ”” | +| `0x0F` | ๐Ÿ˜ญ | +| `0x1A` | (substitution "โฐ", used for unprintable chars) | +| `0x1B` | ๐Ÿค— | +| `0x1C` | ๐Ÿ˜‰ | +| `0x1D` | ๐Ÿ˜ | +| `0x1E` | ๐Ÿซก (saluting face) | +| `0x1F` | ๐Ÿ‘Œ | diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp new file mode 100644 index 000000000..50998930d --- /dev/null +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -0,0 +1,163 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./CannedMessageStore.h" + +#include "FSCommon.h" +#include "NodeDB.h" +#include "SPILock.h" +#include "generated/meshtastic/cannedmessages.pb.h" + +using namespace NicheGraphics; + +// Location of the file which stores the canned messages on flash +static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; + +CannedMessageStore::CannedMessageStore() +{ +#if !MESHTASTIC_EXCLUDE_ADMIN + adminMessageObserver.observe(adminModule); +#endif + + // Load & parse messages from flash + load(); +} + +// Get access to (or create) the singleton instance of this class +CannedMessageStore *CannedMessageStore::getInstance() +{ + // Instantiate the class the first time this method is called + static CannedMessageStore *const singletonInstance = new CannedMessageStore; + + return singletonInstance; +} + +// Access canned messages by index +// Consumer should check CannedMessageStore::size to avoid accessing out of bounds +const std::string &CannedMessageStore::at(uint8_t i) +{ + assert(i < messages.size()); + return messages.at(i); +} + +// Number of canned message strings available +uint8_t CannedMessageStore::size() +{ + return messages.size(); +} + +// Load canned message data from flash, and parse into the individual strings +void CannedMessageStore::load() +{ + // In case we're reloading + messages.clear(); + + // Attempt to load the bulk canned message data from flash + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, + sizeof(meshtastic_CannedMessageModuleConfig), + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + // Abort if nothing to load + if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) + return; + + // Split into individual canned messages + // These are concatenated when stored in flash, using '|' as a delimiter + std::string s; + for (char c : cannedMessageModuleConfig.messages) { // Character by character + + // If found end of a string + if (c == '|' || c == '\0') { + // Copy into the vector (if non-empty) + if (!s.empty()) + messages.push_back(s); + + // Reset the string builder + s.clear(); + + // End of data, all strings processed + if (c == 0) + break; + } + + // Otherwise, append char (continue building string) + else + s.push_back(c); + } +} + +// Handle incoming admin messages +// We get these as an observer of AdminModule +// It's our responsibility to handle setting and getting of canned messages via the client API +// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics +int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) +{ + switch (data->request->which_payload_variant) { + + // Client API changing the canned messages + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + handleSet(data->request); + *data->result = AdminMessageHandleResult::HANDLED; + break; + + // Client API wants to know the current canned messages + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + handleGet(data->response); + *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; + + default: + break; + } + + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) +} + +// Client API changing the canned messages +void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) +{ + // Copy into the correct struct (for writing to flash as protobuf) + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, + sizeof(cannedMessageModuleConfig.messages)); + + // Ensure the directory exists +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + + // Write to flash + nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + // Reload from flash, to update the canned messages in RAM + // (This is a lazy way to handle it) + load(); +} + +// Client API wants to know the current canned messages +// We're reconstructing the monolithic canned message string from our copy of the messages in RAM +// Lazy, but more convenient that reloading the monolithic string from flash just for this +void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) +{ + // Merge the canned messages back into the delimited format expected + std::string merged; + if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 + merged.reserve(201); + for (std::string &s : messages) { + merged += s; + merged += '|'; + } + merged.pop_back(); // Drop the final delimiter (loop added one too many) + } + + // Place the data into the response + // This response is scoped to AdminModule::handleReceivedProtobuf + // We were passed reference to it via the observable + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Utils/CannedMessageStore.h b/src/graphics/niche/Utils/CannedMessageStore.h new file mode 100644 index 000000000..c00e1cf5c --- /dev/null +++ b/src/graphics/niche/Utils/CannedMessageStore.h @@ -0,0 +1,54 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics tool + +Makes canned message data accessible to any NicheGraphics UI. + - handles loading & parsing from flash + - handles the admin messages for setting & getting canned messages via client API (phone apps, etc) + +The original CannedMessageModule class is bound to Screen.cpp, +making it incompatible with the NicheGraphics framework, which suppresses Screen.cpp + +This implementation aims to be self-contained. +The necessary interaction with the AdminModule is done as an observer. + +*/ + +#pragma once + +#include "configuration.h" + +#include "modules/AdminModule.h" + +namespace NicheGraphics +{ + +class CannedMessageStore +{ + public: + static CannedMessageStore *getInstance(); // Create or get the singleton instance + const std::string &at(uint8_t i); // Get canned message at index + uint8_t size(); // Get total number of canned messages + + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + + private: + CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() + + void load(); // Load from flash, and parse + + void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages + void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages + + std::vector messages; + + // Get notified of incoming admin messages, to get / set canned messages + CallbackObserver adminMessageObserver = + CallbackObserver(this, &CannedMessageStore::onAdminMessage); +}; + +}; // namespace NicheGraphics + +#endif \ No newline at end of file diff --git a/src/graphics/niche/FlashData.h b/src/graphics/niche/Utils/FlashData.h similarity index 100% rename from src/graphics/niche/FlashData.h rename to src/graphics/niche/Utils/FlashData.h diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp new file mode 100644 index 000000000..ad667f003 --- /dev/null +++ b/src/input/ButtonThread.cpp @@ -0,0 +1,321 @@ +#include "ButtonThread.h" +#include "meshUtils.h" + +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#include "MeshService.h" +#include "RadioLibInterface.h" +#include "buzz.h" +#include "input/InputBroker.h" +#include "main.h" +#include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" +#include "power.h" +#include "sleep.h" +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +using namespace concurrency; + +#if HAS_BUTTON +#endif +ButtonThread::ButtonThread(const char *name) : OSThread(name) +{ + _originName = name; +} + +bool ButtonThread::initButton(const ButtonConfig &config) +{ + if (inputBroker) + inputBroker->registerSource(this); + _longPressTime = config.longPressTime; + _longLongPressTime = config.longLongPressTime; + _pinNum = config.pinNumber; + _activeLow = config.activeLow; + _touchQuirk = config.touchQuirk; + _intRoutine = config.intRoutine; + _longLongPress = config.longLongPress; + + userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); + + if (config.pullupSense != 0) { + pinMode(config.pinNumber, config.pullupSense); + } + + _singlePress = config.singlePress; + userButton.attachClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_PRESSED; + }, + 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); + } + + if (config.doublePress != INPUT_BROKER_NONE) { + _doublePress = config.doublePress; + userButton.attachDoubleClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; + }, + this); + } + + if (config.triplePress != INPUT_BROKER_NONE) { + _triplePress = config.triplePress; + userButton.attachMultiClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->storeClickCount(); + thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; + }, + this); + } + if (config.shortLong != INPUT_BROKER_NONE) { + _shortLong = config.shortLong; + } + + userButton.setDebounceMs(1); + userButton.setPressMs(_longPressTime); + + if (screen) { + userButton.setClickMs(20); + } else { + userButton.setClickMs(BUTTON_CLICK_MS); + } + attachButtonInterrupts(); +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + return true; +} + +int32_t ButtonThread::runOnce() +{ + // If the button is pressed we suppress CPU sleep until release + canSleep = true; // Assume we should not keep the board awake + + // Check for combination timeout + if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { + waitingForLongPress = false; + } + + userButton.tick(); + canSleep &= userButton.isIdle(); + + // Check if we should play lead-up sound during long press + // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers + bool buttonCurrentlyPressed = isButtonPressed(_pinNum); + + // Detect start of button press + if (buttonCurrentlyPressed && !buttonWasPressed) { + buttonPressStartTime = millis(); + leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + // Progressive lead-up sound system + if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS && + (millis() - buttonPressStartTime) < _longLongPressTime) { + + // Start the progressive sequence if not already active + if (!leadUpSequenceActive) { + leadUpSequenceActive = true; + lastLeadUpNoteTime = millis(); + playNextLeadUpNote(); // Play the first note immediately + } + // Continue playing notes at intervals + else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes + if (playNextLeadUpNote()) { + lastLeadUpNoteTime = millis(); + } + } + } + + // Reset when button is released + if (!buttonCurrentlyPressed && buttonWasPressed) { + leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + buttonWasPressed = buttonCurrentlyPressed; + + // new behavior + if (btnEvent != BUTTON_EVENT_NONE) { + InputEvent evt; + evt.source = _originName; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + evt.inputEvent = _singlePress; + // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types + this->notifyObservers(&evt); + + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); + + break; + } + case BUTTON_EVENT_LONG_PRESSED: { + // Ignore if: TX in progress + // Uncommon T-Echo hardware bug, LoRa TX triggers touch button + if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) + break; + + // Check if this is part of a short-press + long-press combination + if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && + (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { + evt.inputEvent = _shortLong; + // evt.kbchar = _shortLong; + this->notifyObservers(&evt); + // Play the combination tune + playComboTune(); + + break; + } + + // 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; + + break; + } + + case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected + LOG_INFO("Double press!"); + + // Reset combination tracking + waitingForLongPress = false; + + evt.inputEvent = _doublePress; + // evt.kbchar = _doublePress; + this->notifyObservers(&evt); + playComboTune(); + + break; + } + + case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present + LOG_INFO("Mulitipress! %hux", multipressClickCount); + + // Reset combination tracking + waitingForLongPress = false; + + switch (multipressClickCount) { + case 3: + evt.inputEvent = _triplePress; + // evt.kbchar = _triplePress; + this->notifyObservers(&evt); + playComboTune(); + break; + + // No valid multipress action + default: + break; + } // end switch: click count + + break; + } // end multipress event + + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediatedly. + case BUTTON_EVENT_LONG_RELEASED: { + + LOG_INFO("LONG PRESS RELEASE"); + if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && + (millis() - buttonPressStartTime) >= _longLongPressTime) { + evt.inputEvent = _longLongPress; + this->notifyObservers(&evt); + } + // Reset combination tracking + waitingForLongPress = false; + + break; + } + + // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG + default: { + break; + } + } + } + btnEvent = BUTTON_EVENT_NONE; + return 50; +} + +/* + * Attach (or re-attach) hardware interrupts for buttons + * Public method. Used outside class when waking from MCU sleep + */ +void ButtonThread::attachButtonInterrupts() +{ + // Interrupt for user button, during normal use. Improves responsiveness. + attachInterrupt(_pinNum, _intRoutine, CHANGE); +} + +/* + * Detach the "normal" button interrupts. + * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep + */ +void ButtonThread::detachButtonInterrupts() +{ + detachInterrupt(_pinNum); +} + +#ifdef ARCH_ESP32 + +// Detach our class' interrupts before lightsleep +// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press +int ButtonThread::beforeLightSleep(void *unused) +{ + detachButtonInterrupts(); + return 0; // Indicates success +} + +// Reconfigure our interrupts +// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep +int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachButtonInterrupts(); + return 0; // Indicates success +} + +#endif + +// Non-static method, runs during callback. Grabs info while still valid +void ButtonThread::storeClickCount() +{ + multipressClickCount = userButton.getNumberClicks(); +} \ No newline at end of file diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h new file mode 100644 index 000000000..949048de1 --- /dev/null +++ b/src/input/ButtonThread.h @@ -0,0 +1,128 @@ +#pragma once + +#include "InputBroker.h" +#include "OneButton.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +typedef void (*voidFuncPtr)(void); + +struct ButtonConfig { + uint8_t pinNumber; + bool activeLow = true; + bool activePullup = true; + uint32_t pullupSense = 0; + voidFuncPtr intRoutine = nullptr; + input_broker_event singlePress = INPUT_BROKER_NONE; + input_broker_event longPress = INPUT_BROKER_NONE; + uint16_t longPressTime = 500; + input_broker_event doublePress = INPUT_BROKER_NONE; + input_broker_event longLongPress = INPUT_BROKER_NONE; + uint16_t longLongPressTime = 5000; + 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) {} +}; + +#ifndef BUTTON_CLICK_MS +#define BUTTON_CLICK_MS 250 +#endif + +#ifndef BUTTON_TOUCH_MS +#define BUTTON_TOUCH_MS 400 +#endif + +#ifndef BUTTON_COMBO_TIMEOUT_MS +#define BUTTON_COMBO_TIMEOUT_MS 1000 // 1 second to complete the combination -- tap faster +#endif + +#ifndef BUTTON_LEADUP_MS +#define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding +#endif + +class ButtonThread : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot + bool initButton(const ButtonConfig &config); + + enum ButtonEventType { + BUTTON_EVENT_NONE, + BUTTON_EVENT_PRESSED, + BUTTON_EVENT_PRESSED_SCREEN, + BUTTON_EVENT_DOUBLE_PRESSED, + BUTTON_EVENT_MULTI_PRESSED, + BUTTON_EVENT_LONG_PRESSED, + BUTTON_EVENT_LONG_RELEASED, + BUTTON_EVENT_TOUCH_LONG_PRESSED, + BUTTON_EVENT_COMBO_SHORT_LONG, + }; + + ButtonThread(const char *name); + int32_t runOnce() override; + OneButton userButton; + void attachButtonInterrupts(); + void detachButtonInterrupts(); + void storeClickCount(); + bool isButtonPressed(int buttonPin) + { + if (_activeLow) + return !digitalRead(buttonPin); // Active low: pressed = LOW + else + return digitalRead(buttonPin); // Most buttons are active low by default + } + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + private: + input_broker_event _singlePress = INPUT_BROKER_NONE; + input_broker_event _longPress = INPUT_BROKER_NONE; + input_broker_event _longLongPress = INPUT_BROKER_NONE; + + input_broker_event _doublePress = INPUT_BROKER_NONE; + input_broker_event _triplePress = INPUT_BROKER_NONE; + input_broker_event _shortLong = INPUT_BROKER_NONE; + + voidFuncPtr _intRoutine = nullptr; + uint16_t _longPressTime = 500; + uint16_t _longLongPressTime = 5000; + int _pinNum = 0; + bool _activeLow = true; + bool _touchQuirk = false; + + uint32_t buttonPressStartTime = 0; + bool buttonWasPressed = false; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &ButtonThread::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &ButtonThread::afterLightSleep); +#endif + + volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; + + // Store click count during callback, for later use + volatile int multipressClickCount = 0; + + // Combination tracking state + bool waitingForLongPress = false; + uint32_t shortPressTime = 0; + + // Long press lead-up tracking + bool leadUpPlayed = false; + uint32_t lastLeadUpNoteTime = 0; + bool leadUpSequenceActive = false; + + static void wakeOnIrq(int irq, int mode); +}; + +extern ButtonThread *buttonThread; diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 56413bd55..53bcedc63 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -146,31 +146,31 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) { switch (key) { case LEFT: - if (inCannedMessageMenu()) // If in canned message menu - sendKey(CANCEL); // exit the menu (press imaginary cancel key) + if (inCannedMessageMenu()) // If in canned message menu + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) else - sendKey(LEFT); + sendKey(INPUT_BROKER_LEFT); break; case RIGHT: - if (inCannedMessageMenu()) // If in canned message menu: - sendKey(CANCEL); // exit the menu (press imaginary cancel key) + if (inCannedMessageMenu()) // If in canned message menu: + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) else - sendKey(RIGHT); + sendKey(INPUT_BROKER_RIGHT); break; case UP: if (length == LONG) toggleGPS(); else - sendKey(UP); + sendKey(INPUT_BROKER_UP); break; case DOWN: if (length == LONG) sendAdhocPing(); else - sendKey(DOWN); + sendKey(INPUT_BROKER_DOWN); break; case OK: @@ -186,7 +186,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) } // Feed input to the canned messages module -void ExpressLRSFiveWay::sendKey(KeyType key) +void ExpressLRSFiveWay::sendKey(input_broker_event key) { InputEvent e; e.source = inputSourceName; @@ -235,7 +235,7 @@ void ExpressLRSFiveWay::shutdown() { LOG_INFO("Shutdown from long press"); powerFSM.trigger(EVENT_PRESS); - screen->startAlert("Shutting down..."); + 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 @@ -243,17 +243,11 @@ void ExpressLRSFiveWay::shutdown() shutdownAtMsec = millis() + 3000; } -// Emulate user button, or canned message SELECT -// This is necessary as canned message module doesn't translate SELECT to user button presses if the module is disabled -// Contained as one method for easier remapping of buttons by user void ExpressLRSFiveWay::click() { - if (!moduleConfig.canned_message.enabled) - powerFSM.trigger(EVENT_PRESS); - else - sendKey(OK); + sendKey(INPUT_BROKER_SELECT); } ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; -#endif \ No newline at end of file +#endif diff --git a/src/input/ExpressLRSFiveWay.h b/src/input/ExpressLRSFiveWay.h index c53aa9c09..7c7f210f8 100644 --- a/src/input/ExpressLRSFiveWay.h +++ b/src/input/ExpressLRSFiveWay.h @@ -40,13 +40,13 @@ class ExpressLRSFiveWay : public Observable, public concurre // This merged an enum used by the ExpressLRS code, with meshtastic canned message values // Key names are kept simple, to allow user customizaton typedef enum { - UP = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP, - DOWN = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN, - LEFT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT, - RIGHT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT, - OK = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT, - CANCEL = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL, - NO_PRESS = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE + UP = INPUT_BROKER_UP, + DOWN = INPUT_BROKER_DOWN, + LEFT = INPUT_BROKER_LEFT, + RIGHT = INPUT_BROKER_RIGHT, + OK = INPUT_BROKER_SELECT, + CANCEL = INPUT_BROKER_CANCEL, + NO_PRESS = INPUT_BROKER_NONE } KeyType; typedef enum { SHORT, LONG } PressLength; @@ -63,7 +63,7 @@ class ExpressLRSFiveWay : public Observable, public concurre // Meshtastic code void determineAction(KeyType key, PressLength length); - void sendKey(KeyType key); + void sendKey(input_broker_event key); inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } int32_t runOnce() override; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index cb73e32ba..ef6d8df91 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -12,7 +12,7 @@ void InputBroker::registerSource(Observable *source) int InputBroker::handleInputEvent(const InputEvent *event) { - powerFSM.trigger(EVENT_INPUT); + powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release this->notifyObservers(event); return 0; } \ No newline at end of file diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index db7524bb0..4487fa662 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -1,29 +1,40 @@ #pragma once #include "Observer.h" -#define ANYKEY 0xFF -#define MATRIXKEY 0xFE +enum input_broker_event { + INPUT_BROKER_NONE = 0, + INPUT_BROKER_SELECT = 10, + INPUT_BROKER_UP = 17, + INPUT_BROKER_DOWN = 18, + INPUT_BROKER_LEFT = 19, + INPUT_BROKER_RIGHT = 20, + INPUT_BROKER_CANCEL = 24, + INPUT_BROKER_BACK = 27, + INPUT_BROKER_USER_PRESS, + INPUT_BROKER_ALT_PRESS, + INPUT_BROKER_ALT_LONG, + INPUT_BROKER_SHUTDOWN = 0x9b, + INPUT_BROKER_GPS_TOGGLE = 0x9e, + INPUT_BROKER_SEND_PING = 0xaf, + INPUT_BROKER_MATRIXKEY = 0xFE, + INPUT_BROKER_ANYKEY = 0xff + +}; #define INPUT_BROKER_MSG_BRIGHTNESS_UP 0x11 #define INPUT_BROKER_MSG_BRIGHTNESS_DOWN 0x12 #define INPUT_BROKER_MSG_REBOOT 0x90 -#define INPUT_BROKER_MSG_SHUTDOWN 0x9b -#define INPUT_BROKER_MSG_GPS_TOGGLE 0x9e #define INPUT_BROKER_MSG_MUTE_TOGGLE 0xac -#define INPUT_BROKER_MSG_SEND_PING 0xaf -#define INPUT_BROKER_MSG_DISMISS_FRAME 0x8b -#define INPUT_BROKER_MSG_LEFT 0xb4 -#define INPUT_BROKER_MSG_UP 0xb5 -#define INPUT_BROKER_MSG_DOWN 0xb6 -#define INPUT_BROKER_MSG_RIGHT 0xb7 #define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1 #define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA +#define INPUT_BROKER_MSG_TAB 0x09 +#define INPUT_BROKER_MSG_EMOTE_LIST 0x8F typedef struct _InputEvent { const char *source; - char inputEvent; - char kbchar; + input_broker_event inputEvent; + unsigned char kbchar; uint16_t touchX; uint16_t touchY; } InputEvent; @@ -35,6 +46,7 @@ class InputBroker : public Observable public: InputBroker(); void registerSource(Observable *source); + void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } protected: int handleInputEvent(const InputEvent *event); diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index 57a87b0ef..90f06ecc9 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,7 @@ int32_t LinuxInput::runOnce() perror("unable to epoll add"); return disable(); } + kb_found = true; // This is the first time the OSThread library has called this function, so do port setup firstTime = 0; } @@ -72,7 +74,7 @@ int32_t LinuxInput::runOnce() assert(rd > ((signed int)sizeof(struct input_event))); for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; e.kbchar = 0; unsigned int type, code; @@ -131,36 +133,36 @@ int32_t LinuxInput::runOnce() mod = 0x08; break; case KEY_ESC: // ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; break; case KEY_BACK: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; // e.kbchar = key; break; case KEY_UP: // Up - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; break; case KEY_DOWN: // Down - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; break; case KEY_LEFT: // Left - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; break; - e.kbchar = INPUT_BROKER_MSG_LEFT; + e.kbchar = INPUT_BROKER_LEFT; case KEY_RIGHT: // Right - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; break; - e.kbchar = INPUT_BROKER_MSG_RIGHT; + e.kbchar = 0; case KEY_ENTER: // Enter - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case KEY_POWER: system("poweroff"); break; default: // all other keys if (keymap[code]) { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = keymap[code]; } break; @@ -173,8 +175,8 @@ int32_t LinuxInput::runOnce() } report[0] = modifiers; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { - if (e.inputEvent == ANYKEY && (modifiers && 0x22)) + if (e.inputEvent != INPUT_BROKER_NONE) { + if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. this->notifyObservers(&e); } diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 785d98ebe..0557bc180 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -7,7 +7,8 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu } void RotaryEncoderInterruptBase::init( - uint8_t pinA, uint8_t pinB, uint8_t pinPress, char eventCw, char eventCcw, char eventPressed, + uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, + input_broker_event eventPressed, // std::function onIntA, std::function onIntB, std::function onIntPress) : void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) { @@ -34,7 +35,7 @@ void RotaryEncoderInterruptBase::init( int32_t RotaryEncoderInterruptBase::runOnce() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; if (this->action == ROTARY_ACTION_PRESSED) { @@ -48,7 +49,7 @@ int32_t RotaryEncoderInterruptBase::runOnce() e.inputEvent = this->_eventCcw; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h index 9bcf25a69..9bdab4730 100644 --- a/src/input/RotaryEncoderInterruptBase.h +++ b/src/input/RotaryEncoderInterruptBase.h @@ -12,7 +12,8 @@ class RotaryEncoderInterruptBase : public Observable, public { public: explicit RotaryEncoderInterruptBase(const char *name); - void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, char eventCw, char eventCcw, char eventPressed, + void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, + input_broker_event eventPressed, // std::function onIntA, std::function onIntB, std::function onIntPress); void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); void intPressHandler(); @@ -34,8 +35,8 @@ class RotaryEncoderInterruptBase : public Observable, public private: uint8_t _pinA = 0; uint8_t _pinB = 0; - char _eventCw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventCcw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + input_broker_event _eventCw = INPUT_BROKER_NONE; + input_broker_event _eventCcw = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; const char *_originName; }; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 7e79289e5..4f19c8b0b 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -16,9 +16,9 @@ bool RotaryEncoderInterruptImpl1::init() uint8_t pinA = moduleConfig.canned_message.inputbroker_pin_a; uint8_t pinB = moduleConfig.canned_message.inputbroker_pin_b; uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - char eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); - char eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); - char eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); // moduleConfig.canned_message.ext_notification_module_output RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, diff --git a/src/input/ScanAndSelect.cpp b/src/input/ScanAndSelect.cpp deleted file mode 100644 index 1262f99b4..000000000 --- a/src/input/ScanAndSelect.cpp +++ /dev/null @@ -1,230 +0,0 @@ -#include "configuration.h" - -// Normally these input methods are protected by guarding in setupModules -// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class -#if HAS_SCREEN - -#include "ScanAndSelect.h" -#include "modules/CannedMessageModule.h" -#include -#ifdef ARCH_PORTDUINO // Only to check for pin conflict with user button -#include "platform/portduino/PortduinoGlue.h" -#endif - -// Config -static const char name[] = "scanAndSelect"; // should match "allow input source" string -static constexpr uint32_t durationShortMs = 50; -static constexpr uint32_t durationLongMs = 1500; -static constexpr uint32_t durationAlertMs = 2000; - -// Constructor: init base class -ScanAndSelectInput::ScanAndSelectInput() : concurrency::OSThread(name) {} - -// Attempt to setup class; true if success. -// Called by setupModules method. Instance deleted if setup fails. -bool ScanAndSelectInput::init() -{ - // Short circuit: Canned messages enabled? - if (!moduleConfig.canned_message.enabled) - return false; - - // Short circuit: Using correct "input source"? - // Todo: protobuf enum instead of string? - if (strcasecmp(moduleConfig.canned_message.allow_input_source, name) != 0) - return false; - - // Determine which pin to use for the single scan-and-select button - // User can specify this by setting any of the inputbroker pins - // If all values are zero, we'll assume the user *does* want GPIO0 - if (moduleConfig.canned_message.inputbroker_pin_press) - pin = moduleConfig.canned_message.inputbroker_pin_press; - else if (moduleConfig.canned_message.inputbroker_pin_a) - pin = moduleConfig.canned_message.inputbroker_pin_a; - else if (moduleConfig.canned_message.inputbroker_pin_b) - pin = moduleConfig.canned_message.inputbroker_pin_b; - else - pin = 0; // GPIO 0 then - - // Short circuit: if selected pin conficts with the user button -#if defined(ARCH_PORTDUINO) - int pinUserButton = 0; - if (settingsMap.count(user) != 0) { - pinUserButton = settingsMap[user]; - } -#elif defined(USERPREFS_BUTTON_PIN) - int pinUserButton = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; -#elif defined(BUTTON_PIN) - int pinUserButton = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; -#else - int pinUserButton = config.device.button_gpio; -#endif - if (pin == pinUserButton) { - LOG_ERROR("ScanAndSelect conflict with user button"); - return false; - } - - // Set-up the button - pinMode(pin, INPUT_PULLUP); - attachInterrupt(pin, handleChangeInterrupt, CHANGE); - - // Connect our class to the canned message module - inputBroker->registerSource(this); - - LOG_INFO("Initialized 'Scan and Select' input for Canned Messages, using pin %d", pin); - return true; // Init succeded -} - -// Runs periodically, unless sleeping between presses -int32_t ScanAndSelectInput::runOnce() -{ - uint32_t now = millis(); - - // If: "no messages added" alert screen currently shown - if (alertingNoMessage) { - // Dismiss the alert screen several seconds after it appears - if (!Throttle::isWithinTimespanMs(alertingSinceMs, durationAlertMs)) { - alertingNoMessage = false; - screen->endAlert(); - } - } - - // If: Button is pressed - if (digitalRead(pin) == LOW) { - // New press - if (!held) { - downSinceMs = now; - } - - // Existing press - else { - // Longer than shortpress window - // Long press not yet fired (prevent repeat firing while held) - if (!longPressFired && !Throttle::isWithinTimespanMs(downSinceMs, durationLongMs)) { - longPressFired = true; - longPress(); - } - } - - // Record the change of state: button is down - held = true; - } - - // If: Button is not pressed - else { - // Button newly released - // Long press event didn't already fire - if (held && !longPressFired) { - // Duration within shortpress window - // - longer than durationShortPress (debounce) - // - shorter than durationLongPress - if (!Throttle::isWithinTimespanMs(downSinceMs, durationShortMs)) { - shortPress(); - } - } - - // Record the change of state: button is up - held = false; - longPressFired = false; // Re-Arm: allow another long press - } - - // If thread's job is done, let it sleep - if (!held && !alertingNoMessage) { - Thread::canSleep = true; - return OSThread::disable(); - } - - // Run this method again is a few ms - return durationShortMs; -} - -void ScanAndSelectInput::longPress() -{ - // (If canned messages set) - if (cannedMessageModule->hasMessages()) { - // If module frame displayed already, send the current message - if (cannedMessageModule->shouldDraw()) - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); - - // Otherwise, initial long press opens the module frame - else - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - } - - // (If canned messages not set) tell the user - else - alertNoMessage(); -} - -void ScanAndSelectInput::shortPress() -{ - // (If canned messages set) scroll to next message - if (cannedMessageModule->hasMessages()) - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - - // (If canned messages not yet set) tell the user - else - alertNoMessage(); -} - -// Begin running runOnce at regular intervals -// Called from pin change interrupt -void ScanAndSelectInput::enableThread() -{ - Thread::canSleep = false; - OSThread::enabled = true; - OSThread::setIntervalFromNow(0); -} - -// Inform user (screen) that no canned messages have been added -// Automatically dismissed after several seconds -void ScanAndSelectInput::alertNoMessage() -{ - alertingNoMessage = true; - alertingSinceMs = millis(); - - // Graphics code: the alert frame to show on screen - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - display->setTextAlignment(TEXT_ALIGN_CENTER_BOTH); - display->setFont(FONT_SMALL); - int16_t textX = display->getWidth() / 2; - int16_t textY = display->getHeight() / 2; - display->drawString(textX + x, textY + y, "No Canned Messages"); - }); -} - -// Remove the canned message frame from screen -// Used to dismiss the module frame when user button pressed -// Returns true if the frame was previously displayed, and has now been closed -// Return value consumed by Screen class when determining how to handle user button -bool ScanAndSelectInput::dismissCannedMessageFrame() -{ - if (cannedMessageModule->shouldDraw()) { - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); - return true; - } - - return false; -} - -// Feed input to the canned messages module -void ScanAndSelectInput::raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key) -{ - InputEvent e; - e.source = name; - e.inputEvent = key; - notifyObservers(&e); -} - -// Pin change interrupt -void ScanAndSelectInput::handleChangeInterrupt() -{ - // Because we need to detect both press and release (rising and falling edge), the interrupt itself can't determine the - // action. Instead, we start up the thread and get it to read the button for us - - // The instance we're referring to here is created in setupModules() - scanAndSelectInput->enableThread(); -} - -ScanAndSelectInput *scanAndSelectInput = nullptr; // Instantiated in setupModules method. Deleted if unused, or init() fails - -#endif \ No newline at end of file diff --git a/src/input/ScanAndSelect.h b/src/input/ScanAndSelect.h deleted file mode 100644 index 0b3e2716e..000000000 --- a/src/input/ScanAndSelect.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - A "single button" input method for Canned Messages - - - Short press to cycle through messages - - Long Press to send - - To use: - - set "allow input source" to "scanAndSelect" - - set the single button's GPIO as either pin A, pin B, or pin Press - - Originally designed to make use of "extra" built-in button on some boards. - Non-intrusive; suitable for use as a default module config. -*/ - -#pragma once -#include "concurrency/OSThread.h" -#include "main.h" - -// Normally these input methods are protected by guarding in setupModules -// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class -#if HAS_SCREEN - -class ScanAndSelectInput : public Observable, public concurrency::OSThread -{ - public: - ScanAndSelectInput(); // No-op constructor, only initializes OSThread base class - bool init(); // Attempt to setup class; true if success. Instance deleted if setup fails - bool dismissCannedMessageFrame(); // Remove the canned message frame from screen. True if frame was open, and now closed. - void alertNoMessage(); // Inform user (screen) that no canned messages have been added - - protected: - int32_t runOnce() override; // Runs at regular intervals, when enabled - void enableThread(); // Begin running runOnce at regular intervals - static void handleChangeInterrupt(); // Calls enableThread from pin change interrupt - void shortPress(); // Code to run when short press fires - void longPress(); // Code to run when long press fires - void raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key); // Feed input to canned message module - - bool held = false; // Have we handled a change in button state? - bool longPressFired = false; // Long press fires while button still held. This bool ensures the release is no-op - uint32_t downSinceMs = 0; // Debouncing for short press, timing for long press - uint8_t pin = -1; // Read from cannned message config during init - - bool alertingNoMessage = false; // Is the "no canned messages" alert shown on screen? - uint32_t alertingSinceMs = 0; // Used to dismiss the "no canned message" alert several seconds -}; - -extern ScanAndSelectInput *scanAndSelectInput; // Instantiated in setupModules method. Deleted if unused, or init() fails - -#endif \ No newline at end of file diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp new file mode 100644 index 000000000..c212773c4 --- /dev/null +++ b/src/input/SeesawRotary.cpp @@ -0,0 +1,83 @@ +#ifdef ARCH_PORTDUINO +#include "SeesawRotary.h" +#include "input/InputBroker.h" + +using namespace concurrency; + +SeesawRotary *seesawRotary; + +SeesawRotary::SeesawRotary(const char *name) : OSThread(name) +{ + _originName = name; +} + +bool SeesawRotary::init() +{ + if (inputBroker) + inputBroker->registerSource(this); + + if (!ss.begin(SEESAW_ADDR)) { + return false; + } + // attachButtonInterrupts(); + + uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF); + if (version != 4991) { + LOG_WARN("Wrong firmware loaded? %u", version); + } else { + LOG_INFO("Found Product 4991"); + } + /* + #ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + #endif + */ + ss.pinMode(SS_SWITCH, INPUT_PULLUP); + + // get starting position + encoder_position = ss.getEncoderPosition(); + + ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); + ss.enableEncoderInterrupt(); + canSleep = true; // Assume we should not keep the board awake + + return true; +} + +int32_t SeesawRotary::runOnce() +{ + InputEvent e; + e.inputEvent = INPUT_BROKER_NONE; + bool currentlyPressed = !ss.digitalRead(SS_SWITCH); + + if (currentlyPressed && !wasPressed) { + e.inputEvent = INPUT_BROKER_SELECT; + } + wasPressed = currentlyPressed; + + int32_t new_position = ss.getEncoderPosition(); + // did we move arounde? + if (encoder_position != new_position) { + if (encoder_position == 0 && new_position != 1) { + e.inputEvent = INPUT_BROKER_ALT_PRESS; + } else if (new_position == 0 && encoder_position != 1) { + e.inputEvent = INPUT_BROKER_USER_PRESS; + } else if (new_position > encoder_position) { + e.inputEvent = INPUT_BROKER_USER_PRESS; + } else { + e.inputEvent = INPUT_BROKER_ALT_PRESS; + } + encoder_position = new_position; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = 0x00; + this->notifyObservers(&e); + } + + return 50; +} +#endif \ No newline at end of file diff --git a/src/input/SeesawRotary.h b/src/input/SeesawRotary.h new file mode 100644 index 000000000..3812b130a --- /dev/null +++ b/src/input/SeesawRotary.h @@ -0,0 +1,29 @@ +#pragma once +#ifdef ARCH_PORTDUINO + +#include "Adafruit_seesaw.h" +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +#define SS_SWITCH 24 +#define SS_NEOPIX 6 + +#define SEESAW_ADDR 0x36 + +class SeesawRotary : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + bool init(); + SeesawRotary(const char *name); + int32_t runOnce() override; + + private: + Adafruit_seesaw ss; + int32_t encoder_position; + bool wasPressed = false; +}; + +extern SeesawRotary *seesawRotary; +#endif \ No newline at end of file diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index 8d0730418..63501bda5 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -30,7 +30,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) void SerialKeyboard::erase() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; e.source = this->_originName; this->notifyObservers(&e); @@ -81,18 +81,18 @@ int32_t SerialKeyboard::runOnce() if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but // shouldn't be a limitation InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; // SELECT OR SEND OR CANCEL EVENT if (!(shiftRegister2 & (1 << 3))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; } else if (!(shiftRegister2 & (1 << 2))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; - e.kbchar = INPUT_BROKER_MSG_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; } else if (!(shiftRegister2 & (1 << 1))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; } else if (!(shiftRegister2 & (1 << 0))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; } // TEXT INPUT EVENT @@ -120,10 +120,10 @@ int32_t SerialKeyboard::runOnce() // BACKSPACE or TAB else if (!(shiftRegister1 & (1 << 7))) { if (shift == 0 || shift == 2) { // BACKSPACE - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; } else { // shift = 1 => TAB - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; } } @@ -146,7 +146,7 @@ int32_t SerialKeyboard::runOnce() if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { erase(); } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); } else { // then it's shift shift += 1; @@ -159,7 +159,7 @@ int32_t SerialKeyboard::runOnce() keyPressed = 13; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index 21cd7b2d5..d99379b23 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -147,7 +147,6 @@ TCA8418Keyboard::TCA8418Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nu { state = Init; last_key = -1; - next_key = -1; should_backspace = false; last_tap = 0L; char_idx = 0; diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index c7f3c1f28..5c53452a4 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -21,7 +21,6 @@ class TCA8418Keyboard KeyState state; int8_t last_key; - int8_t next_key; bool should_backspace; uint32_t last_tap; uint8_t char_idx; diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index d2f7b54f8..c2755980e 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -43,6 +43,8 @@ int32_t TouchScreenBase::runOnce() // process touch events int16_t x, y; bool touched = getTouch(x, y); + if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen + touched = false; if (touched) { this->setInterval(20); _last_x = x; @@ -93,8 +95,6 @@ int32_t TouchScreenBase::runOnce() if (duration > 0 && duration < TIME_LONG_PRESS) { if (_tapped) { _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_DOUBLE_TAP); - LOG_DEBUG("action DOUBLE TAP(%d/%d)", x, y); } else { _tapped = true; } @@ -124,7 +124,7 @@ int32_t TouchScreenBase::runOnce() } #else // fire TAP event when no 2nd tap occured within time - if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { + if (_tapped) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h index 0b2002551..90314cf02 100644 --- a/src/input/TouchScreenBase.h +++ b/src/input/TouchScreenBase.h @@ -28,7 +28,6 @@ class TouchScreenBase : public Observable, public concurrenc TOUCH_ACTION_LEFT, TOUCH_ACTION_RIGHT, TOUCH_ACTION_TAP, - TOUCH_ACTION_DOUBLE_TAP, TOUCH_ACTION_LONG_PRESS }; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index 20196278d..cea47faeb 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -49,41 +49,33 @@ void TouchScreenImpl1::onEvent(const TouchEvent &event) { InputEvent e; e.source = event.source; - + e.kbchar = 0; e.touchX = event.x; e.touchY = event.y; switch (event.touchEvent) { case TOUCH_ACTION_LEFT: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + e.inputEvent = INPUT_BROKER_LEFT; break; } case TOUCH_ACTION_RIGHT: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); + e.inputEvent = INPUT_BROKER_RIGHT; break; } case TOUCH_ACTION_UP: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + e.inputEvent = INPUT_BROKER_UP; break; } case TOUCH_ACTION_DOWN: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - break; - } - case TOUCH_ACTION_DOUBLE_TAP: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + e.inputEvent = INPUT_BROKER_DOWN; break; } case TOUCH_ACTION_LONG_PRESS: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); + e.inputEvent = INPUT_BROKER_SELECT; break; } case TOUCH_ACTION_TAP: { - if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { - externalNotificationModule->stopNow(); - } else { - powerFSM.trigger(EVENT_INPUT); - } + e.inputEvent = INPUT_BROKER_USER_PRESS; break; } default: diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index e35da3622..d41ad2fd6 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -4,31 +4,41 @@ TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, - char eventDown, char eventUp, char eventLeft, char eventRight, char eventPressed, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), - void (*onIntPress)()) + input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, + input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; this->_pinLeft = pinLeft; this->_pinRight = pinRight; + this->_pinPress = pinPress; this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinDown, INPUT_PULLUP); - pinMode(this->_pinUp, INPUT_PULLUP); - pinMode(this->_pinLeft, INPUT_PULLUP); - pinMode(this->_pinRight, INPUT_PULLUP); - - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinDown, onIntDown, RISING); - attachInterrupt(this->_pinUp, onIntUp, RISING); - attachInterrupt(this->_pinLeft, onIntLeft, RISING); - attachInterrupt(this->_pinRight, onIntRight, RISING); + if (pinPress != 255) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, TB_DIRECTION); + } + if (this->_pinDown != 255) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); + } + if (this->_pinUp != 255) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); + } + if (this->_pinLeft != 255) { + pinMode(this->_pinLeft, INPUT_PULLUP); + attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); + } + if (this->_pinRight != 255) { + pinMode(this->_pinRight, INPUT_PULLUP); + attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); + } LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, pinPress); @@ -39,31 +49,49 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef int32_t TrackballInterruptBase::runOnce() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - + e.inputEvent = INPUT_BROKER_NONE; +#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball if (this->action == TB_ACTION_PRESSED) { // LOG_DEBUG("Trackball event Press"); e.inputEvent = this->_eventPressed; - } else if (this->action == TB_ACTION_UP) { + } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN) { + } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { // LOG_DEBUG("Trackball event DOWN"); e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT) { + } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { // LOG_DEBUG("Trackball event LEFT"); e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT) { + } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { // LOG_DEBUG("Trackball event RIGHT"); e.inputEvent = this->_eventRight; } +#else + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; + } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { + // LOG_DEBUG("Trackball event UP"); + e.inputEvent = this->_eventUp; + } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) { + // LOG_DEBUG("Trackball event DOWN"); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) { + // LOG_DEBUG("Trackball event LEFT"); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) { + // LOG_DEBUG("Trackball event RIGHT"); + e.inputEvent = this->_eventRight; + } +#endif - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); } - + lastEvent = action; this->action = TB_ACTION_NONE; return 100; diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index e7fc99f54..92db8720e 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -3,18 +3,29 @@ #include "InputBroker.h" #include "mesh/NodeDB.h" +#ifndef TB_DIRECTION +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#define TB_DIRECTION (PinStatus) settingsMap[tbDirection] +#else +#define TB_DIRECTION RISING +#endif +#endif + class TrackballInterruptBase : public Observable, public concurrency::OSThread { public: explicit TrackballInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, char eventDown, char eventUp, - char eventLeft, char eventRight, char eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), - void (*onIntRight)(), void (*onIntPress)()); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, + input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, + input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); void intLeftHandler(); void intRightHandler(); + uint32_t lastTime = 0; virtual int32_t runOnce() override; @@ -27,18 +38,20 @@ class TrackballInterruptBase : public Observable, public con TB_ACTION_LEFT, TB_ACTION_RIGHT }; - - volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - - private: uint8_t _pinDown = 0; uint8_t _pinUp = 0; uint8_t _pinLeft = 0; uint8_t _pinRight = 0; - char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventLeft = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventRight = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + uint8_t _pinPress = 0; + + volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + + private: + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventLeft = INPUT_BROKER_NONE; + input_broker_event _eventRight = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; const char *_originName; + TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 0a73b83b6..896238f38 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -6,49 +6,58 @@ TrackballInterruptImpl1 *trackballInterruptImpl1; TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} -void TrackballInterruptImpl1::init() +void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) { -#if !HAS_TRACKBALL - // Input device is disabled. - return; -#else - uint8_t pinUp = TB_UP; - uint8_t pinDown = TB_DOWN; - uint8_t pinLeft = TB_LEFT; - uint8_t pinRight = TB_RIGHT; - uint8_t pinPress = TB_PRESS; - - char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); - char eventLeft = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); - char eventRight = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); - char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + input_broker_event eventDown = INPUT_BROKER_DOWN; + input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventLeft = INPUT_BROKER_LEFT; + input_broker_event eventRight = INPUT_BROKER_RIGHT; + input_broker_event eventPressed = INPUT_BROKER_SELECT; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); -#endif } void TrackballInterruptImpl1::handleIntDown() { - trackballInterruptImpl1->intDownHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intDownHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntUp() { - trackballInterruptImpl1->intUpHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intUpHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntLeft() { - trackballInterruptImpl1->intLeftHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intLeftHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntRight() { - trackballInterruptImpl1->intRightHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intRightHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntPressed() { - trackballInterruptImpl1->intPressHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intPressHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } diff --git a/src/input/TrackballInterruptImpl1.h b/src/input/TrackballInterruptImpl1.h index 36efac6a6..4683efa41 100644 --- a/src/input/TrackballInterruptImpl1.h +++ b/src/input/TrackballInterruptImpl1.h @@ -5,7 +5,7 @@ class TrackballInterruptImpl1 : public TrackballInterruptBase { public: TrackballInterruptImpl1(); - void init(); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); static void handleIntDown(); static void handleIntUp(); static void handleIntLeft(); diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index 979489c57..c66eb13d0 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -6,8 +6,9 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre this->_originName = name; } -void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, char eventDown, char eventUp, char eventPressed, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)()) +void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, + input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -31,27 +32,35 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, int32_t UpDownInterruptBase::runOnce() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - + e.inputEvent = INPUT_BROKER_NONE; + unsigned long now = millis(); if (this->action == UPDOWN_ACTION_PRESSED) { - LOG_DEBUG("GPIO event Press"); - e.inputEvent = this->_eventPressed; + if (now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + LOG_DEBUG("GPIO event Press"); + e.inputEvent = this->_eventPressed; + } } else if (this->action == UPDOWN_ACTION_UP) { - LOG_DEBUG("GPIO event Up"); - e.inputEvent = this->_eventUp; + if (now - lastUpKeyTime >= updownDebounceMs) { + lastUpKeyTime = now; + LOG_DEBUG("GPIO event Up"); + e.inputEvent = this->_eventUp; + } } else if (this->action == UPDOWN_ACTION_DOWN) { - LOG_DEBUG("GPIO event Down"); - e.inputEvent = this->_eventDown; + if (now - lastDownKeyTime >= updownDebounceMs) { + lastDownKeyTime = now; + LOG_DEBUG("GPIO event Down"); + e.inputEvent = this->_eventDown; + } } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; - e.kbchar = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.kbchar = INPUT_BROKER_NONE; this->notifyObservers(&e); } this->action = UPDOWN_ACTION_NONE; - return 100; } diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h index 7060a0d80..a83a298f2 100644 --- a/src/input/UpDownInterruptBase.h +++ b/src/input/UpDownInterruptBase.h @@ -7,8 +7,9 @@ class UpDownInterruptBase : public Observable, public concur { public: explicit UpDownInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, char eventDown, char eventUp, char eventPressed, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)()); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, + input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), + unsigned long updownDebounceMs = 50); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -23,8 +24,14 @@ class UpDownInterruptBase : public Observable, public concur private: uint8_t _pinDown = 0; uint8_t _pinUp = 0; - char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; const char *_originName; + + unsigned long lastUpKeyTime = 0; + unsigned long lastDownKeyTime = 0; + unsigned long lastPressKeyTime = 0; + unsigned long updownDebounceMs = 50; + const unsigned long pressDebounceMs = 200; }; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 7dd1f76b2..761b92348 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -17,9 +17,9 @@ bool UpDownInterruptImpl1::init() uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); - char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + input_broker_event eventDown = INPUT_BROKER_DOWN; + input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventPressed = INPUT_BROKER_SELECT; UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 70e9e4365..5cc069816 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -75,94 +75,94 @@ int32_t KbI2cBase::runOnce() const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key.key) { case 'p': // TAB case 't': // TAB as well if (is_sym) { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; // TAB Scancode is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'q': // ESC if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; - e.kbchar = 0x1b; + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 0x08: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = key.key; break; case 'e': // sym e if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; - e.kbchar = INPUT_BROKER_MSG_UP; + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = INPUT_BROKER_UP; is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'x': // sym x if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; - e.kbchar = INPUT_BROKER_MSG_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 's': // sym s if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; // tweak for destSelect is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'f': // sym f if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; // tweak for destSelect is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 0x13: // Code scanner says the SYM key is 0x13 is_sym = !is_sym; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active break; case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; default: // all other keys - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; is_sym = false; // reset sym state after second keypress break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } @@ -175,57 +175,57 @@ int32_t KbI2cBase::runOnce() while (MPRkeyboard.hasEvent()) { char nextEvent = MPRkeyboard.dequeueEvent(); - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x00; e.source = this->_originName; switch (nextEvent) { case 0x00: // MPR121_NONE - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; case 0x90: // MPR121_REBOOT - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_REBOOT; break; case 0xb4: // MPR121_LEFT - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; break; case 0xb5: // MPR121_UP - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0x00; break; case 0xb6: // MPR121_DOWN - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0x00; break; case 0xb7: // MPR121_RIGHT - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; break; case 0x1b: // MPR121_ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; - e.kbchar = 0x1b; + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; break; case 0x08: // MPR121_BSP - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; break; case 0x0d: // MPR121_SELECT - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; - e.kbchar = 0x0d; + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; break; default: if (nextEvent > 127) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = nextEvent; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); this->notifyObservers(&e); } @@ -237,57 +237,57 @@ int32_t KbI2cBase::runOnce() InputEvent e; while (TCAKeyboard.hasEvent()) { char nextEvent = TCAKeyboard.dequeueEvent(); - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x00; e.source = this->_originName; switch (nextEvent) { case _TCA8418_NONE: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; case _TCA8418_REBOOT: - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_REBOOT; break; case _TCA8418_LEFT: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; break; case _TCA8418_UP: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0x00; break; case _TCA8418_DOWN: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0x00; break; case _TCA8418_RIGHT: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; break; case _TCA8418_BSP: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; break; case _TCA8418_SELECT: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; - e.kbchar = 0x0d; + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; break; case _TCA8418_ESC: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; - e.kbchar = 0x1b; + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; break; default: if (nextEvent > 127) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = nextEvent; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); this->notifyObservers(&e); } @@ -310,7 +310,7 @@ int32_t KbI2cBase::runOnce() if (PrintDataBuf != 0) { LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); InputEvent e; - e.inputEvent = MATRIXKEY; + e.inputEvent = INPUT_BROKER_MATRIXKEY; e.source = this->_originName; e.kbchar = PrintDataBuf; this->notifyObservers(&e); @@ -325,138 +325,150 @@ int32_t KbI2cBase::runOnce() if (i2cBus->available()) { char c = i2cBus->read(); InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (c) { case 0x71: // This is the button q. If modifier and q pressed, it cancels the input if (is_sym) { is_sym = false; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x74: // letter t. if modifier and t pressed call 'tab' if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; // TAB Scancode } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x6d: // letter m. Modifier makes it mute notifications if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x6f: // letter o(+). Modifier makes screen increase in brightness if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x69: // letter i(-). Modifier makes screen decrease in brightness if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x20: // Space. Send network ping like double press does if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; - e.kbchar = INPUT_BROKER_MSG_SEND_PING; // (fn + space) + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x67: // letter g. toggle gps if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; - e.kbchar = INPUT_BROKER_MSG_GPS_TOGGLE; + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = INPUT_BROKER_GPS_TOGGLE; } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x1b: // ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; break; case 0x08: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; - e.kbchar = c; + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; break; case 0xb5: // Up - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; - e.kbchar = INPUT_BROKER_MSG_UP; + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0; break; case 0xb6: // Down - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; - e.kbchar = INPUT_BROKER_MSG_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; break; case 0xb4: // Left - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; - e.kbchar = INPUT_BROKER_MSG_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; break; case 0xb7: // Right - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; - e.kbchar = INPUT_BROKER_MSG_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; break; case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) // toggle moddifiers button. is_sym = !is_sym; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active break; + case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = c; + break; + case 0xaf: // fn+space INPUT_BROKER_SEND_PING + e.inputEvent = INPUT_BROKER_SEND_PING; + e.kbchar = c; + break; + case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN + e.inputEvent = INPUT_BROKER_SHUTDOWN; + e.kbchar = c; + break; + case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x91: // fn+t - case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE - case 0x9e: // fn+g INPUT_BROKER_MSG_GPS_TOGGLE - case 0xaf: // fn+space INPUT_BROKER_MSG_SEND_PING + case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE + case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST // just pass those unmodified - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; break; case 0x0d: // Enter - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; default: // all other keys if (c > 127) { // bogus key value - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; is_sym = false; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp index 51815b525..05f4d8177 100644 --- a/src/input/kbMatrixBase.cpp +++ b/src/input/kbMatrixBase.cpp @@ -73,35 +73,35 @@ int32_t KbMatrixBase::runOnce() LOG_DEBUG("Key 0x%x pressed", key); // reset shift now that we have a keypress InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key) { case 0x1b: // ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; break; case 0x08: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; - e.kbchar = key; + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; break; case 0xb5: // Up - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; break; case 0xb6: // Down - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; break; case 0xb4: // Left - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; - e.kbchar = key; + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; break; case 0xb7: // Right - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; - e.kbchar = key; + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; break; case 0x0d: // Enter - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; case 0x1a: // Shift shift++; @@ -110,11 +110,11 @@ int32_t KbMatrixBase::runOnce() } break; default: // all other keys - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } diff --git a/src/main.cpp b/src/main.cpp index 863f81abc..645fc34c2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,7 +99,24 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #endif #if HAS_BUTTON || defined(ARCH_PORTDUINO) -#include "ButtonThread.h" +#include "input/ButtonThread.h" + +#if defined(BUTTON_PIN_TOUCH) +ButtonThread *TouchButtonThread = nullptr; +#endif + +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) +ButtonThread *UserButtonThread = nullptr; +#endif + +#if defined(ALT_BUTTON_PIN) +ButtonThread *BackButtonThread = nullptr; +#endif + +#if defined(CANCEL_BUTTON_PIN) +ButtonThread *CancelButtonThread = nullptr; +#endif + #endif #include "AmbientLightingThread.h" @@ -169,6 +186,8 @@ ScanI2C::DeviceAddress screen_found = ScanI2C::ADDRESS_NONE; ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; // 0x02 for RAK14004, 0x00 for cardkb, 0x10 for T-Deck uint8_t kb_model; +// global bool to record that a kb is present +bool kb_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -176,6 +195,8 @@ ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE; // The I2C address of the RGB LED (if found) ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE); +/// The I2C address of our Air Quality Indicator (if found) +ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; #ifdef T_WATCH_S3 Adafruit_DRV2605 drv; @@ -220,64 +241,6 @@ const char *getDeviceName() return name; } -#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) -static int32_t ledBlinkCount = 0; - -static int32_t elecrowLedBlinker() -{ - // are we in alert buzzer mode? -#if HAS_BUTTON - if (buttonThread->isBuzzing()) { - // blink LED three times for 3 seconds, then 3 times for a second, with one second pause - if (ledBlinkCount % 2) { // odd means LED OFF - ledBlink.set(false); - ledBlinkCount++; - if (ledBlinkCount >= 12) - ledBlinkCount = 0; - noTone(PIN_BUZZER); - return 1000; - } else { - if (ledBlinkCount < 6) { - ledBlink.set(true); - tone(PIN_BUZZER, 4000, 3000); - ledBlinkCount++; - return 3000; - } else { - ledBlink.set(true); - tone(PIN_BUZZER, 4000, 1000); - ledBlinkCount++; - return 1000; - } - } - } else { -#endif - ledBlinkCount = 0; - if (config.device.led_heartbeat_disabled) - return 1000; - - static bool ledOn; - // remain on when fully charged or discharging above 10% - if ((powerStatus->getIsCharging() && powerStatus->getBatteryChargePercent() >= 100) || - (!powerStatus->getIsCharging() && powerStatus->getBatteryChargePercent() >= 10)) { - ledOn = true; - } else { - ledOn ^= 1; - } - ledBlink.set(ledOn); - // when charging, blink 0.5Hz square wave rate to indicate that - if (powerStatus->getIsCharging()) { - return 500; - } - // Blink rapidly when almost empty or if battery is not connected - if ((!powerStatus->getIsCharging() && powerStatus->getBatteryChargePercent() < 10) || !powerStatus->getHasBattery()) { - return 250; - } -#if HAS_BUTTON - } -#endif - return 1000; -} -#else static int32_t ledBlinker() { // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if @@ -293,7 +256,6 @@ static int32_t ledBlinker() // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); } -#endif uint32_t timeLastPowered = 0; @@ -337,12 +299,22 @@ void setup() #ifdef LED_POWER pinMode(LED_POWER, OUTPUT); - digitalWrite(LED_POWER, HIGH); + digitalWrite(LED_POWER, LED_STATE_ON); #endif #ifdef USER_LED pinMode(USER_LED, OUTPUT); - digitalWrite(USER_LED, LOW); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +#endif + +#ifdef WIFI_LED + pinMode(WIFI_LED, OUTPUT); + digitalWrite(WIFI_LED, LOW); +#endif + +#ifdef BLE_LED + pinMode(BLE_LED, OUTPUT); + digitalWrite(BLE_LED, LOW); #endif #if defined(T_DECK) @@ -372,11 +344,9 @@ void setup() SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif -#if !HAS_TFT meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; -#endif #ifdef USE_SEGGER auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; @@ -465,6 +435,10 @@ void setup() gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); delay(10); #endif +#ifdef BUTTON_NEED_PULLUP2 + gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); + delay(10); +#endif #endif #endif #endif @@ -475,7 +449,7 @@ void setup() #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) // The ThinkNodes have their own blink logic - ledPeriodic = new Periodic("Blink", elecrowLedBlinker); + // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); #else ledPeriodic = new Periodic("Blink", ledBlinker); #endif @@ -526,10 +500,6 @@ void setup() digitalWrite(AQ_SET_PIN, HIGH); #endif -#if HAS_TFT - tftSetup(); -#endif - // Currently only the tbeam has a PMU // PMU initialization needs to be placed before i2c scanning power = new Power(); @@ -592,7 +562,6 @@ void setup() } #endif -#if !HAS_TFT auto screenInfo = i2cScanner->firstScreen(); screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; @@ -610,16 +579,18 @@ void setup() screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; } } -#endif #define UPDATE_FROM_SCANNER(FIND_FN) - +#if defined(USE_VIRTUAL_KEYBOARD) + kb_found = true; +#endif auto rtc_info = i2cScanner->firstRTC(); rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; auto kb_info = i2cScanner->firstKeyboard(); if (kb_info.type != ScanI2C::DeviceType::NONE) { + kb_found = true; cardkb_found = kb_info.address; switch (kb_info.type) { case ScanI2C::DeviceType::RAK14004: @@ -656,6 +627,9 @@ void setup() pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); + auto aqiInfo = i2cScanner->firstAQI(); + aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE; + /* * There are a bunch of sensors that have no further logic than to be found and stuffed into the * nodeTelemetrySensorsMap singleton. This wraps that logic in a temporary scope to declare the temporary field @@ -722,7 +696,9 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN, meshtastic_TelemetrySensorType_DFROBOT_RAIN); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::LTR390UV, meshtastic_TelemetrySensorType_LTR390UV); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::DPS310, meshtastic_TelemetrySensorType_DPS310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); i2cScanner.reset(); #endif @@ -760,6 +736,12 @@ void setup() // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; +#if HAS_TFT + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + tftSetup(); + } +#endif + // If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { router = new NextHopRouter(); @@ -769,11 +751,6 @@ void setup() } else router = new ReliableRouter(); -#if HAS_BUTTON || defined(ARCH_PORTDUINO) - // Buttons. Moved here cause we need NodeDB to be initialized - buttonThread = new ButtonThread(); -#endif - // only play start melody when role is not tracker or sensor if (config.power.is_power_saving == true && IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, @@ -782,11 +759,9 @@ void setup() else playStartMelody(); -#if !HAS_TFT // fixed screen override? if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) screen_model = config.display.oled; -#endif #if defined(USE_SH1107) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 @@ -856,8 +831,23 @@ void setup() // Initialize the screen first so we can show the logo while we start up everything else. #if HAS_SCREEN - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + 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) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); +#elif defined(ARCH_PORTDUINO) + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + } +#else + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #endif + } +#endif // HAS_SCREEN + // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string @@ -921,9 +911,164 @@ 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(); +// buttons are now inputBroker, so have to come after setupModules +#if HAS_BUTTON + int pullup_sense = 0; +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did +#ifdef BUTTON_SENSE_TYPE + pullup_sense = BUTTON_SENSE_TYPE; +#else + pullup_sense = INPUT_PULLUP_SENSE; +#endif +#endif +#if defined(ARCH_PORTDUINO) + + if (settingsMap.count(userButtonPin) != 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { + + LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)settingsMap[userButtonPin]; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); + } + } +#endif + +#ifdef BUTTON_PIN_TOUCH + TouchButtonThread = new ButtonThread("BackButton"); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; + TouchButtonThread->initButton(touchConfig); +#endif + +#if defined(CANCEL_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); +#endif + +#if defined(ALT_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); +#endif + +#if defined(BUTTON_PIN) +#if defined(USERPREFS_BUTTON_PIN) + int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; +#else + int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; +#endif +#ifndef BUTTON_ACTIVE_LOW +#define BUTTON_ACTIVE_LOW true +#endif +#ifndef BUTTON_ACTIVE_PULLUP +#define BUTTON_ACTIVE_PULLUP true +#endif + + // Buttons. Moved here cause we need NodeDB to be initialized + // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.longPressTime = 5000; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } +#endif + +#endif + #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // After modules are setup, so we can observe modules setupNicheGraphics(); @@ -946,19 +1091,19 @@ void setup() // 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) - screen->setup(); + if (screen) + screen->setup(); #elif defined(ARCH_PORTDUINO) - if (screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) { + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } #else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) screen->setup(); #endif #endif - screen->print("Started...\n"); - #ifdef PIN_PWR_DELAY_MS // This may be required to give the peripherals time to power up. delay(PIN_PWR_DELAY_MS); @@ -1217,9 +1362,12 @@ void setup() LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); + if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); - screen->startAlert("Rebooting..."); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } rebootAtMsec = millis() + 5000; } } @@ -1291,6 +1439,9 @@ void setup() LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); #endif + + // We manually run this to update the NodeStatus + nodeDB->notifyObservers(true); } #endif @@ -1322,7 +1473,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; #endif // Option to explicitly include canned messages for edge cases, e.g. niche graphics -#if (!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES +#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif #if NO_EXT_GPIO diff --git a/src/main.h b/src/main.h index beeb1f940..7105bd62b 100644 --- a/src/main.h +++ b/src/main.h @@ -31,13 +31,14 @@ extern HardwareSPI *LoraSPI; extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; +extern bool kb_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; +extern ScanI2C::DeviceAddress aqi_found; extern bool eink_found; extern bool pmu_found; -extern bool isCharging; extern bool isUSBPowered; #ifdef T_WATCH_S3 @@ -92,4 +93,4 @@ void scannerToSensorsMap(const std::unique_ptr &i2cScanner, Scan #endif // We default to 4MHz SPI, SPI mode 0 -extern SPISettings spiSettings; \ No newline at end of file +extern SPISettings spiSettings; diff --git a/src/mesh/Default.h b/src/mesh/Default.h index 208f992c8..2f05da98d 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -5,8 +5,10 @@ #define ONE_DAY 24 * 60 * 60 #define ONE_MINUTE_MS 60 * 1000 #define THIRTY_SECONDS_MS 30 * 1000 +#define TWO_SECONDS_MS 2 * 1000 #define FIVE_SECONDS_MS 5 * 1000 #define TEN_SECONDS_MS 10 * 1000 +#define MAX_INTERVAL INT32_MAX // FIXME: INT32_MAX to avoid overflow issues with Apple clients but should be UINT32_MAX #define min_default_telemetry_interval_secs 30 * 60 #define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) @@ -22,6 +24,11 @@ #define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour #define min_neighbor_info_broadcast_secs 4 * 60 * 60 #define default_map_publish_interval_secs 60 * 60 +#ifdef USERPREFS_RINGTONE_NAG_SECS +#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS +#else +#define default_ringtone_nag_secs 60 +#endif #define default_mqtt_address "mqtt.meshtastic.org" #define default_mqtt_username "meshdev" @@ -61,12 +68,17 @@ class Default throttlingFactor = 0.04; else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) throttlingFactor = 0.02; - else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW) - throttlingFactor = 0.01; else if (config.lora.use_preset && IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO)) - return 1.0; // Don't bother throttling for highest bandwidth presets + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) + throttlingFactor = 0.01; + +#if USERPREFS_EVENT_MODE + // If we are in event mode, scale down the throttling factor + throttlingFactor = 0.04; +#endif + // Scaling up traffic based on number of nodes over 40 int nodesOverForty = (numOnlineNodes - 40); return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 8cc05994c..a20db808e 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -71,10 +71,7 @@ template bool LR11x0Interface::init() RadioLibInterface::init(); - limitPower(); - - if (power > LR1110_MAX_POWER) // Clamp power to maximum defined level - power = LR1110_MAX_POWER; + limitPower(LR1110_MAX_POWER); if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 62d3c82bc..c5748a560 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -244,10 +244,13 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) p->decoded.request_id = to.id; } -std::vector MeshModule::GetMeshModulesWithUIFrames() +std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) { - std::vector modulesWithUIFrames; + + // Fill with nullptr up to startIndex + modulesWithUIFrames.resize(startIndex, nullptr); + if (modules) { for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index f08b8f49c..eda3f8881 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -75,7 +75,7 @@ class MeshModule */ static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); - static std::vector GetMeshModulesWithUIFrames(); + static std::vector GetMeshModulesWithUIFrames(int startIndex); static void observeUIEvents(Observer *observer); static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index 0c312fd1e..f8af81321 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -118,10 +118,10 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t } /* Attempt to find a packet from this queue. Return true if it was found. */ -bool MeshPacketQueue::find(NodeNum from, PacketId id) +bool MeshPacketQueue::find(const NodeNum from, const PacketId id) { for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); + const auto p = (*it); if (getFrom(p) == from && p->id == id) { return true; } diff --git a/src/mesh/MeshPacketQueue.h b/src/mesh/MeshPacketQueue.h index 6b2c3998a..1b338f9ed 100644 --- a/src/mesh/MeshPacketQueue.h +++ b/src/mesh/MeshPacketQueue.h @@ -39,5 +39,5 @@ class MeshPacketQueue meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true); /* Attempt to find a packet from this queue. Return true if it was found. */ - bool find(NodeNum from, PacketId id); + bool find(const NodeNum from, const PacketId id); }; \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 0a79f94a8..185ea0744 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -8,6 +8,7 @@ #include "Default.h" #include "FSCommon.h" #include "MeshRadio.h" +#include "MeshService.h" #include "NodeDB.h" #include "PacketHistory.h" #include "PowerFSM.h" @@ -287,6 +288,16 @@ 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); @@ -328,6 +339,38 @@ NodeDB::NodeDB() moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); } + // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched + if (config.device.node_info_broadcast_secs > MAX_INTERVAL) + config.device.node_info_broadcast_secs = MAX_INTERVAL; + if (config.position.position_broadcast_secs > MAX_INTERVAL) + config.position.position_broadcast_secs = MAX_INTERVAL; + if (config.position.gps_update_interval > MAX_INTERVAL) + config.position.gps_update_interval = MAX_INTERVAL; + if (config.position.gps_attempt_time > MAX_INTERVAL) + config.position.gps_attempt_time = MAX_INTERVAL; + if (config.position.position_flags > MAX_INTERVAL) + config.position.position_flags = MAX_INTERVAL; + if (config.position.rx_gpio > MAX_INTERVAL) + config.position.rx_gpio = MAX_INTERVAL; + if (config.position.tx_gpio > MAX_INTERVAL) + config.position.tx_gpio = MAX_INTERVAL; + if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL) + config.position.broadcast_smart_minimum_distance = MAX_INTERVAL; + if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL) + config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL; + if (config.position.gps_en_gpio > MAX_INTERVAL) + config.position.gps_en_gpio = MAX_INTERVAL; + if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; + if (moduleConfig.mqtt.has_map_report_settings && moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; @@ -342,6 +385,14 @@ NodeDB::NodeDB() config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; } +#if !HAS_TFT + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // On a device without MUI, this display mode makes no sense, and will break logic. + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + config.bluetooth.enabled = true; + } +#endif + if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) saveWhat |= SEGMENT_DEVICESTATE; if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) @@ -380,6 +431,7 @@ NodeDB::NodeDB() #endif } #endif + sortMeshDB(); saveToDisk(saveWhat); } @@ -499,6 +551,25 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; + +#if HAS_TFT // For the devices that support MUI, default to that + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; +#endif + +#ifdef USERPREFS_CONFIG_DEVICE_ROLE + // Restrict ROUTER*, LOST AND FOUND, and REPEATER roles for security reasons + if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_REPEATER, + meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { + LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else { + config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; + } +#else + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. +#endif + #ifdef USERPREFS_CONFIG_LORA_REGION config.lora.region = USERPREFS_CONFIG_LORA_REGION; #else @@ -671,6 +742,11 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) } #endif +#ifdef USERPREFS_CONFIG_DEVICE_ROLE + // Apply role-specific defaults when role is set via user preferences + installRoleDefaults(config.device.role); +#endif + initConfigIntervals(); } @@ -718,16 +794,16 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_buzzer = PIN_BUZZER; moduleConfig.external_notification.use_pwm = true; moduleConfig.external_notification.alert_message_buzzer = true; - moduleConfig.external_notification.nag_timeout = 60; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif -#if defined(RAK4630) || defined(RAK11310) +#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) // Default to RAK led pin 2 (blue) moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output = PIN_LED2; moduleConfig.external_notification.active = true; moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = 60; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #ifdef HAS_I2S @@ -736,10 +812,10 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.use_i2s_as_buzzer = true; moduleConfig.external_notification.alert_message_buzzer = true; #if HAS_TFT - if (moduleConfig.external_notification.nag_timeout == 60) + if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) moduleConfig.external_notification.nag_timeout = 0; #else - moduleConfig.external_notification.nag_timeout = 60; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #endif #ifdef NANO_G2_ULTRA @@ -757,15 +833,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; #endif -#ifdef BUTTON_SECONDARY_CANNEDMESSAGES - // Use a board's second built-in button as input source for canned messages - moduleConfig.canned_message.enabled = true; - moduleConfig.canned_message.inputbroker_pin_press = BUTTON_PIN_SECONDARY; - strcpy(moduleConfig.canned_message.allow_input_source, "scanAndSelect"); -#endif - moduleConfig.has_canned_message = true; - #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT moduleConfig.mqtt.enabled = true; #endif @@ -823,10 +891,12 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) { initConfigIntervals(); initModuleConfigIntervals(); + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; owner.has_is_unmessagable = true; owner.is_unmessagable = true; } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + moduleConfig.telemetry.device_update_interval = ONE_DAY; owner.has_is_unmessagable = true; owner.is_unmessagable = true; } else if (role == meshtastic_Config_DeviceConfig_Role_REPEATER) { @@ -837,6 +907,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; moduleConfig.telemetry.environment_measurement_enabled = true; moduleConfig.telemetry.environment_update_interval = 300; } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { @@ -854,6 +925,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; @@ -869,21 +941,25 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) moduleConfig.telemetry.device_update_interval = ONE_DAY; } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - config.device.node_info_broadcast_secs = UINT32_MAX; + config.device.node_info_broadcast_secs = MAX_INTERVAL; config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = UINT32_MAX; - moduleConfig.neighbor_info.update_interval = UINT32_MAX; - moduleConfig.telemetry.device_update_interval = UINT32_MAX; - moduleConfig.telemetry.environment_update_interval = UINT32_MAX; - moduleConfig.telemetry.air_quality_interval = UINT32_MAX; - moduleConfig.telemetry.health_update_interval = UINT32_MAX; + config.position.position_broadcast_secs = MAX_INTERVAL; + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; } } void NodeDB::initModuleConfigIntervals() { // Zero out telemetry intervals so that they coalesce to defaults in Default.h - moduleConfig.telemetry.device_update_interval = 0; +#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL + moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; +#else + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; +#endif moduleConfig.telemetry.environment_update_interval = 0; moduleConfig.telemetry.air_quality_interval = 0; moduleConfig.telemetry.power_update_interval = 0; @@ -949,7 +1025,10 @@ void NodeDB::cleanupMeshDB() meshNodes->at(i).user.public_key.size = 0; } } - meshNodes->at(newPos++) = meshNodes->at(i); + if (newPos != i) + meshNodes->at(newPos++) = meshNodes->at(i); + else + newPos++; } else { removed++; } @@ -1036,8 +1115,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t if (f) { LOG_INFO("Load %s", filename); pb_istream_t stream = {&readcb, &f, protoSize}; - - memset(dest_struct, 0, objSize); + if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object + memset(dest_struct, 0, objSize); if (!pb_decode(&stream, fields, dest_struct)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); state = LoadFileResult::DECODE_FAILED; @@ -1246,6 +1325,13 @@ void NodeDB::loadFromDisk() saveToDisk(SEGMENT_MODULECONFIG); } +#if ARCH_PORTDUINO + // set any config overrides + if (settingsMap[has_configDisplayMode]) { + config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)settingsMap[configDisplayMode]; + } + +#endif } /** Save a protobuf from a file, return true for success */ @@ -1514,15 +1600,26 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) return; } info->num = contact.node_num; - info->last_heard = getValidTime(RTCQualityNTP); info->has_user = true; info->user = TypeConversions::ConvertToUserLite(contact.user); - info->is_favorite = true; - // Mark the node's key as manually verified to indicate trustworthiness. - info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - updateGUIforNode = info; - powerFSM.trigger(EVENT_NODEDB_UPDATED); - notifyObservers(true); // Force an update whether or not our node counts have changed + if (contact.should_ignore) { + // If should_ignore is set, + // we need to clear the public key and other cruft, in addition to setting the node as ignored + info->is_ignored = true; + info->has_device_metrics = false; + info->has_position = false; + info->user.public_key.size = 0; + info->user.public_key.bytes[0] = 0; + } else { + info->last_heard = getValidTime(RTCQualityNTP); + info->is_favorite = true; + info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + // Mark the node's key as manually verified to indicate trustworthiness. + updateGUIforNode = info; + // powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired + sortMeshDB(); + notifyObservers(true); // Force an update whether or not our node counts have changed + } saveNodeDatabaseToDisk(); } @@ -1536,8 +1633,22 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde } #if !(MESHTASTIC_EXCLUDE_PKI) - if (p.public_key.size > 0) { + if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { 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 (info->user.public_key.size > 0) { // if we have a key for this user already, don't overwrite with a new one LOG_INFO("Public Key set for node, not updating!"); @@ -1565,7 +1676,6 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde if (changed) { updateGUIforNode = info; - powerFSM.trigger(EVENT_NODEDB_UPDATED); notifyObservers(true); // Force an update whether or not our node counts have changed // We just changed something about a User, @@ -1611,6 +1721,52 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) info->has_hops_away = true; info->hops_away = mp.hop_start - mp.hop_limit; } + sortMeshDB(); + } +} + +void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) +{ + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + if (lite && lite->is_favorite != is_favorite) { + lite->is_favorite = is_favorite; + sortMeshDB(); + saveNodeDatabaseToDisk(); + } +} + +void NodeDB::pause_sort(bool paused) +{ + sortingIsPaused = paused; +} + +void NodeDB::sortMeshDB() +{ + if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { + lastSort = millis(); + bool changed = true; + while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing + changed = false; + for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 + if (meshNodes->at(i - 1).num == getNodeNum()) { + // noop + } else if (meshNodes->at(i).num == + getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there + // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { + // noop + } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } + } + } + LOG_INFO("Sort took %u milliseconds", millis() - lastSort); } } @@ -1712,6 +1868,39 @@ 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) +{ + 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; + } + } + return false; +} + bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { bool success = false; @@ -1803,10 +1992,6 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, /// Record an error that should be reported via analytics void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) { - // Print error to screen and serial port - String lcd = String("Critical error ") + code + "!\n"; - if (screen) - screen->print(lcd.c_str()); if (filename) { LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); } else { @@ -1822,4 +2007,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co LOG_ERROR("A critical failure occurred, portduino is exiting"); exit(2); #endif -} \ No newline at end of file +} diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 16159d380..845f42c76 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -17,6 +17,70 @@ #include "PortduinoGlue.h" #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."; +#endif /* DeviceState versions used to be defined in the .proto file but really only this function cares. So changed to a #define here. @@ -87,6 +151,9 @@ class NodeDB Observable newStatus; pb_size_t numMeshNodes; + bool keyIsLowEntropy = false; + bool hasWarned = false; + /// don't do mesh based algorithm for node id assignment (initially) /// instead just store in flash - possibly even in the initial alpha release do this hack NodeDB(); @@ -124,6 +191,16 @@ class NodeDB */ bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); + /* + * Sets a node either favorite or unfavorite + */ + void set_favorite(bool is_favorite, uint32_t nodeId); + + /** + * Other functions like the node picker can request a pause in the node sorting + */ + void pause_sort(bool paused); + /// @return our node number NodeNum getNodeNum() { return myNodeInfo.my_node_num; } @@ -141,9 +218,6 @@ class NodeDB their denial?) */ - /// pick a provisional nodenum we hope no one is using - void pickNewNodeNum(); - // get channel channel index we heard a nodeNum on, defaults to 0 if not found uint8_t getMeshNodeChannel(NodeNum n); @@ -205,16 +279,12 @@ class NodeDB bool hasValidPosition(const meshtastic_NodeInfoLite *n); + bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t keyToTest); + bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); - private: - uint32_t lastNodeDbSave = 0; // when we last saved our db to flash - uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually - /// Find a node in our DB, create an empty NodeInfoLite if missing - meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); - /// Notify observers of changes to the DB void notifyObservers(bool forceUpdate = false) { @@ -223,6 +293,22 @@ class NodeDB newStatus.notifyObservers(&status); } + private: + bool duplicateWarned = false; + uint32_t lastNodeDbSave = 0; // when we last saved our db to flash + uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually + uint32_t lastSort = 0; // When last sorted the nodeDB + /// Find a node in our DB, create an empty NodeInfoLite if missing + meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); + + /* + * Internal boolean to track sorting paused + */ + bool sortingIsPaused = false; + + /// pick a provisional nodenum we hope no one is using + void pickNewNodeNum(); + /// read our db from flash void loadFromDisk(); @@ -240,6 +326,7 @@ class NodeDB bool saveChannelsToDisk(); bool saveDeviceStateToDisk(); bool saveNodeDatabaseToDisk(); + void sortMeshDB(); }; extern NodeDB *nodeDB; diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 15fa9cdcd..8cac31a3e 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -7,152 +7,382 @@ #endif #include "Throttle.h" -PacketHistory::PacketHistory() +#define PACKETHISTORY_MAX \ + max((u_int32_t)(MAX_NUM_NODES * 2.0), \ + (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 + +#define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min + +#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging +#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots + +PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members { - recentPackets.reserve(MAX_NUM_NODES); // Prealloc the worst case # of records - to prevent heap fragmentation - // setup our periodic task + if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense + LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); + size = PACKETHISTORY_MAX; // Use default size if invalid + } + + // Allocate memory for the recent packets array + recentPacketsCapacity = size; + recentPackets = new PacketRecord[recentPacketsCapacity]; + if (!recentPackets) { // No logging here, console/log probably uninitialized yet. + LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, + sizeof(PacketRecord) * recentPacketsCapacity); + recentPacketsCapacity = 0; // mark allocation fail + return; // return early + } + + // Initialize the recent packets array to zero + memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); } -/** - * Update recentBroadcasts and return true if we have already seen this packet - */ +PacketHistory::~PacketHistory() +{ + recentPacketsCapacity = 0; + delete[] recentPackets; + recentPackets = NULL; +} + +/** Update recentPackets and return true if we have already seen this packet */ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop) { + if (!initOk()) { + LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); + return false; + } + if (p->id == 0) { - LOG_DEBUG("Ignore message with zero id"); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); +#endif return false; // Not a floodable message ID, so we don't care } PacketRecord r; + memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero + + // Save basic info from checked packet r.id = p->id; - r.sender = getFrom(p); - r.rxTimeMsec = millis(); + r.sender = getFrom(p); // If 0 then use our ID r.next_hop = p->next_hop; r.relayed_by[0] = p->relay_node; - // LOG_INFO("Add relayed_by 0x%x for id=0x%x", p->relay_node, r.id); - auto found = recentPackets.find(r); - bool seenRecently = (found != recentPackets.end()); // found not equal to .end() means packet was seen recently + r.rxTimeMsec = millis(); // + if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special + r.rxTimeMsec = 1; - if (seenRecently && - !Throttle::isWithinTimespanMs(found->rxTimeMsec, FLOOD_EXPIRE_TIME)) { // Check whether found packet has already expired - recentPackets.erase(found); // Erase and pretend packet has not been seen recently - found = recentPackets.end(); - seenRecently = false; - } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d wWNH?%d", + r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, + weWereNextHop ? *weWereNextHop : -1); +#endif + + PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array + bool seenRecently = (found != NULL); // If found -> the packet was seen recently if (seenRecently) { - LOG_DEBUG("Found existing packet record for fr=0x%x,to=0x%x,id=0x%x", p->from, p->to, p->id); - uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); // Get our relay ID from our node number + if (wasFallback) { // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already // before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle // it now. if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && - found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, found) && - !wasRelayer(ourRelayID, found) && !wasRelayer(found->next_hop, found)) { + found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && + !wasRelayer(ourRelayID, *found) && + !wasRelayer( + found->next_hop, + *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", + p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif *wasFallback = true; + } else { + // debug log only +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", + p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif } } // Check if we were the next hop for this packet if (weWereNextHop) { - *weWereNextHop = found->next_hop == ourRelayID; + *weWereNextHop = (found->next_hop == ourRelayID); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", + p->from, p->id, p->next_hop, p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); +#endif } } if (withUpdate) { - if (found != recentPackets.end()) { // delete existing to updated timestamp and relayed_by (re-insert) + if (found != NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", + found->sender, found->id, found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], + millis() - found->rxTimeMsec); +#endif + // Add the existing relayed_by to the new record - for (uint8_t i = 0; i < NUM_RELAYERS - 1; i++) { - if (found->relayed_by[i]) + for (uint8_t i = 0; i < (NUM_RELAYERS - 1); i++) { + if (found->relayed_by[i] != 0) r.relayed_by[i + 1] = found->relayed_by[i]; } r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) - recentPackets.erase(found); // as unsorted_set::iterator is const (can't update - so re-insert..) +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, + r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); +#endif + // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this } - recentPackets.insert(r); - LOG_DEBUG("Add packet record fr=0x%x, id=0x%x", p->from, p->id); - } - - // Capacity is reerved, so only purge expired packets if recentPackets fills past 90% capacity - // Expiry is normally dealt with after having searched/found a packet (above) - if (recentPackets.size() > (MAX_NUM_NODES * 0.9)) { - clearExpiredRecentPackets(); + insert(r); // Insert or update the packet record in the history } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " + "found?%s seenRecently?%s wUpd?%s", + r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, + found ? "YES" : "NO ", seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); +#endif return seenRecently; } -/** - * Iterate through all recent packets, and remove all older than FLOOD_EXPIRE_TIME - */ -void PacketHistory::clearExpiredRecentPackets() +/** Find a packet record in history. + * @return pointer to PacketRecord if found, NULL if not found */ +PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) { - LOG_DEBUG("recentPackets size=%ld", recentPackets.size()); + if (sender == 0 || id == 0) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); +#endif + return NULL; + } - for (auto it = recentPackets.begin(); it != recentPackets.end();) { - if (!Throttle::isWithinTimespanMs(it->rxTimeMsec, FLOOD_EXPIRE_TIME)) { - it = recentPackets.erase(it); // erase returns iterator pointing to element immediately following the one erased - } else { - ++it; + PacketRecord *it = NULL; + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == id && it->sender == sender) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, + it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), + it - recentPackets, recentPacketsCapacity); +#endif + // only the first match is returned, so be careful not to create duplicate entries + return it; // Return pointer to the found record } } - LOG_DEBUG("recentPackets size=%ld (after clearing expired packets)", recentPackets.size()); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); +#endif + return NULL; // Not found +} + +/** Insert/Replace oldest PacketRecord in recentPackets. */ +void PacketHistory::insert(PacketRecord &r) +{ + uint32_t now_millis = millis(); // Should not jump with time changes + uint32_t OldtrxTimeMsec = 0; + PacketRecord *tu = NULL; // Will insert here. + PacketRecord *it = NULL; + + // Find a free, matching or oldest used slot in the recentPackets array + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty + tu = it; // Remember the free slot +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); +#endif + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert + tu = it; // Remember the matching slot + OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); +#endif + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else { + if (it->rxTimeMsec == 0) { + LOG_WARN( + "Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!", + it->sender, it->id, it - recentPackets, recentPacketsCapacity); + } + if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly + OldtrxTimeMsec = now_millis - it->rxTimeMsec; + tu = it; // remember the oldest packet +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); +#endif + } + // keep looking for oldest till entire array is checked + } + } + + if (tu == NULL) { + LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx + // assert(false); // This should never happen, we should always have at least one packet to clear + return; // Return early if we can't update the history + } + +#if VERBOSE_PACKET_HISTORY + if (tu->id == 0 && tu->sender == 0) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); + } else if (tu->id == r.id && tu->sender == r.sender) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); + } else { + LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); + } +#endif + + // If we are reusing a slot, we should warn if the packet is too recent +#if RECENT_WARN_AGE > 0 + if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { + if (!(tu->id == r.id && tu->sender == r.sender)) { +#if VERBOSE_PACKET_HISTORY + LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, + RECENT_WARN_AGE / 1000); +#endif + } else { + // debug only +#if VERBOSE_PACKET_HISTORY + LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", + OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000); +#endif + } + } + +#if PACKET_HISTORY_TRACE_AGING + if (tu->rxTimeMsec != 0) { + LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., + (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); + } else { + LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); + } +#endif + +#endif + +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", + tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], + tu->relayed_by[2], tu->rxTimeMsec); +#endif + + if (r.rxTimeMsec == 0) { +#if VERBOSE_PACKET_HISTORY + LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); +#endif + return; // Return early if we can't update the history + } + + *tu = r; // store the packet + +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", + tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], + tu->relayed_by[2], tu->rxTimeMsec); +#endif } /* Check if a certain node was a relayer of a packet in the history given an ID and sender * @return true if node was indeed a relayer, false if not */ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { - if (relayer == 0) - return false; - - PacketRecord r = {.sender = sender, .id = id, .rxTimeMsec = 0, .next_hop = 0}; - auto found = recentPackets.find(r); - - if (found == recentPackets.end()) { + if (!initOk()) { + LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); return false; } - return wasRelayer(relayer, found); + if (relayer == 0) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); +#endif + return false; + } + + PacketRecord *found = find(sender, id); + + if (found == NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); +#endif + return false; + } + +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", + found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], + found->relayed_by[2], relayer); +#endif + return wasRelayer(relayer, *found); } /* 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, std::unordered_set::iterator r) +bool PacketHistory::wasRelayer(const uint8_t relayer, PacketRecord &r) { for (uint8_t i = 0; i < NUM_RELAYERS; i++) { - if (r->relayed_by[i] == relayer) { + if (r.relayed_by[i] == relayer) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id, + r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); +#endif return true; } } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], + r.relayed_by[1], r.relayed_by[2], relayer); +#endif return false; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { - PacketRecord r = {.sender = sender, .id = id, .rxTimeMsec = 0, .next_hop = 0}; - auto found = recentPackets.find(r); - - if (found == recentPackets.end()) { + if (!initOk()) { + LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); return; } - // Make a copy of the found record - r.next_hop = found->next_hop; - r.rxTimeMsec = found->rxTimeMsec; - // Only add the relayers that are not the one we want to remove - uint8_t j = 0; - for (uint8_t i = 0; i < NUM_RELAYERS; i++) { - if (found->relayed_by[i] != relayer) { - r.relayed_by[j] = found->relayed_by[i]; - j++; - } + PacketRecord *found = find(sender, id); + if (found == NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); +#endif + return; // Nothing to remove } - recentPackets.erase(found); - recentPackets.insert(r); -} \ No newline at end of file +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, + found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); +#endif + + // nexthop and rxTimeMsec too stay in found entry + + uint8_t j = 0; + uint8_t i = 0; + for (; i < NUM_RELAYERS; i++) { + if (found->relayed_by[i] != relayer) { + found->relayed_by[j] = found->relayed_by[i]; + j++; + } else + found->relayed_by[i] = 0; + } + for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array + found->relayed_by[j] = 0; + } + +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, + found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); +#endif +} diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index db7698f5b..d06c9bd2f 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -1,49 +1,47 @@ #pragma once #include "NodeDB.h" -#include - -/// We clear our old flood record 10 minutes after we see the last of it -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -#define FLOOD_EXPIRE_TIME (5 * 1000L) // Don't allow too many packets to accumulate when fuzzing. -#else -#define FLOOD_EXPIRE_TIME (10 * 60 * 1000L) -#endif #define NUM_RELAYERS \ 3 // Number of relayer we keep track of. Use 3 to be efficient with memory alignment of PacketRecord to 16 bytes -/** - * A record of a recent message broadcast - */ -struct PacketRecord { - NodeNum sender; - PacketId id; - uint32_t rxTimeMsec; // Unix time in msecs - the time we received it - uint8_t next_hop; // The next hop asked for this packet - uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet - - bool operator==(const PacketRecord &p) const { return sender == p.sender && id == p.id; } -}; - -class PacketRecordHashFunction -{ - public: - size_t operator()(const PacketRecord &p) const { return (std::hash()(p.sender)) ^ (std::hash()(p.id)); } -}; - /** * This is a mixin that adds a record of past packets we have seen */ class PacketHistory { private: - std::unordered_set recentPackets; + struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. + NodeNum sender; + PacketId id; + uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty + uint8_t next_hop; // The next hop asked for this packet + uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet + }; // 4B + 4B + 4B + 1B + 3B = 16B - void clearExpiredRecentPackets(); // clear all recentPackets older than FLOOD_EXPIRE_TIME + uint32_t recentPacketsCapacity = + 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. + PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. + /** Find a packet record in history. + * @param sender NodeNum + * @param id PacketId + * @return pointer to PacketRecord if found, NULL if not found */ + PacketRecord *find(NodeNum sender, PacketId id); + + /** 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 + + /* 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); + + PacketHistory(const PacketHistory &); // non construction-copyable + PacketHistory &operator=(const PacketHistory &); // non copyable public: - PacketHistory(); + explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX + ~PacketHistory(); /** * Update recentBroadcasts and return true if we have already seen this packet @@ -59,10 +57,9 @@ class PacketHistory * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); - /* 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, std::unordered_set::iterator r); - // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); -}; \ No newline at end of file + + // To check if the PacketHistory was initialized correctly by constructor + bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } +}; diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index e2acd8463..287de38fa 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -670,7 +670,8 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; - } else if (IS_ONE_OF(meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_ALERT_APP) && + } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, + meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP) && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset @@ -680,6 +681,13 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) // FIXME: Figure out why this continues to happen // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { + 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"); + return false; } lastPortNumToRadio[p.decoded.portnum] = millis(); service->handleToRadio(p); diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 943a79a5f..97f21fc34 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -122,10 +122,7 @@ bool RF95Interface::init() power = dacDbValues.db; #endif - limitPower(); - - if (power > RF95_MAX_POWER) // This chip has lower power limits than some - power = RF95_MAX_POWER; + limitPower(RF95_MAX_POWER); iface = lora = new RadioLibRF95(&module); diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 06398e6c3..faa67a1c2 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -12,6 +12,12 @@ #include #include +// Calculate 2^n without calling pow() +uint32_t pow_of_2(uint32_t n) +{ + return 1 << n; +} + #define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ { \ meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \ @@ -64,6 +70,13 @@ const RegionInfo regions[] = { */ RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false), + /* + 433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions + AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence + NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100 + */ + RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false), + /* https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf @@ -148,6 +161,14 @@ const RegionInfo regions[] = { RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false), RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false), + /* + Kazakhstan + 433.075 - 434.775 MHz <10 mW EIRP, Low Powered Devices (LPD) + 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields + https://github.com/meshtastic/firmware/issues/7204 + */ + 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), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ @@ -239,7 +260,7 @@ uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) float channelUtil = airTime->channelUtilizationPercent(); uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); // Assuming we pick max. of CWsize and there will be a client with SNR at half the range - return 2 * packetAirtime + (pow(2, CWsize) + 2 * CWmax + pow(2, int((CWmax + CWmin) / 2))) * slotTimeMsec + + return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec + PROCESSING_TIME_MSEC; } @@ -252,7 +273,7 @@ uint32_t RadioInterface::getTxDelayMsec() float channelUtil = airTime->channelUtilizationPercent(); uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); // LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize); - return random(0, pow(2, CWsize)) * slotTimeMsec; + return random(0, pow_of_2(CWsize)) * slotTimeMsec; } /** The CW size to use when calculating SNR_based delays */ @@ -272,7 +293,7 @@ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) { uint8_t CWsize = getCWsize(snr); // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) - return (2 * CWmax * slotTimeMsec) + pow(2, CWsize) * slotTimeMsec; + return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } /** The delay to use when we want to flood a message */ @@ -289,7 +310,7 @@ uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); } else { // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) - delay = (2 * CWmax * slotTimeMsec) + random(0, pow(2, CWsize)) * slotTimeMsec; + delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); } @@ -589,7 +610,7 @@ void RadioInterface::applyModemConfig() uint32_t RadioInterface::computeSlotTimeMsec() { float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds - float symbolTime = pow(2, sf) / bw; // in milliseconds + float symbolTime = pow_of_2(sf) / bw; // in milliseconds if (myRegion->wideLora) { // CAD duration derived from AN1200.22 of SX1280 @@ -604,7 +625,7 @@ uint32_t RadioInterface::computeSlotTimeMsec() * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it */ -void RadioInterface::limitPower() +void RadioInterface::limitPower(int8_t loraMaxPower) { uint8_t maxPower = 255; // No limit @@ -621,6 +642,9 @@ void RadioInterface::limitPower() power -= TX_GAIN_LORA; } + if (power > loraMaxPower) // Clamp power to maximum defined level + power = loraMaxPower; + LOG_INFO("Final Tx power: %d dBm", power); } @@ -661,4 +685,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) sendingPacket = p; return p->encrypted.size + sizeof(PacketHeader); -} \ No newline at end of file +} diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 68ae09635..c9e71cfa8 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -230,7 +230,7 @@ class RadioInterface * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it */ - void limitPower(); + void limitPower(int8_t MAX_POWER); /** * Save the frequency we selected for later reuse. diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index fef29388e..02968513c 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -548,6 +548,20 @@ 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 diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index 3c8bf89c3..d7bc37466 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -25,10 +25,7 @@ bool STM32WLE5JCInterface::init() lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); - limitPower(); - - if (power > STM32WLx_MAX_POWER) // This chip has lower power limits than some - power = STM32WLx_MAX_POWER; + limitPower(STM32WLx_MAX_POWER); int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index e5ecd9302..729c1abc6 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -69,10 +69,7 @@ template bool SX126xInterface::init() RadioLibInterface::init(); - limitPower(); - - if (power > SX126X_MAX_POWER) // Clamp power to maximum defined level - power = SX126X_MAX_POWER; + limitPower(SX126X_MAX_POWER); int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); // \todo Display actual typename of the adapter, not just `SX126x` diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 2b17543fc..866426872 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -62,10 +62,7 @@ template bool SX128xInterface::init() RadioLibInterface::init(); - limitPower(); - - if (power > SX128X_MAX_POWER) // This chip has lower power limits than some - power = SX128X_MAX_POWER; + limitPower(SX128X_MAX_POWER); preambleLength = 12; // 12 is the default for this chip, 32 does not RX at all diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp index a650ba2fc..420d80e9a 100644 --- a/src/mesh/aes-ccm.cpp +++ b/src/mesh/aes-ccm.cpp @@ -10,6 +10,32 @@ #include "aes-ccm.h" #if !MESHTASTIC_EXCLUDE_PKI +/** + * Constant-time comparison of two byte arrays + * + * @param a First byte array to compare + * @param b Second byte array to compare + * @param len Number of bytes to compare + * @return 0 if arrays are equal, -1 if different or if inputs are invalid + */ +static int constant_time_compare(const void *a_, const void *b_, size_t len) +{ + /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ + const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; + const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; + if (len == 0) + return 0; + if (a == NULL || b == NULL) + return -1; + size_t i; + volatile uint8_t d = 0U; + for (i = 0U; i < len; i++) { + d |= (a[i] ^ b[i]); + } + /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ + return (1 & ((d - 1) >> 8)) - 1; +} + static void WPA_PUT_BE16(uint8_t *a, uint16_t val) { a[0] = val >> 8; @@ -146,7 +172,7 @@ bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t aes_ccm_encr(L, crypt, crypt_len, plain, a); aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); aes_ccm_auth(plain, crypt_len, x); - if (memcmp(x, t, M) != 0) { // FIXME make const comp + if (constant_time_compare(x, t, M) != 0) { return false; } return true; diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 70c6e3fe4..9c92a6c27 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -64,7 +64,9 @@ static int32_t reconnectETH() } #if !MESHTASTIC_EXCLUDE_SOCKETAPI - initApiServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif ethStartupComplete = true; diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index a9c82f7c0..4c4d0e3d1 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -9,6 +9,9 @@ PB_BIND(meshtastic_AdminMessage, meshtastic_AdminMessage, 2) +PB_BIND(meshtastic_AdminMessage_InputEvent, meshtastic_AdminMessage_InputEvent, AUTO) + + PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO) diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index 2a5fd78b0..071640b0d 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -91,6 +91,18 @@ typedef enum _meshtastic_KeyVerificationAdmin_MessageType { } meshtastic_KeyVerificationAdmin_MessageType; /* Struct definitions */ +/* Input event message to be sent to the node. */ +typedef struct _meshtastic_AdminMessage_InputEvent { + /* The input event code */ + uint8_t event_code; + /* Keyboard character code */ + uint8_t kb_char; + /* The touch X coordinate */ + uint16_t touch_x; + /* The touch Y coordinate */ + uint16_t touch_y; +} meshtastic_AdminMessage_InputEvent; + /* Parameters for setting up Meshtastic for ameteur radio usage */ typedef struct _meshtastic_HamParameters { /* Amateur radio call sign, eg. KD2ABC */ @@ -118,6 +130,8 @@ typedef struct _meshtastic_SharedContact { /* The User of the contact */ bool has_user; meshtastic_User user; + /* Add this contact to the blocked / ignored list */ + bool should_ignore; } meshtastic_SharedContact; /* This message is used by a client to initiate or complete a key verification */ @@ -191,6 +205,9 @@ typedef struct _meshtastic_AdminMessage { meshtastic_AdminMessage_BackupLocation restore_preferences; /* Remove backups of the node's preferences */ meshtastic_AdminMessage_BackupLocation remove_backup_preferences; + /* Send an input event to the node. + This is used to trigger physical input events like button presses, touch events, etc. */ + meshtastic_AdminMessage_InputEvent send_input_event; /* Set the owner for this node */ meshtastic_User set_owner; /* Set channels (using the new API). @@ -293,22 +310,29 @@ extern "C" { + #define meshtastic_KeyVerificationAdmin_message_type_ENUMTYPE meshtastic_KeyVerificationAdmin_MessageType /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} +#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} #define meshtastic_HamParameters_init_default {"", 0, 0, ""} #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} -#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default} +#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} +#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_HamParameters_init_zero {"", 0, 0, ""} #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} -#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero} +#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} /* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_AdminMessage_InputEvent_event_code_tag 1 +#define meshtastic_AdminMessage_InputEvent_kb_char_tag 2 +#define meshtastic_AdminMessage_InputEvent_touch_x_tag 3 +#define meshtastic_AdminMessage_InputEvent_touch_y_tag 4 #define meshtastic_HamParameters_call_sign_tag 1 #define meshtastic_HamParameters_tx_power_tag 2 #define meshtastic_HamParameters_frequency_tag 3 @@ -316,6 +340,7 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_node_remote_hardware_pins_tag 1 #define meshtastic_SharedContact_node_num_tag 1 #define meshtastic_SharedContact_user_tag 2 +#define meshtastic_SharedContact_should_ignore_tag 3 #define meshtastic_KeyVerificationAdmin_message_type_tag 1 #define meshtastic_KeyVerificationAdmin_remote_nodenum_tag 2 #define meshtastic_KeyVerificationAdmin_nonce_tag 3 @@ -345,6 +370,7 @@ extern "C" { #define meshtastic_AdminMessage_backup_preferences_tag 24 #define meshtastic_AdminMessage_restore_preferences_tag 25 #define meshtastic_AdminMessage_remove_backup_preferences_tag 26 +#define meshtastic_AdminMessage_send_input_event_tag 27 #define meshtastic_AdminMessage_set_owner_tag 32 #define meshtastic_AdminMessage_set_channel_tag 33 #define meshtastic_AdminMessage_set_config_tag 34 @@ -402,6 +428,7 @@ X(a, STATIC, ONEOF, UINT32, (payload_variant,set_scale,set_scale), 23) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,backup_preferences,backup_preferences), 24) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,restore_preferences,restore_preferences), 25) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,remove_backup_preferences,remove_backup_preferences), 26) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,send_input_event,send_input_event), 27) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_owner,set_owner), 32) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_channel,set_channel), 33) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_config,set_config), 34) \ @@ -441,6 +468,7 @@ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #define meshtastic_AdminMessage_payload_variant_get_device_connection_status_response_MSGTYPE meshtastic_DeviceConnectionStatus #define meshtastic_AdminMessage_payload_variant_set_ham_mode_MSGTYPE meshtastic_HamParameters #define meshtastic_AdminMessage_payload_variant_get_node_remote_hardware_pins_response_MSGTYPE meshtastic_NodeRemoteHardwarePinsResponse +#define meshtastic_AdminMessage_payload_variant_send_input_event_MSGTYPE meshtastic_AdminMessage_InputEvent #define meshtastic_AdminMessage_payload_variant_set_owner_MSGTYPE meshtastic_User #define meshtastic_AdminMessage_payload_variant_set_channel_MSGTYPE meshtastic_Channel #define meshtastic_AdminMessage_payload_variant_set_config_MSGTYPE meshtastic_Config @@ -451,6 +479,14 @@ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #define meshtastic_AdminMessage_payload_variant_add_contact_MSGTYPE meshtastic_SharedContact #define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin +#define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, event_code, 1) \ +X(a, STATIC, SINGULAR, UINT32, kb_char, 2) \ +X(a, STATIC, SINGULAR, UINT32, touch_x, 3) \ +X(a, STATIC, SINGULAR, UINT32, touch_y, 4) +#define meshtastic_AdminMessage_InputEvent_CALLBACK NULL +#define meshtastic_AdminMessage_InputEvent_DEFAULT NULL + #define meshtastic_HamParameters_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, call_sign, 1) \ X(a, STATIC, SINGULAR, INT32, tx_power, 2) \ @@ -467,7 +503,8 @@ X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 1) #define meshtastic_SharedContact_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, node_num, 1) \ -X(a, STATIC, OPTIONAL, MESSAGE, user, 2) +X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ +X(a, STATIC, SINGULAR, BOOL, should_ignore, 3) #define meshtastic_SharedContact_CALLBACK NULL #define meshtastic_SharedContact_DEFAULT NULL #define meshtastic_SharedContact_user_MSGTYPE meshtastic_User @@ -481,6 +518,7 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_KeyVerificationAdmin_DEFAULT NULL extern const pb_msgdesc_t meshtastic_AdminMessage_msg; +extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_HamParameters_msg; extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; extern const pb_msgdesc_t meshtastic_SharedContact_msg; @@ -488,6 +526,7 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg +#define meshtastic_AdminMessage_InputEvent_fields &meshtastic_AdminMessage_InputEvent_msg #define meshtastic_HamParameters_fields &meshtastic_HamParameters_msg #define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg #define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg @@ -495,11 +534,12 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size +#define meshtastic_AdminMessage_InputEvent_size 14 #define meshtastic_AdminMessage_size 511 #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 -#define meshtastic_SharedContact_size 123 +#define meshtastic_SharedContact_size 125 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp index 5512584a7..52a591f33 100644 --- a/src/mesh/generated/meshtastic/config.pb.cpp +++ b/src/mesh/generated/meshtastic/config.pb.cpp @@ -65,6 +65,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig, + + diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 9d7a96dc8..f28daadbd 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -88,6 +88,23 @@ typedef enum _meshtastic_Config_DeviceConfig_RebroadcastMode { meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY = 5 } meshtastic_Config_DeviceConfig_RebroadcastMode; +/* Defines buzzer behavior for audio feedback */ +typedef enum _meshtastic_Config_DeviceConfig_BuzzerMode { + /* Default behavior. + Buzzer is enabled for all audio feedback including button presses and alerts. */ + meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED = 0, + /* Disabled. + All buzzer audio feedback is disabled. */ + meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED = 1, + /* Notifications Only. + Buzzer is enabled only for notifications and alerts, but not for button presses. + External notification config determines the specifics of the notification behavior. */ + 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; + /* Bit field of boolean configuration options, indicating which optional fields to include when assembling POSITION messages. Longitude, latitude, altitude, speed, heading, and DOP @@ -266,7 +283,13 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { /* Philippines 868mhz */ meshtastic_Config_LoRaConfig_RegionCode_PH_868 = 20, /* Philippines 915mhz */ - meshtastic_Config_LoRaConfig_RegionCode_PH_915 = 21 + meshtastic_Config_LoRaConfig_RegionCode_PH_915 = 21, + /* Australia / New Zealand 433MHz */ + meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 = 22, + /* Kazakhstan 433MHz */ + meshtastic_Config_LoRaConfig_RegionCode_KZ_433 = 23, + /* Kazakhstan 863MHz */ + meshtastic_Config_LoRaConfig_RegionCode_KZ_863 = 24 } meshtastic_Config_LoRaConfig_RegionCode; /* Standard predefined channel settings @@ -335,6 +358,9 @@ typedef struct _meshtastic_Config_DeviceConfig { char tzdef[65]; /* If true, disable the default blinking LED (LED_PIN) behavior on the device */ bool led_heartbeat_disabled; + /* Controls buzzer behavior for audio feedback + Defaults to ENABLED */ + meshtastic_Config_DeviceConfig_BuzzerMode buzzer_mode; } meshtastic_Config_DeviceConfig; /* Position Config */ @@ -618,6 +644,10 @@ extern "C" { #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY #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_PositionConfig_PositionFlags_MIN meshtastic_Config_PositionConfig_PositionFlags_UNSET #define _meshtastic_Config_PositionConfig_PositionFlags_MAX meshtastic_Config_PositionConfig_PositionFlags_SPEED #define _meshtastic_Config_PositionConfig_PositionFlags_ARRAYSIZE ((meshtastic_Config_PositionConfig_PositionFlags)(meshtastic_Config_PositionConfig_PositionFlags_SPEED+1)) @@ -655,8 +685,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_PH_915 -#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_PH_915+1)) +#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_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO @@ -669,6 +699,7 @@ extern "C" { #define meshtastic_Config_DeviceConfig_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role #define meshtastic_Config_DeviceConfig_rebroadcast_mode_ENUMTYPE meshtastic_Config_DeviceConfig_RebroadcastMode +#define meshtastic_Config_DeviceConfig_buzzer_mode_ENUMTYPE meshtastic_Config_DeviceConfig_BuzzerMode #define meshtastic_Config_PositionConfig_gps_mode_ENUMTYPE meshtastic_Config_PositionConfig_GpsMode @@ -692,7 +723,7 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_Config_init_default {0, {meshtastic_Config_DeviceConfig_init_default}} -#define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0} +#define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #define meshtastic_Config_PositionConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0} @@ -703,7 +734,7 @@ extern "C" { #define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} #define meshtastic_Config_SessionkeyConfig_init_default {0} #define meshtastic_Config_init_zero {0, {meshtastic_Config_DeviceConfig_init_zero}} -#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0} +#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #define meshtastic_Config_PositionConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0} @@ -726,6 +757,7 @@ extern "C" { #define meshtastic_Config_DeviceConfig_disable_triple_click_tag 10 #define meshtastic_Config_DeviceConfig_tzdef_tag 11 #define meshtastic_Config_DeviceConfig_led_heartbeat_disabled_tag 12 +#define meshtastic_Config_DeviceConfig_buzzer_mode_tag 13 #define meshtastic_Config_PositionConfig_position_broadcast_secs_tag 1 #define meshtastic_Config_PositionConfig_position_broadcast_smart_enabled_tag 2 #define meshtastic_Config_PositionConfig_fixed_position_tag 3 @@ -849,7 +881,8 @@ X(a, STATIC, SINGULAR, BOOL, double_tap_as_button_press, 8) \ X(a, STATIC, SINGULAR, BOOL, is_managed, 9) \ X(a, STATIC, SINGULAR, BOOL, disable_triple_click, 10) \ X(a, STATIC, SINGULAR, STRING, tzdef, 11) \ -X(a, STATIC, SINGULAR, BOOL, led_heartbeat_disabled, 12) +X(a, STATIC, SINGULAR, BOOL, led_heartbeat_disabled, 12) \ +X(a, STATIC, SINGULAR, UENUM, buzzer_mode, 13) #define meshtastic_Config_DeviceConfig_CALLBACK NULL #define meshtastic_Config_DeviceConfig_DEFAULT NULL @@ -995,7 +1028,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size #define meshtastic_Config_BluetoothConfig_size 10 -#define meshtastic_Config_DeviceConfig_size 98 +#define meshtastic_Config_DeviceConfig_size 100 #define meshtastic_Config_DisplayConfig_size 32 #define meshtastic_Config_LoRaConfig_size 85 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 diff --git a/src/mesh/generated/meshtastic/device_ui.pb.cpp b/src/mesh/generated/meshtastic/device_ui.pb.cpp index 4bb3cc66c..2fc8d9461 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.cpp +++ b/src/mesh/generated/meshtastic/device_ui.pb.cpp @@ -26,3 +26,5 @@ PB_BIND(meshtastic_Map, meshtastic_Map, AUTO) + + diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 5692a2749..8313438f8 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -10,6 +10,15 @@ #endif /* Enum definitions */ +typedef enum _meshtastic_CompassMode { + /* Compass with dynamic ring and heading */ + meshtastic_CompassMode_DYNAMIC = 0, + /* Compass with fixed ring and heading */ + meshtastic_CompassMode_FIXED_RING = 1, + /* Compass with heading and freeze option */ + meshtastic_CompassMode_FREEZE_HEADING = 2 +} meshtastic_CompassMode; + typedef enum _meshtastic_Theme { /* Dark */ meshtastic_Theme_DARK = 0, @@ -55,6 +64,8 @@ typedef enum _meshtastic_Language { meshtastic_Language_SLOVENIAN = 15, /* Ukrainian */ meshtastic_Language_UKRAINIAN = 16, + /* Bulgarian */ + meshtastic_Language_BULGARIAN = 17, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ @@ -142,6 +153,14 @@ typedef struct _meshtastic_DeviceUIConfig { /* Map related data */ bool has_map_data; meshtastic_Map map_data; + /* Compass mode */ + meshtastic_CompassMode compass_mode; + /* RGB color for BaseUI + 0xRRGGBB format, e.g. 0xFF0000 for red */ + uint32_t screen_rgb_color; + /* Clockface analog style + true for analog clockface, false for digital clockface */ + bool is_clockface_analog; } meshtastic_DeviceUIConfig; @@ -150,6 +169,10 @@ extern "C" { #endif /* Helper constants for enums */ +#define _meshtastic_CompassMode_MIN meshtastic_CompassMode_DYNAMIC +#define _meshtastic_CompassMode_MAX meshtastic_CompassMode_FREEZE_HEADING +#define _meshtastic_CompassMode_ARRAYSIZE ((meshtastic_CompassMode)(meshtastic_CompassMode_FREEZE_HEADING+1)) + #define _meshtastic_Theme_MIN meshtastic_Theme_DARK #define _meshtastic_Theme_MAX meshtastic_Theme_RED #define _meshtastic_Theme_ARRAYSIZE ((meshtastic_Theme)(meshtastic_Theme_RED+1)) @@ -160,6 +183,7 @@ extern "C" { #define meshtastic_DeviceUIConfig_theme_ENUMTYPE meshtastic_Theme #define meshtastic_DeviceUIConfig_language_ENUMTYPE meshtastic_Language +#define meshtastic_DeviceUIConfig_compass_mode_ENUMTYPE meshtastic_CompassMode @@ -167,12 +191,12 @@ extern "C" { /* Initializer values for message structs */ -#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}, false, meshtastic_Map_init_default} +#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}, false, meshtastic_Map_init_default, _meshtastic_CompassMode_MIN, 0, 0} #define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_default {0, 0, 0, 0, ""} #define meshtastic_GeoPoint_init_default {0, 0, 0} #define meshtastic_Map_init_default {false, meshtastic_GeoPoint_init_default, "", 0} -#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}, false, meshtastic_Map_init_zero} +#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}, false, meshtastic_Map_init_zero, _meshtastic_CompassMode_MIN, 0, 0} #define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_zero {0, 0, 0, 0, ""} #define meshtastic_GeoPoint_init_zero {0, 0, 0} @@ -212,6 +236,9 @@ extern "C" { #define meshtastic_DeviceUIConfig_node_highlight_tag 13 #define meshtastic_DeviceUIConfig_calibration_data_tag 14 #define meshtastic_DeviceUIConfig_map_data_tag 15 +#define meshtastic_DeviceUIConfig_compass_mode_tag 16 +#define meshtastic_DeviceUIConfig_screen_rgb_color_tag 17 +#define meshtastic_DeviceUIConfig_is_clockface_analog_tag 18 /* Struct field encoding specification for nanopb */ #define meshtastic_DeviceUIConfig_FIELDLIST(X, a) \ @@ -229,7 +256,10 @@ X(a, STATIC, SINGULAR, UENUM, language, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, node_filter, 12) \ X(a, STATIC, OPTIONAL, MESSAGE, node_highlight, 13) \ X(a, STATIC, SINGULAR, BYTES, calibration_data, 14) \ -X(a, STATIC, OPTIONAL, MESSAGE, map_data, 15) +X(a, STATIC, OPTIONAL, MESSAGE, map_data, 15) \ +X(a, STATIC, SINGULAR, UENUM, compass_mode, 16) \ +X(a, STATIC, SINGULAR, UINT32, screen_rgb_color, 17) \ +X(a, STATIC, SINGULAR, BOOL, is_clockface_analog, 18) #define meshtastic_DeviceUIConfig_CALLBACK NULL #define meshtastic_DeviceUIConfig_DEFAULT NULL #define meshtastic_DeviceUIConfig_node_filter_MSGTYPE meshtastic_NodeFilter @@ -286,7 +316,7 @@ extern const pb_msgdesc_t meshtastic_Map_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_MAX_SIZE meshtastic_DeviceUIConfig_size -#define meshtastic_DeviceUIConfig_size 188 +#define meshtastic_DeviceUIConfig_size 201 #define meshtastic_GeoPoint_size 33 #define meshtastic_Map_size 58 #define meshtastic_NodeFilter_size 47 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 37f99d8b5..f78689cb2 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2269 +#define meshtastic_BackupPreferences_size 2271 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1722 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index bb2eefc04..ca8dcd5fb 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -187,7 +187,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size -#define meshtastic_LocalConfig_size 745 +#define meshtastic_LocalConfig_size 747 #define meshtastic_LocalModuleConfig_size 669 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 11875fadd..361d01b9a 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -60,6 +60,12 @@ PB_BIND(meshtastic_KeyVerificationNumberRequest, meshtastic_KeyVerificationNumbe PB_BIND(meshtastic_KeyVerificationFinal, meshtastic_KeyVerificationFinal, AUTO) +PB_BIND(meshtastic_DuplicatedPublicKey, meshtastic_DuplicatedPublicKey, AUTO) + + +PB_BIND(meshtastic_LowEntropyKey, meshtastic_LowEntropyKey, AUTO) + + PB_BIND(meshtastic_FileInfo, meshtastic_FileInfo, AUTO) diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 5fc1cc4f5..9e0415198 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -258,6 +258,21 @@ typedef enum _meshtastic_HardwareModel { 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 */ + meshtastic_HardwareModel_T_DECK_PRO = 102, + /* * + Lilygo TLora Pager */ + meshtastic_HardwareModel_T_LORA_PAGER = 103, + /* * + GAT562 Mesh Trial Tracker */ + meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104, + /* * + RAKwireless WisMesh Tag */ + meshtastic_HardwareModel_WISMESH_TAG = 105, + /* * + RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */ + meshtastic_HardwareModel_RAK3312 = 106, /* ------------------------------------------------------------------------------------------------------------------------------------------ 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. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -949,6 +964,14 @@ typedef struct _meshtastic_KeyVerificationFinal { char verification_characters[10]; } meshtastic_KeyVerificationFinal; +typedef struct _meshtastic_DuplicatedPublicKey { + char dummy_field; +} meshtastic_DuplicatedPublicKey; + +typedef struct _meshtastic_LowEntropyKey { + char dummy_field; +} meshtastic_LowEntropyKey; + /* A notification message from the device to the client To be used for important messages that should to be displayed to the user in the form of push notifications or validation messages when saving @@ -968,6 +991,8 @@ typedef struct _meshtastic_ClientNotification { meshtastic_KeyVerificationNumberInform key_verification_number_inform; meshtastic_KeyVerificationNumberRequest key_verification_number_request; meshtastic_KeyVerificationFinal key_verification_final; + meshtastic_DuplicatedPublicKey duplicated_public_key; + meshtastic_LowEntropyKey low_entropy_key; } payload_variant; } meshtastic_ClientNotification; @@ -1248,6 +1273,8 @@ extern "C" { + + #define meshtastic_Compressed_portnum_ENUMTYPE meshtastic_PortNum @@ -1280,6 +1307,8 @@ extern "C" { #define meshtastic_KeyVerificationNumberInform_init_default {0, "", 0} #define meshtastic_KeyVerificationNumberRequest_init_default {0, ""} #define meshtastic_KeyVerificationFinal_init_default {0, "", 0, ""} +#define meshtastic_DuplicatedPublicKey_init_default {0} +#define meshtastic_LowEntropyKey_init_default {0} #define meshtastic_FileInfo_init_default {"", 0} #define meshtastic_ToRadio_init_default {0, {meshtastic_MeshPacket_init_default}} #define meshtastic_Compressed_init_default {_meshtastic_PortNum_MIN, {0, {0}}} @@ -1309,6 +1338,8 @@ extern "C" { #define meshtastic_KeyVerificationNumberInform_init_zero {0, "", 0} #define meshtastic_KeyVerificationNumberRequest_init_zero {0, ""} #define meshtastic_KeyVerificationFinal_init_zero {0, "", 0, ""} +#define meshtastic_DuplicatedPublicKey_init_zero {0} +#define meshtastic_LowEntropyKey_init_zero {0} #define meshtastic_FileInfo_init_zero {"", 0} #define meshtastic_ToRadio_init_zero {0, {meshtastic_MeshPacket_init_zero}} #define meshtastic_Compressed_init_zero {_meshtastic_PortNum_MIN, {0, {0}}} @@ -1446,6 +1477,8 @@ extern "C" { #define meshtastic_ClientNotification_key_verification_number_inform_tag 11 #define meshtastic_ClientNotification_key_verification_number_request_tag 12 #define meshtastic_ClientNotification_key_verification_final_tag 13 +#define meshtastic_ClientNotification_duplicated_public_key_tag 14 +#define meshtastic_ClientNotification_low_entropy_key_tag 15 #define meshtastic_FileInfo_file_name_tag 1 #define meshtastic_FileInfo_size_bytes_tag 2 #define meshtastic_Compressed_portnum_tag 1 @@ -1714,12 +1747,16 @@ X(a, STATIC, SINGULAR, UENUM, level, 3) \ X(a, STATIC, SINGULAR, STRING, message, 4) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_number_inform,payload_variant.key_verification_number_inform), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_number_request,payload_variant.key_verification_number_request), 12) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_final,payload_variant.key_verification_final), 13) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_final,payload_variant.key_verification_final), 13) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,duplicated_public_key,payload_variant.duplicated_public_key), 14) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,low_entropy_key,payload_variant.low_entropy_key), 15) #define meshtastic_ClientNotification_CALLBACK NULL #define meshtastic_ClientNotification_DEFAULT NULL #define meshtastic_ClientNotification_payload_variant_key_verification_number_inform_MSGTYPE meshtastic_KeyVerificationNumberInform #define meshtastic_ClientNotification_payload_variant_key_verification_number_request_MSGTYPE meshtastic_KeyVerificationNumberRequest #define meshtastic_ClientNotification_payload_variant_key_verification_final_MSGTYPE meshtastic_KeyVerificationFinal +#define meshtastic_ClientNotification_payload_variant_duplicated_public_key_MSGTYPE meshtastic_DuplicatedPublicKey +#define meshtastic_ClientNotification_payload_variant_low_entropy_key_MSGTYPE meshtastic_LowEntropyKey #define meshtastic_KeyVerificationNumberInform_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ @@ -1742,6 +1779,16 @@ X(a, STATIC, SINGULAR, STRING, verification_characters, 4) #define meshtastic_KeyVerificationFinal_CALLBACK NULL #define meshtastic_KeyVerificationFinal_DEFAULT NULL +#define meshtastic_DuplicatedPublicKey_FIELDLIST(X, a) \ + +#define meshtastic_DuplicatedPublicKey_CALLBACK NULL +#define meshtastic_DuplicatedPublicKey_DEFAULT NULL + +#define meshtastic_LowEntropyKey_FIELDLIST(X, a) \ + +#define meshtastic_LowEntropyKey_CALLBACK NULL +#define meshtastic_LowEntropyKey_DEFAULT NULL + #define meshtastic_FileInfo_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, file_name, 1) \ X(a, STATIC, SINGULAR, UINT32, size_bytes, 2) @@ -1853,6 +1900,8 @@ extern const pb_msgdesc_t meshtastic_ClientNotification_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationNumberInform_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationNumberRequest_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationFinal_msg; +extern const pb_msgdesc_t meshtastic_DuplicatedPublicKey_msg; +extern const pb_msgdesc_t meshtastic_LowEntropyKey_msg; extern const pb_msgdesc_t meshtastic_FileInfo_msg; extern const pb_msgdesc_t meshtastic_ToRadio_msg; extern const pb_msgdesc_t meshtastic_Compressed_msg; @@ -1884,6 +1933,8 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_KeyVerificationNumberInform_fields &meshtastic_KeyVerificationNumberInform_msg #define meshtastic_KeyVerificationNumberRequest_fields &meshtastic_KeyVerificationNumberRequest_msg #define meshtastic_KeyVerificationFinal_fields &meshtastic_KeyVerificationFinal_msg +#define meshtastic_DuplicatedPublicKey_fields &meshtastic_DuplicatedPublicKey_msg +#define meshtastic_LowEntropyKey_fields &meshtastic_LowEntropyKey_msg #define meshtastic_FileInfo_fields &meshtastic_FileInfo_msg #define meshtastic_ToRadio_fields &meshtastic_ToRadio_msg #define meshtastic_Compressed_fields &meshtastic_Compressed_msg @@ -1905,6 +1956,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_Compressed_size 239 #define meshtastic_Data_size 269 #define meshtastic_DeviceMetadata_size 54 +#define meshtastic_DuplicatedPublicKey_size 0 #define meshtastic_FileInfo_size 236 #define meshtastic_FromRadio_size 510 #define meshtastic_Heartbeat_size 0 @@ -1913,6 +1965,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #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_MqttClientProxyMessage_size 501 #define meshtastic_MyNodeInfo_size 77 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 5bd27ef7d..67adc60cc 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -133,6 +133,10 @@ typedef enum _meshtastic_PortNum { /* Reticulum Network Stack Tunnel App ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface */ meshtastic_PortNum_RETICULUM_TUNNEL_APP = 76, + /* App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send + arbitrary telemetry over meshtastic that is not covered by telemetry.proto + ENCODING: CayenneLLP */ + meshtastic_PortNum_CAYENNE_APP = 77, /* Private applications should use portnums >= 256. To simplify initial development and testing you can use "PRIVATE_APP" in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) */ diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index 4fa673df8..072a99a24 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -91,7 +91,9 @@ typedef enum _meshtastic_TelemetrySensorType { /* MAX17261 lipo battery gauge */ meshtastic_TelemetrySensorType_MAX17261 = 38, /* PCT2075 Temperature Sensor */ - meshtastic_TelemetrySensorType_PCT2075 = 39 + meshtastic_TelemetrySensorType_PCT2075 = 39, + /* ADS1X15 ADC */ + meshtastic_TelemetrySensorType_ADS1X15 = 40 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -206,6 +208,36 @@ typedef struct _meshtastic_PowerMetrics { /* Current (Ch3) */ bool has_ch3_current; float ch3_current; + /* Voltage (Ch4) */ + bool has_ch4_voltage; + float ch4_voltage; + /* Current (Ch4) */ + bool has_ch4_current; + float ch4_current; + /* Voltage (Ch5) */ + bool has_ch5_voltage; + float ch5_voltage; + /* Current (Ch5) */ + bool has_ch5_current; + float ch5_current; + /* Voltage (Ch6) */ + bool has_ch6_voltage; + float ch6_voltage; + /* Current (Ch6) */ + bool has_ch6_current; + float ch6_current; + /* Voltage (Ch7) */ + bool has_ch7_voltage; + float ch7_voltage; + /* Current (Ch7) */ + bool has_ch7_current; + float ch7_current; + /* Voltage (Ch8) */ + bool has_ch8_voltage; + float ch8_voltage; + /* Current (Ch8) */ + bool has_ch8_current; + float ch8_current; } meshtastic_PowerMetrics; /* Air quality metrics */ @@ -249,6 +281,12 @@ typedef struct _meshtastic_AirQualityMetrics { /* CO2 concentration in ppm */ bool has_co2; uint32_t co2; + /* CO2 sensor temperature in degC */ + bool has_co2_temperature; + float co2_temperature; + /* CO2 sensor relative humidity in % */ + bool has_co2_humidity; + float co2_humidity; } meshtastic_AirQualityMetrics; /* Local device mesh statistics */ @@ -360,8 +398,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_PCT2075 -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_PCT2075+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_ADS1X15 +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_ADS1X15+1)) @@ -376,8 +414,8 @@ extern "C" { /* Initializer values for message structs */ #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} -#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} +#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_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, ""} @@ -385,8 +423,8 @@ extern "C" { #define meshtastic_Nau7802Config_init_default {0, 0} #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} -#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} +#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_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, ""} @@ -427,6 +465,16 @@ extern "C" { #define meshtastic_PowerMetrics_ch2_current_tag 4 #define meshtastic_PowerMetrics_ch3_voltage_tag 5 #define meshtastic_PowerMetrics_ch3_current_tag 6 +#define meshtastic_PowerMetrics_ch4_voltage_tag 7 +#define meshtastic_PowerMetrics_ch4_current_tag 8 +#define meshtastic_PowerMetrics_ch5_voltage_tag 9 +#define meshtastic_PowerMetrics_ch5_current_tag 10 +#define meshtastic_PowerMetrics_ch6_voltage_tag 11 +#define meshtastic_PowerMetrics_ch6_current_tag 12 +#define meshtastic_PowerMetrics_ch7_voltage_tag 13 +#define meshtastic_PowerMetrics_ch7_current_tag 14 +#define meshtastic_PowerMetrics_ch8_voltage_tag 15 +#define meshtastic_PowerMetrics_ch8_current_tag 16 #define meshtastic_AirQualityMetrics_pm10_standard_tag 1 #define meshtastic_AirQualityMetrics_pm25_standard_tag 2 #define meshtastic_AirQualityMetrics_pm100_standard_tag 3 @@ -440,6 +488,8 @@ extern "C" { #define meshtastic_AirQualityMetrics_particles_50um_tag 11 #define meshtastic_AirQualityMetrics_particles_100um_tag 12 #define meshtastic_AirQualityMetrics_co2_tag 13 +#define meshtastic_AirQualityMetrics_co2_temperature_tag 14 +#define meshtastic_AirQualityMetrics_co2_humidity_tag 15 #define meshtastic_LocalStats_uptime_seconds_tag 1 #define meshtastic_LocalStats_channel_utilization_tag 2 #define meshtastic_LocalStats_air_util_tx_tag 3 @@ -518,7 +568,17 @@ X(a, STATIC, OPTIONAL, FLOAT, ch1_current, 2) \ X(a, STATIC, OPTIONAL, FLOAT, ch2_voltage, 3) \ X(a, STATIC, OPTIONAL, FLOAT, ch2_current, 4) \ X(a, STATIC, OPTIONAL, FLOAT, ch3_voltage, 5) \ -X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6) +X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6) \ +X(a, STATIC, OPTIONAL, FLOAT, ch4_voltage, 7) \ +X(a, STATIC, OPTIONAL, FLOAT, ch4_current, 8) \ +X(a, STATIC, OPTIONAL, FLOAT, ch5_voltage, 9) \ +X(a, STATIC, OPTIONAL, FLOAT, ch5_current, 10) \ +X(a, STATIC, OPTIONAL, FLOAT, ch6_voltage, 11) \ +X(a, STATIC, OPTIONAL, FLOAT, ch6_current, 12) \ +X(a, STATIC, OPTIONAL, FLOAT, ch7_voltage, 13) \ +X(a, STATIC, OPTIONAL, FLOAT, ch7_current, 14) \ +X(a, STATIC, OPTIONAL, FLOAT, ch8_voltage, 15) \ +X(a, STATIC, OPTIONAL, FLOAT, ch8_current, 16) #define meshtastic_PowerMetrics_CALLBACK NULL #define meshtastic_PowerMetrics_DEFAULT NULL @@ -535,7 +595,9 @@ X(a, STATIC, OPTIONAL, UINT32, particles_10um, 9) \ X(a, STATIC, OPTIONAL, UINT32, particles_25um, 10) \ 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, UINT32, co2, 13) \ +X(a, STATIC, OPTIONAL, FLOAT, co2_temperature, 14) \ +X(a, STATIC, OPTIONAL, FLOAT, co2_humidity, 15) #define meshtastic_AirQualityMetrics_CALLBACK NULL #define meshtastic_AirQualityMetrics_DEFAULT NULL @@ -624,14 +686,14 @@ 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 78 +#define meshtastic_AirQualityMetrics_size 88 #define meshtastic_DeviceMetrics_size 27 #define meshtastic_EnvironmentMetrics_size 113 #define meshtastic_HealthMetrics_size 11 #define meshtastic_HostMetrics_size 264 #define meshtastic_LocalStats_size 72 #define meshtastic_Nau7802Config_size 16 -#define meshtastic_PowerMetrics_size 30 +#define meshtastic_PowerMetrics_size 81 #define meshtastic_Telemetry_size 272 #ifdef __cplusplus diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 5841fe478..42ebb8417 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -903,7 +903,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) } } else { #if HAS_SCREEN - screen->blink(); + if (screen) + screen->blink(); #endif } diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 5f6ad9eb3..bf170de59 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -154,7 +154,8 @@ void createSSLCert() esp_task_wdt_reset(); #if HAS_SCREEN if (millis() / 1000 >= 3) { - screen->setSSLFrames(); + if (screen) + screen->setSSLFrames(); } #endif } diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 39bd61021..d1cc1065c 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -44,7 +44,10 @@ class UdpMulticastHandler final meshtastic_MeshPacket mp; 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) { + if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.pki_encrypted = false; + mp.public_key.size = 0; + memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); // Unset received SNR/RSSI p->rx_snr = 0; diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 789f8ac44..7a56c258b 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -46,6 +46,10 @@ uint8_t wifiDisconnectReason = 0; // Stores our hostname char ourHost[16]; +// To replace blocking wifi connect delay with a non-blocking sleep +static unsigned long wifiReconnectStartMillis = 0; +static bool wifiReconnectPending = false; + bool APStartupComplete = 0; unsigned long lastrun_ntp = 0; @@ -83,16 +87,18 @@ static void onNetworkConnected() // start mdns if (!MDNS.begin("Meshtastic")) { - LOG_ERROR("Error setting up MDNS responder!"); + LOG_ERROR("Error setting up mDNS responder!"); } else { LOG_INFO("mDNS Host: Meshtastic.local"); MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT); +// ESPmDNS (ESP32) and SimpleMDNS (RP2040) have slightly different APIs for adding TXT records #ifdef ARCH_ESP32 - MDNS.addService("http", "tcp", 80); - MDNS.addService("https", "tcp", 443); + MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name)); + MDNS.addServiceTxt("meshtastic", "tcp", "id", String(owner.id)); // ESP32 prints obtained IP address in WiFiEvent #elif defined(ARCH_RP2040) - // ARCH_RP2040 does not support HTTPS + MDNS.addServiceTxt("meshtastic", "shortname", owner.short_name); + MDNS.addServiceTxt("meshtastic", "id", owner.id); LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); #endif } @@ -124,10 +130,14 @@ static void onNetworkConnected() } #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - initWebServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initWebServer(); + } #endif #if !MESHTASTIC_EXCLUDE_SOCKETAPI - initApiServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif APStartupComplete = true; } @@ -160,17 +170,30 @@ static int32_t reconnectWiFi() #endif LOG_INFO("Reconnecting to WiFi access point %s", wifiName); - delay(5000); + // Start the non-blocking wait for 5 seconds + wifiReconnectStartMillis = millis(); + wifiReconnectPending = true; + // Do not attempt to connect yet, wait for the next invocation + return 5000; // Schedule next check soon + } - if (!WiFi.isConnected()) { + // Check if we are ready to proceed with the WiFi connection after the 5s wait + if (wifiReconnectPending) { + if (millis() - wifiReconnectStartMillis >= 5000) { + if (!WiFi.isConnected()) { #ifdef CONFIG_IDF_TARGET_ESP32C3 - WiFi.mode(WIFI_MODE_NULL); - WiFi.useStaticBuffers(true); - WiFi.mode(WIFI_STA); + WiFi.mode(WIFI_MODE_NULL); + WiFi.useStaticBuffers(true); + WiFi.mode(WIFI_STA); #endif - WiFi.begin(wifiName, wifiPsw); + WiFi.begin(wifiName, wifiPsw); + } + isReconnecting = false; + wifiReconnectPending = false; + } else { + // Still waiting for 5s to elapse + return 100; // Check again soon } - isReconnecting = false; } #ifndef DISABLE_NTP @@ -193,8 +216,6 @@ static int32_t reconnectWiFi() if (config.network.wifi_enabled && !WiFi.isConnected()) { #ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) - /* If APStartupComplete, but we're not connected, try again. - Shouldn't try again before APStartupComplete. */ needReconnect = APStartupComplete; #endif return 1000; // check once per second @@ -327,9 +348,15 @@ static void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: LOG_INFO("Connected to access point"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, HIGH); +#endif break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: LOG_INFO("Disconnected from WiFi access point"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, LOW); +#endif if (!isReconnecting) { WiFi.disconnect(false, true); syslog.disable(); @@ -378,9 +405,15 @@ static void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_AP_START: LOG_INFO("WiFi access point started"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, HIGH); +#endif break; case ARDUINO_EVENT_WIFI_AP_STOP: LOG_INFO("WiFi access point stopped"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, LOW); +#endif break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: LOG_INFO("Client connected"); @@ -474,4 +507,4 @@ uint8_t getWifiDisconnectReason() { return wifiDisconnectReason; } -#endif \ No newline at end of file +#endif // HAS_WIFI \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index 47d42b41b..9fcf6f8a8 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -11,6 +11,15 @@ template constexpr const T &clamp(const T &v, const T &lo, const T &hi return (v < lo) ? lo : (hi < v) ? hi : v; } +#if HAS_SCREEN +#define IF_SCREEN(X) \ + if (screen) { \ + X; \ + } +#else +#define IF_SCREEN(...) +#endif + #if (defined(ARCH_PORTDUINO) && !defined(STRNSTR)) #define STRNSTR #include diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4005222dc..8d3e710df 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -5,8 +5,10 @@ #include "PowerFSM.h" #include "RTC.h" #include "SPILock.h" +#include "input/InputBroker.h" #include "meshUtils.h" #include +#include // for better whitespace handling #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH #include "BleOta.h" #endif @@ -155,6 +157,28 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta */ case meshtastic_AdminMessage_set_owner_tag: LOG_DEBUG("Client set owner"); + // Validate names + if (*r->set_owner.long_name) { + const char *start = r->set_owner.long_name; + // Skip all whitespace (space, tab, newline, etc) + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; + } + } + if (*r->set_owner.short_name) { + const char *start = r->set_owner.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; + } + } handleSetOwner(r->set_owner); break; @@ -200,14 +224,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #if defined(ARCH_ESP32) #if !MESHTASTIC_EXCLUDE_BLUETOOTH if (!BleOta::getOtaAppVersion().isEmpty()) { - screen->startFirmwareUpdateScreen(); + if (screen) + screen->startFirmwareUpdateScreen(); BleOta::switchToOtaApp(); LOG_INFO("Rebooting to BLE OTA"); } #endif #if !MESHTASTIC_EXCLUDE_WIFI if (WiFiOTA::trySwitchToOTA()) { - screen->startFirmwareUpdateScreen(); + if (screen) + screen->startFirmwareUpdateScreen(); WiFiOTA::saveConfig(&config.network); LOG_INFO("Rebooting to WiFi OTA"); } @@ -297,6 +323,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = true; saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -306,6 +334,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = false; saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -420,6 +450,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #endif break; } + case meshtastic_AdminMessage_send_input_event_tag: { + LOG_INFO("Client requesting to send input event"); + handleSendInputEvent(r->send_input_event); + break; + } #ifdef ARCH_PORTDUINO case meshtastic_AdminMessage_exit_simulator_tag: LOG_INFO("Exiting simulator"); @@ -435,22 +470,38 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta setPassKey(&res); myReply = allocDataProtobuf(res); } else if (mp.decoded.want_response) { - LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant); + LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); } else if (handleResult != AdminMessageHandleResult::HANDLED) { // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages - LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant); + LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); } break; } + // Allow any observers (e.g. the UI) to handle/respond + AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; + meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; + AdminModule_ObserverData observerData = { + .request = r, + .response = &observerResponse, + .result = &observerResult, + }; + + notifyObservers(&observerData); + + if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&observerResponse); + myReply = allocDataProtobuf(observerResponse); + LOG_DEBUG("Observer responded to admin message"); + } else if (observerResult == AdminMessageHandleResult::HANDLED) { + LOG_DEBUG("Observer handled admin message"); + } + // If asked for a response and it is not yet set, generate an 'ACK' response if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - // Allow any observers (e.g. the UI) to respond to this event - notifyObservers(r); - return handled; } @@ -507,7 +558,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) if (owner.has_is_unmessagable != o.has_is_unmessagable || (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { changed = 1; - owner.has_is_unmessagable = o.has_is_unmessagable || o.has_is_unmessagable; + owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; owner.is_unmessagable = o.is_unmessagable; } @@ -579,6 +630,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) #if USERPREFS_EVENT_MODE // If we're in event mode, nobody is a Router or Repeater if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE || config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; } @@ -620,8 +672,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) config.has_display = true; if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && config.display.flip_screen == c.payload_variant.display.flip_screen && - config.display.oled == c.payload_variant.display.oled) { + config.display.oled == c.payload_variant.display.oled && + config.display.displaymode == c.payload_variant.display.displaymode) { requiresReboot = false; + } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && + c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + config.bluetooth.enabled = false; } #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && @@ -699,11 +755,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) LOG_INFO("Set config: Security"); config.security = c.payload_variant.security; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI) - // We check for a potentially valid private key, and a blank public key, and regen the public key if needed. - if (config.security.private_key.size == 32 && !memfll(config.security.private_key.bytes, 0, 32) && - (config.security.public_key.size == 0 || memfll(config.security.public_key.bytes, 0, 32))) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - config.security.public_key.size = 32; + // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + 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. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + config.security.public_key.size = 32; + } } } #endif @@ -1093,7 +1154,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r #endif #endif conn.has_serial = true; // No serial-less devices -#if !EXCLUDE_POWER_FSM +#if !MESHTASTIC_EXCLUDE_POWER_FSM conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; #else conn.serial.is_connected = powerFSM.getState(); @@ -1129,7 +1190,8 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); - screen->startAlert("Rebooting..."); + if (screen) + screen->showSimpleBanner("Rebooting...", 0); // stays on screen rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } @@ -1153,6 +1215,27 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { + // Validate ham parameters before setting since this would bypass validation in the owner struct + if (*p.call_sign) { + const char *start = p.call_sign; + // Skip all whitespace + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); + return; + } + } + if (*p.short_name) { + const char *start = p.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); + return; + } + } + // Set call sign and override lora limitations for licensed use strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); @@ -1239,6 +1322,33 @@ bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) return false; } +void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) +{ + LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, + inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y); + + // Create InputEvent for injection + InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, + .kbchar = (unsigned char)inputEvent.kb_char, + .touchX = inputEvent.touch_x, + .touchY = inputEvent.touch_y}; + + // Log the event being injected + LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, + (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); + + // Wake the device if asleep + powerFSM.trigger(EVENT_INPUT); +#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) + // Inject the event through InputBroker + if (inputBroker) { + inputBroker->injectInputEvent(&event); + } else { + LOG_ERROR("InputBroker not available for event injection"); + } +#endif +} + void AdminModule::sendWarning(const char *message) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 246d39e37..867751f49 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -6,10 +6,19 @@ #include "mesh/wifi/WiFiAPClient.h" #endif +/** + * Datatype passed to Observers by AdminModule, to allow external handling of admin messages + */ +struct AdminModule_ObserverData { + const meshtastic_AdminMessage *request; + meshtastic_AdminMessage *response; + AdminMessageHandleResult *result; +}; + /** * Admin module for admin messages */ -class AdminModule : public ProtobufModule, public Observable +class AdminModule : public ProtobufModule, public Observable { public: /** Constructor @@ -54,6 +63,7 @@ class AdminModule : public ProtobufModule, public Obser void handleSetChannel(); void handleSetHamMode(const meshtastic_HamParameters &req); void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); + void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); void reboot(int32_t seconds); void setPassKey(meshtastic_AdminMessage *res); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index c16c0e4b3..a1b89e0f8 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -8,14 +8,16 @@ #include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" -#include "PowerFSM.h" // needed for button bypass #include "SPILock.h" +#include "buzz.h" #include "detect/ScanI2C.h" -#include "input/ScanAndSelect.h" +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "graphics/images.h" +#include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" - -#include "main.h" // for cardkb_found #include "modules/ExternalNotificationModule.h" // for buzzer control #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" @@ -35,6 +37,7 @@ #define INACTIVATE_AFTER_MS 20000 extern ScanI2C::DeviceAddress cardkb_found; +extern bool graphics::isMuted; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; @@ -45,358 +48,777 @@ CannedMessageModule *cannedMessageModule; CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { - if (moduleConfig.canned_message.enabled || CANNED_MESSAGE_MODULE_ENABLE) { - this->loadProtoForModule(); - if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && - !CANNED_MESSAGE_MODULE_ENABLE) { - LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); - this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; - disable(); - } else { - LOG_INFO("CannedMessageModule is enabled"); - - // T-Watch interface currently has no way to select destination type, so default to 'node' -#if defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; -#endif - - this->inputObserver.observe(inputBroker); - } - } else { + this->loadProtoForModule(); + if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && + !CANNED_MESSAGE_MODULE_ENABLE) { + LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; disable(); + } else { + LOG_INFO("CannedMessageModule is enabled"); + this->inputObserver.observe(inputBroker); } } +void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) +{ + dest = newDest; + channel = newChannel; + // Always select the first real canned message on activation + int firstRealMsgIdx = 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; + break; + } + } + currentMessageIndex = firstRealMsgIdx; + + // This triggers the canned message list + runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); +} + +void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) +{ + dest = newDest; + channel = newChannel; + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); +} + +static bool returnToCannedList = false; +bool hasKeyForNode(const meshtastic_NodeInfoLite *node) +{ + return node && node->has_user && node->user.public_key.size > 0; +} /** * @brief Items in array this->messages will be set to be pointing on the right * starting points of the string this->messageStore * * @return int Returns the number of messages found. */ -// FIXME: This is just one set of messages now + int CannedMessageModule::splitConfiguredMessages() { - int messageIndex = 0; int i = 0; String canned_messages = cannedMessageModuleConfig.messages; -#if defined(USE_VIRTUAL_KEYBOARD) - String separator = canned_messages.length() ? "|" : ""; - - canned_messages = "[---- Free Text ----]" + separator + canned_messages; -#endif - - // collect all the message parts + // Copy all message parts into the buffer strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); - // The first message points to the beginning of the store. - this->messages[messageIndex++] = this->messageStore; + // Temporary array to allow for insertion + const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; + int tempCount = 0; + // Insert at position 0 (top) + tempMessages[tempCount++] = "[Select Destination]"; + +#if defined(USE_VIRTUAL_KEYBOARD) + // Add a "Free Text" entry at the top if using a keyboard + tempMessages[tempCount++] = "[-- Free Text --]"; +#endif + + // First message always starts at buffer start + tempMessages[tempCount++] = this->messageStore; int upTo = strlen(this->messageStore) - 1; + // Walk buffer, splitting on '|' while (i < upTo) { if (this->messageStore[i] == '|') { - // Message ending found, replace it with string-end character. - this->messageStore[i] = '\0'; - - // hit our max messages, bail - if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) { - this->messagesCount = messageIndex; - return this->messagesCount; - } - - // Next message starts after pipe (|) just found. - this->messages[messageIndex++] = (this->messageStore + i + 1); + this->messageStore[i] = '\0'; // End previous message + if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) + break; + tempMessages[tempCount++] = (this->messageStore + i + 1); } i += 1; } - if (strlen(this->messages[messageIndex - 1]) > 0) { - // We have a last message. - LOG_DEBUG("CannedMessage %d is: '%s'", messageIndex - 1, this->messages[messageIndex - 1]); - this->messagesCount = messageIndex; - } else { - this->messagesCount = messageIndex - 1; + + // Add [Exit] as the last entry + tempMessages[tempCount++] = "[Exit]"; + + // Copy to the member array + for (int k = 0; k < tempCount; ++k) { + this->messages[k] = (char *)tempMessages[k]; } + this->messagesCount = tempCount; return this->messagesCount; } +void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) +{ + if (graphics::isHighResolution) { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + } + } else { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: Broadc@%.5s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + } + } +} +void CannedMessageModule::resetSearch() +{ + LOG_INFO("Resetting search, restoring full destination list"); + + int previousDestIndex = destIndex; + + searchQuery = ""; + updateDestinationSelectionList(); + + // Adjust scrollIndex so previousDestIndex is still visible + int totalEntries = activeChannelIndices.size() + filteredNodes.size(); + this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; + if (this->visibleRows < 1) + this->visibleRows = 1; + int maxScrollIndex = std::max(0, totalEntries - visibleRows); + scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); + + lastUpdateMillis = millis(); + requestFocus(); +} +void CannedMessageModule::updateDestinationSelectionList() +{ + static size_t lastNumMeshNodes = 0; + static String lastSearchQuery = ""; + + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + bool nodesChanged = (numMeshNodes != lastNumMeshNodes); + lastNumMeshNodes = numMeshNodes; + + // Early exit if nothing changed + if (searchQuery == lastSearchQuery && !nodesChanged) + return; + lastSearchQuery = searchQuery; + needsUpdate = false; + + this->filteredNodes.clear(); + this->activeChannelIndices.clear(); + + NodeNum myNodeNum = nodeDB->getNodeNum(); + String lowerSearchQuery = searchQuery; + lowerSearchQuery.toLowerCase(); + + // Preallocate space to reduce reallocation + this->filteredNodes.reserve(numMeshNodes); + + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == myNodeNum) + continue; + + const String &nodeName = node->user.long_name; + + if (searchQuery.length() == 0) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } else { + // Avoid unnecessary lowercase conversion if already matched + String lowerNodeName = nodeName; + lowerNodeName.toLowerCase(); + + if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } + } + } + + // Populate active channels + std::vector seenChannels; + seenChannels.reserve(channels.getNumChannels()); + for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { + String name = channels.getName(i); + if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { + this->activeChannelIndices.push_back(i); + seenChannels.push_back(name); + } + } + + /* As the nodeDB is sorted, can skip this step + // Sort by favorite, then last heard + std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { + if (a.node->is_favorite != b.node->is_favorite) + return a.node->is_favorite > b.node->is_favorite; + return a.lastHeard < b.lastHeard; + }); + */ + + scrollIndex = 0; // Show first result at the top + destIndex = 0; // Highlight the first entry + if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + LOG_INFO("Nodes changed, forcing UI refresh."); + screen->forceDisplay(); + } +} + +// Returns true if character input is currently allowed (used for search/freetext states) +bool CannedMessageModule::isCharInputAllowed() const +{ + return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; +} +/** + * Main input event dispatcher for CannedMessageModule. + * Routes keyboard/button/touch input to the correct handler based on the current runState. + * Only one handler (per state) processes each event, eliminating redundancy. + */ int CannedMessageModule::handleInputEvent(const InputEvent *event) { - if ((strlen(moduleConfig.canned_message.allow_input_source) > 0) && - (strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) && - (strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) { - // Event source is not accepted. - // Event only accepted if source matches the configured one, or - // the configured one is "_any" (or if there is no configured - // source at all) + // Block ALL input if an alert banner is active + if (screen && screen->isOverlayBannerShowing()) { return 0; } - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - return 0; // Ignore input while sending - } - bool validEvent = false; - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - validEvent = true; - } - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - validEvent = true; - } - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { -#if defined(USE_VIRTUAL_KEYBOARD) - if (this->currentMessageIndex == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + // Tab key: Always allow switching between canned/destination screens + if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) + return 1; - requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + // Matrix keypad: If matrix key, trigger action select for canned message + if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + payload = INPUT_BROKER_MATRIXKEY; + currentMessageIndex = event->kbchar - 1; + lastTouchMillis = millis(); + requestFocus(); + return 1; + } + + // Always normalize navigation/select buttons for further handlers + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + + // Route event to handler for current UI state (no double-handling) + switch (runState) { + // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace + case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: + if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) + return 1; + return 0; // prevent fall-through to selector input + + // Free text input mode: Handles character input, cancel, backspace, select, etc. + case CANNED_MESSAGE_RUN_STATE_FREETEXT: + return handleFreeTextInput(event); // All allowed input for this state + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: + return 1; + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: + return handleEmotePickerInput(event); + + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + if (isSelect) { + return 0; // Main button press no longer runs through powerFSM + } + // Let LEFT/RIGHT pass through so frame navigation works + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) { + break; + } + // Handle UP/DOWN: activate canned message list! + if (event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN || + event->inputEvent == INPUT_BROKER_ALT_LONG) { + LaunchWithDestination(NODENUM_BROADCAST); + return 1; + } + // Printable char (ASCII) opens free text compose + if (event->kbchar >= 32 && event->kbchar <= 126) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - - return 0; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Immediately process the input in the new state (freetext) + return handleFreeTextInput(event); } -#endif + break; - // when inactive, call the onebutton shortpress instead. Activate Module only on up/down - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { - powerFSM.trigger(EVENT_PRESS); - } else { - this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - validEvent = true; - } + // (Other states can be added here as needed) + default: + break; } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->currentMessageIndex = -1; -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->freetext = ""; // clear freetext - this->cursor = 0; - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif + // If no state handler above processed the event, let the message selector try to handle it + // (Handles up/down/select on canned message list, exit/return) + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) + return 1; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->notifyObservers(&e); - } - if ((event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) || - (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || - (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) { + // Default: event not handled by canned message system, allow others to process + return 0; +} -#if defined(USE_VIRTUAL_KEYBOARD) - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - this->payload = INPUT_BROKER_MSG_LEFT; - } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - this->payload = INPUT_BROKER_MSG_RIGHT; - } -#else - // tweak for left/right events generated via trackball/touch with empty kbchar - if (!event->kbchar) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - this->payload = INPUT_BROKER_MSG_LEFT; - } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - this->payload = INPUT_BROKER_MSG_RIGHT; - } - } else { - // pass the pressed key - this->payload = event->kbchar; - } -#endif +bool CannedMessageModule::isUpEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_UP || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + event->inputEvent == INPUT_BROKER_ALT_PRESS); +} +bool CannedMessageModule::isDownEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_DOWN || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + event->inputEvent == INPUT_BROKER_USER_PRESS); +} +bool CannedMessageModule::isSelectEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_SELECT; +} - this->lastTouchMillis = millis(); - validEvent = true; - } - if (event->inputEvent == static_cast(ANYKEY)) { - // when inactive, this will switch to the freetext mode - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - } +bool CannedMessageModule::handleTabSwitch(const InputEvent *event) +{ + if (event->kbchar != 0x09) + return false; - validEvent = false; // If key is normal than it will be set to true. + runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - // Run modifier key code below, (doesnt inturrupt typing or reset to start screen page) - switch (event->kbchar) { - case INPUT_BROKER_MSG_BRIGHTNESS_UP: // make screen brighter - if (screen) - screen->increaseBrightness(); - LOG_DEBUG("Increase Screen Brightness"); - break; - case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: // make screen dimmer - if (screen) - screen->decreaseBrightness(); - LOG_DEBUG("Decrease Screen Brightness"); - break; - case INPUT_BROKER_MSG_FN_SYMBOL_ON: // draw modifier (function) symbol - if (screen) - screen->setFunctionSymbol("Fn"); - break; - case INPUT_BROKER_MSG_FN_SYMBOL_OFF: // remove modifier (function) symbol - if (screen) - screen->removeFunctionSymbol("Fn"); - break; - // mute (switch off/toggle) external notifications on fn+m - case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled == true) { - if (externalNotificationModule->getMute()) { - externalNotificationModule->setMute(false); - showTemporaryMessage("Notifications \nEnabled"); - if (screen) - screen->removeFunctionSymbol("M"); // remove the mute symbol from the bottom right corner - } else { - externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop - externalNotificationModule->setMute(true); - showTemporaryMessage("Notifications \nDisabled"); - if (screen) - screen->setFunctionSymbol("M"); // add the mute symbol to the bottom right corner - } - } - break; - case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does -#if !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) { - gps->toggleGpsMode(); - } - if (screen) - screen->forceDisplay(); - showTemporaryMessage("GPS Toggled"); -#endif - break; - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off - if (config.bluetooth.enabled == true) { - config.bluetooth.enabled = false; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); - disableBluetooth(); - showTemporaryMessage("Bluetooth OFF"); - } else if (config.bluetooth.enabled == false) { - config.bluetooth.enabled = true; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); - rebootAtMsec = millis() + 2000; - showTemporaryMessage("Bluetooth ON\nReboot"); - } - break; - case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - showTemporaryMessage("Position \nUpdate Sent"); - } else { - showTemporaryMessage("Node Info \nUpdate Sent"); - } - break; - case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint - // Avoid opening the canned message screen frame - // We're only handling the keypress here by convention, this has nothing to do with canned messages - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - // Attempt to close whatever frame is currently shown on display - screen->dismissCurrentFrame(); - return 0; - default: - // pass the pressed key - // LOG_DEBUG("Canned message ANYKEY (%x)", event->kbchar); - this->payload = event->kbchar; - this->lastTouchMillis = millis(); - validEvent = true; - break; - } - if (screen && (event->kbchar != INPUT_BROKER_MSG_FN_SYMBOL_ON)) { - screen->removeFunctionSymbol("Fn"); // remove modifier (function) symbol + destIndex = 0; + scrollIndex = 0; + // RESTORE THIS! + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + updateDestinationSelectionList(); + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; +} + +int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + // Override isDown and isSelect ONLY for destination selector behavior + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; } } -#if defined(USE_VIRTUAL_KEYBOARD) - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - String keyTapped = keyForCoordinates(event->touchX, event->touchY); - - if (keyTapped == "โ‡ง") { - this->highlight = -1; - - this->payload = 0x00; - - validEvent = true; - - this->shift = !this->shift; - } else if (keyTapped == "โŒซ") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = 0x08; - - validEvent = true; - - this->shift = false; - } else if (keyTapped == "123" || keyTapped == "ABC") { - this->highlight = -1; - - this->payload = 0x00; - - this->charSet = this->charSet == 0 ? 1 : 0; - - validEvent = true; - } else if (keyTapped == " ") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = keyTapped[0]; - - validEvent = true; - - this->shift = false; - } else if (keyTapped == "โ†ต") { - this->highlight = 0x00; - - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - - this->currentMessageIndex = event->kbchar - 1; - - validEvent = true; - - this->shift = false; - } else if (keyTapped != "") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = this->shift ? keyTapped[0] : std::tolower(keyTapped[0]); - - validEvent = true; - - this->shift = false; + if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && + event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { + this->searchQuery += (char)event->kbchar; + needsUpdate = true; + if ((millis() - lastFilterUpdate) > filterDebounceMs) { + runOnce(); // update filter immediately + lastFilterUpdate = millis(); } - } -#endif - - if (event->inputEvent == static_cast(MATRIXKEY)) { - // this will send the text immediately on matrix press - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - this->payload = MATRIXKEY; - this->currentMessageIndex = event->kbchar - 1; - this->lastTouchMillis = millis(); - validEvent = true; + return 1; } - if (validEvent) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + size_t numMeshNodes = filteredNodes.size(); + int totalEntries = numMeshNodes + activeChannelIndices.size(); + int columns = 1; + int totalRows = totalEntries; + int maxScrollIndex = std::max(0, totalRows - visibleRows); + scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); - // Let runOnce to be called immediately. - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - setIntervalFromNow(0); // on fast keypresses, this isn't fast enough. - } else { + // Handle backspace + if (event->inputEvent == INPUT_BROKER_BACK) { + if (searchQuery.length() > 0) { + searchQuery.remove(searchQuery.length() - 1); + needsUpdate = true; runOnce(); } + if (searchQuery.length() == 0) { + resetSearch(); + needsUpdate = false; + } + return 1; + } + + if (isUp) { + if (destIndex > 0) { + destIndex--; + } else if (totalEntries > 0) { + destIndex = totalEntries - 1; + } + + if ((destIndex / columns) < scrollIndex) + scrollIndex = destIndex / columns; + else if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(true); + return 1; + } + + if (isDown) { + if (destIndex + 1 < totalEntries) { + destIndex++; + } else if (totalEntries > 0) { + destIndex = 0; + scrollIndex = 0; + } + + if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(true); + return 1; + } + + // SELECT + if (isSelect) { + if (destIndex < static_cast(activeChannelIndices.size())) { + dest = NODENUM_BROADCAST; + channel = activeChannelIndices[destIndex]; + } else { + int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); + if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { + const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; + if (selectedNode) { + dest = selectedNode->num; + channel = selectedNode->channel; + } + } + } + + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + screen->forceDisplay(true); + return 1; + } + + // CANCEL + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + searchQuery = ""; + + // UIFrameEvent e; + // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + // notifyObservers(&e); + screen->forceDisplay(true); + return 1; + } + + return 0; +} + +bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + // Override isDown and isSelect ONLY for canned message list behavior + if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + return false; + + // === Handle Cancel key: go inactive, clear UI state === + if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && + (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + bool handled = false; + + // Handle up/down navigation + if (isUp && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + handled = true; + } else if (isDown && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + handled = true; + } else if (isSelect) { + const char *current = messages[currentMessageIndex]; + + // === [Select Destination] triggers destination selection UI === + if (strcmp(current, "[Select Destination]") == 0) { + returnToCannedList = true; + runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + destIndex = 0; + scrollIndex = 0; + updateDestinationSelectionList(); // Make sure list is fresh + screen->forceDisplay(); + return true; + } + + // === [Exit] returns to the main/inactive screen === + if (strcmp(current, "[Exit]") == 0) { + // Set runState to inactive so we return to main UI + 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(); + return true; + } + + // === [Free Text] triggers the free text input (virtual keyboard) === +#if defined(USE_VIRTUAL_KEYBOARD) + if (strcmp(current, "[-- Free Text --]") == 0) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return true; + } +#endif + + // Normal canned message selection + if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + } else { + payload = runState; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + handled = true; + } + } + + if (handled) { + requestFocus(); + if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) + setIntervalFromNow(0); + else + runOnce(); + } + + return handled; +} +bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) +{ + // Always process only if in FREETEXT mode + if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) + return false; + +#if defined(USE_VIRTUAL_KEYBOARD) + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_LEFT) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + // Touch input (virtual keyboard) handling + // Only handle if touch coordinates present (CardKB won't set these) + if (event->touchX != 0 || event->touchY != 0) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + bool valid = false; + + if (keyTapped == "โ‡ง") { + highlight = -1; + payload = 0x00; + shift = !shift; + valid = true; + } else if (keyTapped == "โŒซ") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = 0x08; + shift = false; + valid = true; + } else if (keyTapped == "123" || keyTapped == "ABC") { + highlight = -1; + payload = 0x00; + charSet = (charSet == 0 ? 1 : 0); + valid = true; + } else if (keyTapped == " ") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = keyTapped[0]; + shift = false; + valid = true; + } + // Touch enter/submit + else if (keyTapped == "โ†ต") { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + shift = false; + valid = true; + } else if (!(keyTapped == "")) { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); + shift = false; + valid = true; + } + + if (valid) { + lastTouchMillis = millis(); + runOnce(); + payload = 0; + return true; // STOP: We handled a VKB touch + } + } +#endif // USE_VIRTUAL_KEYBOARD + + // ---- All hardware keys fall through to here (CardKB, physical, etc.) ---- + + if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { + runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; + requestFocus(); + screen->forceDisplay(); + return true; + } + // Confirm select (Enter) + bool isSelect = isSelectEvent(event); + if (isSelect) { + LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, + freetext.c_str()); + if (dest == 0) + dest = NODENUM_BROADCAST; + // Defensive: If channel isn't valid, pick the first available channel + if (channel >= channels.getNumChannels()) + channel = 0; + + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Backspace + if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { + payload = 0x08; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Move cursor left + if (event->inputEvent == INPUT_BROKER_LEFT) { + payload = INPUT_BROKER_LEFT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + // Move cursor right + if (event->inputEvent == INPUT_BROKER_RIGHT) { + payload = INPUT_BROKER_RIGHT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || + (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + // Tab (switch destination) + if (event->kbchar == INPUT_BROKER_MSG_TAB) { + return handleTabSwitch(event); // Reuse tab logic + } + + // Printable ASCII (add char to draft) + if (event->kbchar >= 32 && event->kbchar <= 126) { + payload = event->kbchar; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + return false; +} + +int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) +{ + int numEmotes = graphics::numEmotes; + + // Override isDown and isSelect ONLY for emote picker behavior + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + // Scroll emote list + if (isUp && emotePickerIndex > 0) { + emotePickerIndex--; + screen->forceDisplay(); + return 1; + } + if (isDown && emotePickerIndex < numEmotes - 1) { + emotePickerIndex++; + screen->forceDisplay(); + return 1; + } + + // Select emote: insert into freetext at cursor and return to freetext + if (isSelect) { + String label = graphics::emotes[emotePickerIndex].label; + String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" + if (cursor == freetext.length()) { + freetext += emoteInsert; + } else { + freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); + } + cursor += emoteInsert.length(); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + + // Cancel returns to freetext + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; } return 0; @@ -404,278 +826,198 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) { + // === Prepare packet === meshtastic_MeshPacket *p = allocDataPacket(); p->to = dest; p->channel = channel; p->want_ack = true; + + // Save destination for ACK/NACK UI fallback + this->lastSentNode = dest; + this->incoming = dest; + + // Copy message payload p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + // Optionally add bell character if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character - p->decoded.payload.size++; + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate } - // Only receive routing messages when expecting ACK for a canned message - // Prevents the canned message module from regenerating the screen's frameset at unexpected times, - // or raising a UIFrameEvent before another module has the chance + // Mark as waiting for ACK to trigger ACK/NACK screen 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); - service->sendToMesh( - p, RX_SRC_LOCAL, - true); // send to mesh, cc to phone. Even if there's no phone connected, this stores the message to match ACKs -} + // Send to mesh and phone (even if no phone connected, to track ACKs) + service->sendToMesh(p, RX_SRC_LOCAL, true); + // === Simulate local message to clear unread UI === + if (screen) { + meshtastic_MeshPacket simulatedPacket = {}; + simulatedPacket.from = 0; // Local device + screen->handleTextMessage(&simulatedPacket); + } + playComboTune(); +} int32_t CannedMessageModule::runOnce() { - if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { + updateDestinationSelectionList(); + needsUpdate = false; + } + + // If we're in node selection, do nothing except keep alive + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + return INACTIVATE_AFTER_MS; + } + + // Normal module disable/idle handling + if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; return INT32_MAX; } - // LOG_DEBUG("Check status"); + UIFrameEvent e; if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE)) { - // TODO: might have some feedback of sending state + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || + (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { - // Reset module - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + // Reset module on inactivity + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { - sendText(this->dest, indexChannels[this->channel], this->freetext.c_str(), true); + sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } else { + if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + return INT32_MAX; + } if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { - powerFSM.trigger(EVENT_PRESS); return INT32_MAX; } else { -#if defined(USE_VIRTUAL_KEYBOARD) - sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); -#else - sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); -#endif + sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); } this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { - // LOG_DEBUG("Reset message is empty"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->notifyObservers(&e); return 2000; - } else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - this->currentMessageIndex = 0; - LOG_DEBUG("First touch (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + } + // Always highlight the first real canned message when entering the message list + else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { + int firstRealMsgIdx = 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; + break; + } + } + this->currentMessageIndex = firstRealMsgIdx; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { if (this->messagesCount > 0) { this->currentMessageIndex = getPrevIndex(); - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { - case INPUT_BROKER_MSG_LEFT: - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (this->dest == NODENUM_BROADCAST) { - this->dest = nodeDB->getNodeNum(); - } - for (unsigned int i = 0; i < numMeshNodes; i++) { - if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { - this->dest = - (i > 0) ? nodeDB->getMeshNodeByIndex(i - 1)->num : nodeDB->getMeshNodeByIndex(numMeshNodes - 1)->num; - break; - } - } - if (this->dest == nodeDB->getNodeNum()) { - this->dest = NODENUM_BROADCAST; - } - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - for (unsigned int i = 0; i < channels.getNumChannels(); i++) { - if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || - (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { - indexChannels[numChannels] = i; - numChannels++; - } - } - if (this->channel == 0) { - this->channel = numChannels - 1; - } else { - this->channel--; - } - } else { - if (this->cursor > 0) { - this->cursor--; - } + case INPUT_BROKER_LEFT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { + this->cursor--; } break; - case INPUT_BROKER_MSG_RIGHT: - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (this->dest == NODENUM_BROADCAST) { - this->dest = nodeDB->getNodeNum(); - } - for (unsigned int i = 0; i < numMeshNodes; i++) { - if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { - this->dest = - (i < numMeshNodes - 1) ? nodeDB->getMeshNodeByIndex(i + 1)->num : nodeDB->getMeshNodeByIndex(0)->num; - break; - } - } - if (this->dest == nodeDB->getNodeNum()) { - this->dest = NODENUM_BROADCAST; - } - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - for (unsigned int i = 0; i < channels.getNumChannels(); i++) { - if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || - (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { - indexChannels[numChannels] = i; - numChannels++; - } - } - if (this->channel == numChannels - 1) { - this->channel = 0; - } else { - this->channel++; - } - } else { - if (this->cursor < this->freetext.length()) { - this->cursor++; - } + case INPUT_BROKER_RIGHT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { + this->cursor++; } break; default: break; } if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the - // display back to the default window - case 0x08: // backspace - if (this->freetext.length() > 0 && this->highlight == 0x00) { - if (this->cursor == this->freetext.length()) { - this->freetext = this->freetext.substring(0, this->freetext.length() - 1); - } else { - this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + switch (this->payload) { + case 0x08: // backspace + if (this->freetext.length() > 0) { + if (this->cursor > 0) { + if (this->cursor == this->freetext.length()) { + this->freetext = this->freetext.substring(0, this->freetext.length() - 1); + } else { + this->freetext = this->freetext.substring(0, this->cursor - 1) + + this->freetext.substring(this->cursor, this->freetext.length()); + } + this->cursor--; } - this->cursor--; - } - break; - case 0x09: // tab - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL; } else { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; } break; - case INPUT_BROKER_MSG_LEFT: - case INPUT_BROKER_MSG_RIGHT: - // already handled above - break; - // handle fn+s for shutdown - case INPUT_BROKER_MSG_SHUTDOWN: - if (screen) - screen->startAlert("Shutting down..."); - shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - break; - // and fn+r for reboot - case INPUT_BROKER_MSG_REBOOT: - if (screen) - screen->startAlert("Rebooting..."); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler + return 0; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: break; default: - if (this->highlight != 0x00) { - break; - } - - if (this->cursor == this->freetext.length()) { - this->freetext += this->payload; - } else { - this->freetext = - this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); - } - - this->cursor += 1; - - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); - if (this->freetext.length() > maxChars) { - this->cursor = maxChars; - this->freetext = this->freetext.substring(0, maxChars); + // Only insert ASCII printable characters (32โ€“126) + if (this->payload >= 32 && this->payload <= 126) { + requestFocus(); + if (this->cursor == this->freetext.length()) { + this->freetext += (char)this->payload; + } else { + this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + + this->freetext.substring(this->cursor); + } + this->cursor++; + uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + if (this->freetext.length() > maxChars) { + this->cursor = maxChars; + this->freetext = this->freetext.substring(0, maxChars); + } } break; } - if (screen) - screen->removeFunctionSymbol("Fn"); } - this->lastTouchMillis = millis(); this->notifyObservers(&e); return INACTIVATE_AFTER_MS; @@ -686,7 +1028,6 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); return INACTIVATE_AFTER_MS; } - return INT32_MAX; } @@ -709,29 +1050,21 @@ const char *CannedMessageModule::getMessageByIndex(int index) const char *CannedMessageModule::getNodeName(NodeNum node) { - if (node == NODENUM_BROADCAST) { + if (node == NODENUM_BROADCAST) return "Broadcast"; - } else { - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info != NULL) { - return info->user.long_name; - } else { - return "Unknown"; - } + + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user && 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 CannedMessageModule::shouldDraw() { - if (!moduleConfig.canned_message.enabled && !CANNED_MESSAGE_MODULE_ENABLE) { - return false; - } - - // If using "scan and select" input, don't draw the module frame just to say "disabled" - // The scanAndSelectInput class will draw its own temporary alert for user, when the input button is pressed - else if (scanAndSelectInput != nullptr && !hasMessages()) - return false; - return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE); } @@ -765,7 +1098,7 @@ void CannedMessageModule::showTemporaryMessage(const String &message) UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen notifyObservers(&e); - runState = CANNED_MESSAGE_RUN_STATE_MESSAGE; + runState = CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION; // run this loop again in 2 seconds, next iteration will clear the display setIntervalFromNow(2000); } @@ -983,188 +1316,656 @@ bool CannedMessageModule::interceptingKeyboardInput() } } -#if !HAS_TFT +// Draw the node/channel selection screen +void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + requestFocus(); + display->setColor(WHITE); // Always draw cleanly + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + int titleY = 2; + String titleText = "Select Destination"; + titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === List Items === + int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); + int numActiveChannels = this->activeChannelIndices.size(); + int totalEntries = numActiveChannels + this->filteredNodes.size(); + int columns = 1; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); + if (this->visibleRows < 1) + this->visibleRows = 1; + + // === Clamp scrolling === + if (scrollIndex > totalEntries / columns) + scrollIndex = totalEntries / columns; + if (scrollIndex < 0) + scrollIndex = 0; + + for (int row = 0; row < visibleRows; row++) { + int itemIndex = scrollIndex + row; + if (itemIndex >= totalEntries) + break; + + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + char entryText[64] = ""; + + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node) { + if (node->is_favorite) { + snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); + } else { + snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); + } + } + } + } + + if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) + strcpy(entryText, "?"); + + // === Highlight background (if selected) === + if (itemIndex == destIndex) { + int scrollPadding = 8; // Reserve space for scrollbar + display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); + } + + // === Draw entry text === + display->drawString(xOffset + 2, yOffset, entryText); + display->setColor(WHITE); + + // === Draw key icon (after highlight) === + if (itemIndex >= numActiveChannels) { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && hasKeyForNode(node)) { + int iconX = display->getWidth() - key_symbol_width - 15; + int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; + + if (itemIndex == destIndex) { + display->setColor(INVERSE); + } else { + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); + } + } + } + } + + // Scrollbar + if (totalEntries > visibleRows) { + int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); + int totalScrollable = totalEntries; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); + int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; + int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; + display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); + } +} + +void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height + const int headerMargin = 2; // Extra pixels below header + const int labelGap = 6; + const int bitmapGapX = 4; + + // Find max emote height (assume all same, or precalculated) + int maxEmoteHeight = 0; + for (int i = 0; i < graphics::numEmotes; ++i) + if (graphics::emotes[i].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[i].height; + + const int rowHeight = maxEmoteHeight + 2; + + // Place header at top, then compute start of emote list + int headerY = y; + int listTop = headerY + headerFontHeight + headerMargin; + + int visibleRows = (display->getHeight() - listTop - 2) / rowHeight; + int numEmotes = graphics::numEmotes; + + // Clamp highlight index + if (emotePickerIndex < 0) + emotePickerIndex = 0; + if (emotePickerIndex >= numEmotes) + emotePickerIndex = numEmotes - 1; + + // Determine which emote is at the top + int topIndex = emotePickerIndex - visibleRows / 2; + if (topIndex < 0) + topIndex = 0; + if (topIndex > numEmotes - visibleRows) + topIndex = std::max(0, numEmotes - visibleRows); + + // Draw header/title + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, headerY, "Select Emote"); + + // Draw emote rows + display->setTextAlignment(TEXT_ALIGN_LEFT); + + for (int vis = 0; vis < visibleRows; ++vis) { + int emoteIdx = topIndex + vis; + if (emoteIdx >= numEmotes) + break; + const graphics::Emote &emote = graphics::emotes[emoteIdx]; + int rowY = listTop + vis * rowHeight; + + // Draw highlight box 2px taller than emote (1px margin above and below) + if (emoteIdx == emotePickerIndex) { + display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); + display->setColor(BLACK); + } + + // Emote bitmap (left), 1px margin from highlight bar top + int emoteY = rowY + 1; + display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + + // Emote label (right of bitmap) + display->setFont(FONT_MEDIUM); + int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); + display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + + if (emoteIdx == emotePickerIndex) + display->setColor(WHITE); + } + + // Draw scrollbar if needed + if (numEmotes > visibleRows) { + int scrollbarHeight = visibleRows * rowHeight; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); + int scrollBarLen = std::max(6, (scrollbarHeight * visibleRows) / numEmotes); + int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; + display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); + } +} + void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + this->displayHeight = display->getHeight(); // Store display height for later use char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // === Draw temporary message if available === if (temporaryMessage.length() != 0) { requestFocus(); // Tell Screen::setFrames to move to our module's frame LOG_DEBUG("Draw temporary message: %s", temporaryMessage.c_str()); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious + return; + } -#ifdef USE_EINK - display->setFont(FONT_SMALL); // No chunky text -#else - display->setFont(FONT_MEDIUM); // Chunky text -#endif + // === Emote Picker Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here + return; + } - String displayString; - display->setTextAlignment(TEXT_ALIGN_CENTER); - if (this->ack) { - displayString = "Delivered to\n%s"; - } else { - displayString = "Delivery failed\nto %s"; - } - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, - cannedMessageModule->getNodeName(this->incoming)); + // === Destination Selection === + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + drawDestinationSelectionScreen(display, state, x, y); + return; + } - display->setFont(FONT_SMALL); - - String snrString = "Last Rx SNR: %f"; - String rssiString = "Last Rx RSSI: %d"; - - // Don't bother drawing snr and rssi for tiny displays - if (display->getHeight() > 100) { - - // Original implementation used constants of y = 100 and y = 130. Shrink this if screen is *slightly* small - int16_t snrY = 100; - int16_t rssiY = 130; - - // If dislay is *slighly* too small for the original consants, squish up a bit - if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) { - snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL); - rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL); - } - - if (this->ack) { - display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, snrString, this->lastRxSnr); - display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, rssiString, this->lastRxRssi); - } - } - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - // E-Ink: clean the screen *after* this pop-up + // === ACK/NACK Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { + requestFocus(); EINK_ADD_FRAMEFLAG(display, COSMETIC); - - requestFocus(); // Tell Screen::setFrames to move to our module's frame + display->setTextAlignment(TEXT_ALIGN_CENTER); #ifdef USE_EINK - display->setFont(FONT_SMALL); // No chunky text + display->setFont(FONT_SMALL); + int yOffset = y + 10; #else - display->setFont(FONT_MEDIUM); // Chunky text + display->setFont(FONT_MEDIUM); + int yOffset = y + 10; #endif + // --- Delivery Status Message --- + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel)); + } else if (this->lastAckHopLimit > this->lastAckHopStart) { + snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", this->lastAckHopLimit - this->lastAckHopStart, + getNodeName(this->incoming)); + } else { + snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming)); + } + } else { + snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming)); + } + + // Draw delivery message and compute y-offset after text height + int lineCount = 1; + for (const char *ptr = buffer; *ptr; ptr++) { + if (*ptr == '\n') + lineCount++; + } + + display->drawString(display->getWidth() / 2 + x, yOffset, buffer); + yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding + +#ifndef USE_EINK + // --- SNR + RSSI Compact Line --- + if (this->ack) { + display->setFont(FONT_SMALL); + snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); + display->drawString(display->getWidth() / 2 + x, yOffset, buffer); + } +#endif + + return; + } + + // === Sending Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { + EINK_ADD_FRAMEFLAG(display, COSMETIC); + requestFocus(); +#ifdef USE_EINK + display->setFont(FONT_SMALL); +#else + display->setFont(FONT_MEDIUM); +#endif display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending..."); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + return; + } + + // === Disabled Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame + return; + } + + // === Free Text Input Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) EInkDynamicDisplay *einkDisplay = static_cast(display); - einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing + einkDisplay->enableUnlimitedFastMode(); #endif - #if defined(USE_VIRTUAL_KEYBOARD) drawKeyboard(display, state, 0, 0); #else - display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - switch (this->destSelect) { - case CANNED_MESSAGE_DESTINATION_TYPE_NODE: - display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - break; - case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: - display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - break; - default: - if (display->getWidth() > 128) { - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - } else { - display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - } - break; - } - // used chars right aligned, only when not editing the destination - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + + // --- Draw node/channel header at the top --- + drawHeader(display, x, y, buffer); + + // --- Char count right-aligned --- + if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } + + // --- Draw Free Text input with multi-emote support and proper line wrapping --- display->setColor(WHITE); - display->drawStringMaxWidth( - 0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), - cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); -#endif - } else { - if (this->messagesCount > 0) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest)); - int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; - if (lines == 3) { - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); - if (this->messagesCount > 1) { - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage()); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage()); + { + int inputY = 0 + y + FONT_HEIGHT_SMALL; + String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); + + // Tokenize input into (isEmote, token) pairs + std::vector> tokens; + const char *msg = msgWithCursor.c_str(); + int msgLen = strlen(msg); + int pos = 0; + while (pos < msgLen) { + const graphics::Emote *foundEmote = nullptr; + int foundLen = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) + continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; + } + } } - } else { - int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; - for (int i = 0; i < std::min(messagesCount, lines); i++) { - if (i == currentMessageIndex - topMsg) { -#ifdef USE_EINK - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">"); - display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), - cannedMessageModule->getCurrentMessage()); -#else - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), - y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); -#endif - } else if (messagesCount > 1) { // Only draw others if there are multiple messages - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), - cannedMessageModule->getMessageByIndex(topMsg + i)); + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; } } } + + // ===== Advanced word-wrapping (emotes + text, split by word, wrap by char if needed) ===== + std::vector>> lines; + std::vector> currentLine; + int lineWidth = 0; + int maxWidth = display->getWidth(); + for (auto &token : tokens) { + if (token.first) { + // Emote + int tokenWidth = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + tokenWidth = graphics::emotes[j].width + 2; + break; + } + } + if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back(token); + lineWidth += tokenWidth; + } else { + // Text: split by words and wrap inside word if needed + String text = token.second; + uint16_t pos = 0; + while (pos < text.length()) { + // Find next space (or end) + int spacePos = text.indexOf(' ', pos); + int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space + String word = text.substring(pos, endPos); + int wordWidth = display->getStringWidth(word); + + if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + // If word itself too big, split by character + if (wordWidth > maxWidth) { + uint16_t charPos = 0; + while (charPos < word.length()) { + String oneChar = word.substring(charPos, charPos + 1); + int charWidth = display->getStringWidth(oneChar); + if (lineWidth + charWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back({false, oneChar}); + lineWidth += charWidth; + charPos++; + } + } else { + currentLine.push_back({false, word}); + lineWidth += wordWidth; + } + pos = endPos; + } + } + } + if (!currentLine.empty()) + lines.push_back(currentLine); + + // Draw lines with emotes + int rowHeight = FONT_HEIGHT_SMALL; + int yLine = inputY; + for (auto &line : lines) { + int nextX = x; + for (auto &token : line) { + if (token.first) { + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; + display->drawXbm(nextX, yLine + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; + } + } else { + display->drawString(nextX, yLine, token.second); + nextX += display->getStringWidth(token.second); + } + } + yLine += rowHeight; + } + } +#endif + return; + } + + // === Canned Messages List === + if (this->messagesCount > 0) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // ====== Precompute per-row heights based on emotes (centered if present) ====== + const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; + + int topMsg; + std::vector rowHeights; + 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; + + // 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); + + // --- Build per-row max height based on all emotes in line --- + for (int i = 0; i < countRows; i++) { + const char *msg = getMessageByIndex(topMsg + i); + int maxEmoteHeight = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *search = msg; + while ((search = strstr(search, label))) { + if (graphics::emotes[j].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[j].height; + search += strlen(label); // Advance past this emote + } + } + rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); + } + + // --- Draw all message rows with multi-emote support --- + int yCursor = listYOffset; + for (int vis = 0; vis < countRows; vis++) { + int msgIdx = topMsg + vis; + int lineY = yCursor; + const char *msg = getMessageByIndex(msgIdx); + int rowHeight = rowHeights[vis]; + bool highlight = (msgIdx == currentMessageIndex); + + // --- Multi-emote tokenization --- + std::vector> tokens; // (isEmote, token) + int pos = 0; + int msgLen = strlen(msg); + while (pos < msgLen) { + const graphics::Emote *foundEmote = nullptr; + int foundLen = 0; + + // Look for any emote label at this pos (prefer longest match) + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) + continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; + } + } + } + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (label[0] == 0) + continue; + const char *found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; + } + } + } + // --- End multi-emote tokenization --- + + // Vertically center based on rowHeight + int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; + +#ifdef USE_EINK + int nextX = x + (highlight ? 12 : 0); + if (highlight) + display->drawString(x + 0, lineY + textYOffset, ">"); +#else + int scrollPadding = 8; + if (highlight) { + display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); + display->setColor(BLACK); + } + int nextX = x + (highlight ? 2 : 0); +#endif + + // Draw all tokens left to right + for (auto &token : tokens) { + if (token.first) { + // Emote + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; + display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; + } + } else { + // Text + display->drawString(nextX, lineY + textYOffset, token.second); + nextX += display->getStringWidth(token.second); + } + } +#ifndef USE_EINK + if (highlight) + display->setColor(WHITE); +#endif + + yCursor += rowHeight; + } + + // Scrollbar + 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 scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; + display->fillRect(scrollTrackX, scrollPos, 4, barHeight); } } } -#endif //! HAS_TFT ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { - // look for a request_id if (mp.decoded.request_id != 0) { + // Trigger screen refresh for ACK/NACK feedback UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + requestFocus(); this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id); + + // Decode the routing response meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE; - waitingForAck = false; // No longer want routing packets + + // Track hop metadata + this->lastAckWasRelayed = (mp.hop_limit != mp.hop_start); + this->lastAckHopStart = mp.hop_start; + this->lastAckHopLimit = mp.hop_limit; + + // Determine ACK status + bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); + bool isFromDest = (mp.from == this->lastSentNode); + bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + + // Identify the responding node + if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { + this->incoming = mp.from; // Relayed by another node + } else { + this->incoming = this->lastSentNode; // Direct reply + } + + // Final ACK confirmation logic + this->ack = isAck && (wasBroadcast || isFromDest); + + waitingForAck = false; this->notifyObservers(&e); - // run the next time 2 seconds later - setIntervalFromNow(2000); + setIntervalFromNow(3000); // Time to show ACK/NACK screen } } @@ -1206,7 +2007,7 @@ bool CannedMessageModule::saveProtoForModule() */ void CannedMessageModule::installDefaultCannedMessageModuleConfig() { - memset(cannedMessageModuleConfig.messages, 0, sizeof(cannedMessageModuleConfig.messages)); + strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); } /** diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index a91933a0f..55a0a1185 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -3,27 +3,38 @@ #include "ProtobufModule.h" #include "input/InputBroker.h" +// ============================ +// Enums & Defines +// ============================ + enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_DISABLED, CANNED_MESSAGE_RUN_STATE_INACTIVE, CANNED_MESSAGE_RUN_STATE_ACTIVE, - CANNED_MESSAGE_RUN_STATE_FREETEXT, CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, - CANNED_MESSAGE_RUN_STATE_MESSAGE, CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, CANNED_MESSAGE_RUN_STATE_ACTION_UP, CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, -}; - -enum cannedMessageDestinationType { - CANNED_MESSAGE_DESTINATION_TYPE_NONE, - CANNED_MESSAGE_DESTINATION_TYPE_NODE, - CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL + CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, + CANNED_MESSAGE_RUN_STATE_FREETEXT, + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, + CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; +#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 +#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 + +#ifndef CANNED_MESSAGE_MODULE_ENABLE +#define CANNED_MESSAGE_MODULE_ENABLE 0 +#endif + +// ============================ +// Data Structures +// ============================ + struct Letter { String character; float width; @@ -33,71 +44,72 @@ struct Letter { int rectHeight; }; -#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 -/** - * Sum of CannedMessageModuleConfig part sizes. - */ -#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 +struct NodeEntry { + meshtastic_NodeInfoLite *node; + uint32_t lastHeard; +}; -#ifndef CANNED_MESSAGE_MODULE_ENABLE -#define CANNED_MESSAGE_MODULE_ENABLE 0 -#endif +// ============================ +// Main Class +// ============================ class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread { - CallbackObserver inputObserver = - CallbackObserver(this, &CannedMessageModule::handleInputEvent); - public: CannedMessageModule(); + + void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); + void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); + + // === Emote Picker navigation === + int emotePickerIndex = 0; // Tracks currently selected emote in the picker + + // === Message navigation === const char *getCurrentMessage(); const char *getPrevMessage(); const char *getNextMessage(); const char *getMessageByIndex(int index); const char *getNodeName(NodeNum node); + + // === State/UI === bool shouldDraw(); bool hasMessages(); - // void eventUp(); - // void eventDown(); - // void eventSelect(); + void showTemporaryMessage(const String &message); + void resetSearch(); + void updateDestinationSelectionList(); + void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + bool isCharInputAllowed() const; + String drawWithCursor(String text, int cursor); + // === Emote Picker === + int handleEmotePickerInput(const InputEvent *event); + void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + // === Admin Handlers === void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); void handleSetCannedMessageModuleMessages(const char *from_msg); - void showTemporaryMessage(const String &message); - - String drawWithCursor(String text, int cursor); - #ifdef RAK14014 cannedMessageModuleRunState getRunState() const { return runState; } #endif - /* - -Override the wantPacket method. We need the Routing Messages to look for ACKs. - */ + // === Packet Interest Filter === virtual bool wantPacket(const meshtastic_MeshPacket *p) override { - if (p->rx_rssi != 0) { - this->lastRxRssi = p->rx_rssi; - } - - if (p->rx_snr > 0) { - this->lastRxSnr = p->rx_snr; - } - - switch (p->decoded.portnum) { - case meshtastic_PortNum_ROUTING_APP: - return waitingForAck; - default: - return false; - } + if (p->rx_rssi != 0) + lastRxRssi = p->rx_rssi; + if (p->rx_snr > 0) + lastRxSnr = p->rx_snr; + return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; } protected: + // === Thread Entry Point === virtual int32_t runOnce() override; + // === Transmission === void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); - + void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); int splitConfiguredMessages(); int getNextIndex(); int getPrevIndex(); @@ -105,58 +117,87 @@ class CannedMessageModule : public SinglePortModule, public ObservableshouldDraw(); } + virtual bool wantUIFrame() override { return shouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } virtual bool interceptingKeyboardInput() override; -#if !HAS_TFT virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; -#endif virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; - /** Called to handle a particular incoming message - * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered - * for it - */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; void loadProtoForModule(); bool saveProtoForModule(); - void installDefaultCannedMessageModuleConfig(); - int currentMessageIndex = -1; - cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - char payload = 0x00; - unsigned int cursor = 0; - String freetext = ""; // Text Buffer for Freetext Editor - NodeNum dest = NODENUM_BROADCAST; - ChannelIndex channel = 0; - cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - uint8_t numChannels = 0; - ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0}; - NodeNum incoming = NODENUM_BROADCAST; - bool ack = false; // True means ACK, false means NAK (error_reason != NONE) - bool waitingForAck = false; // Are currently interested in routing packets? - float lastRxSnr = 0; - int32_t lastRxRssi = 0; + private: + // === Input Observers === + CallbackObserver inputObserver = + CallbackObserver(this, &CannedMessageModule::handleInputEvent); + // === Display and UI === + int displayHeight = 64; + int destIndex = 0; + int scrollIndex = 0; + int visibleRows = 0; + bool needsUpdate = true; + unsigned long lastUpdateMillis = 0; + String searchQuery; + String freetext; + String temporaryMessage; + + // === Message Storage === char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; int messagesCount = 0; + int currentMessageIndex = -1; + + // === Routing & Acknowledgment === + NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) + NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received + NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) + ChannelIndex channel = 0; // Channel index used when sending a message + + bool ack = false; // True = ACK received, False = NACK or failed + bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets + bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes + uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet + uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet + + float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) + int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) + + // === State Tracking === + cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + char highlight = 0x00; + char payload = 0x00; + unsigned int cursor = 0; unsigned long lastTouchMillis = 0; - String temporaryMessage; + uint32_t lastFilterUpdate = 0; + static constexpr uint32_t filterDebounceMs = 30; + std::vector activeChannelIndices; + std::vector filteredNodes; + +#if defined(USE_VIRTUAL_KEYBOARD) + bool shift = false; + int charSet = 0; // 0=ABC, 1=123 +#endif + + bool isUpEvent(const InputEvent *event); + bool isDownEvent(const InputEvent *event); + bool isSelectEvent(const InputEvent *event); + bool handleTabSwitch(const InputEvent *event); + int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleFreeTextInput(const InputEvent *event); #if defined(USE_VIRTUAL_KEYBOARD) Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index dc17460f6..5d7233279 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -188,7 +188,7 @@ int32_t ExternalNotificationModule::runOnce() // Play RTTTL over i2s audio interface if enabled as buzzer #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { + if (moduleConfig.external_notification.use_i2s_as_buzzer && canBuzz()) { if (audioThread->isPlaying()) { // Continue playing } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -197,7 +197,7 @@ int32_t ExternalNotificationModule::runOnce() } #endif // now let the PWM buzzer play - if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio) { + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { if (rtttl::isPlaying()) { rtttl::play(); } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -210,6 +210,18 @@ int32_t ExternalNotificationModule::runOnce() } } +/** + * Based on buzzer mode, return true if we can buzz. + */ +bool ExternalNotificationModule::canBuzz() +{ + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + return true; + } + return false; +} + bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); @@ -293,6 +305,12 @@ bool ExternalNotificationModule::getExternal(uint8_t index) return externalCurrentState[index]; } +// Allow other firmware components to determine whether a notification is ongoing +bool ExternalNotificationModule::nagging() +{ + return isNagging; +} + void ExternalNotificationModule::stopNow() { rtttl::stop(); @@ -338,12 +356,14 @@ ExternalNotificationModule::ExternalNotificationModule() // moduleConfig.external_notification.alert_message_buzzer = true; if (moduleConfig.external_notification.enabled) { + if (inputBroker) // put our callback in the inputObserver list + inputObserver.observe(inputBroker); + if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); - strncpy(rtttlConfig.ringtone, - "24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p", - sizeof(rtttlConfig.ringtone)); + // The default ringtone is always loaded from userPrefs.jsonc + strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone)); } LOG_INFO("Init External Notification Module"); @@ -364,7 +384,7 @@ ExternalNotificationModule::ExternalNotificationModule() setExternalState(1, false); externalTurnedOn[1] = 0; } - if (moduleConfig.external_notification.output_buzzer) { + if (moduleConfig.external_notification.output_buzzer && canBuzz()) { if (!moduleConfig.external_notification.use_pwm) { LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); @@ -454,7 +474,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_bell_buzzer) { + if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { if (containsBell) { LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); isNagging = true; @@ -583,4 +603,13 @@ void ExternalNotificationModule::handleSetRingtone(const char *from_msg) if (changed) { nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); } +} + +int ExternalNotificationModule::handleInputEvent(const InputEvent *event) +{ + if (nagCycleCutoff != UINT32_MAX) { + stopNow(); + return 1; + } + return 0; } \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index 841ca6de9..19cf9eb7b 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -3,6 +3,8 @@ #include "SinglePortModule.h" #include "concurrency/OSThread.h" #include "configuration.h" +#include "input/InputBroker.h" + #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) #include #else @@ -27,11 +29,15 @@ class rtttl */ class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread { + CallbackObserver inputObserver = + CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); uint32_t output = 0; public: ExternalNotificationModule(); + int handleInputEvent(const InputEvent *arg); + uint32_t nagCycleCutoff = 1; void setExternalState(uint8_t index = 0, bool on = false); @@ -40,6 +46,9 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: void setMute(bool mute) { isMuted = mute; } bool getMute() { return isMuted; } + bool canBuzz(); + bool nagging(); + void stopNow(); void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp new file mode 100644 index 000000000..3b8225763 --- /dev/null +++ b/src/modules/KeyVerificationModule.cpp @@ -0,0 +1,313 @@ +#if !MESHTASTIC_EXCLUDE_PKI +#include "KeyVerificationModule.h" +#include "MeshService.h" +#include "RTC.h" +#include "graphics/draw/MenuHandler.h" +#include "main.h" +#include "meshUtils.h" +#include "modules/AdminModule.h" +#include + +KeyVerificationModule *keyVerificationModule; + +KeyVerificationModule::KeyVerificationModule() + : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) +{ + ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; +} + +AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + updateState(); + if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { + LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); + + if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && + currentState == KEY_VERIFICATION_IDLE) { + sendInitialRequest(request->key_verification.remote_nodenum); + + } else if (request->key_verification.message_type == + meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && + request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && + request->key_verification.nonce == currentNonce) { + processSecurityNumber(request->key_verification.security_number); + + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && + request->key_verification.nonce == currentNonce) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + resetToIdle(); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { + resetToIdle(); + } + return AdminMessageHandleResult::HANDLED; + } + return AdminMessageHandleResult::NOT_HANDLED; +} + +bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) +{ + updateState(); + if (mp.pki_encrypted == false) { + return false; + } + if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply() + return false; + } + if (currentState == KEY_VERIFICATION_IDLE) { + return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() + } + + if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && + r->hash1.size == 0) { + memcpy(hash2, r->hash2.bytes, 32); + IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void { + keyVerificationModule->processSecurityNumber(number_picked); + });) + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Enter Security Number for Key Verification"); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; + cn->payload_variant.key_verification_number_request.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + service->sendClientNotification(cn); + LOG_INFO("Received hash2"); + currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; + return true; + + } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { + if (memcmp(hash1, r->hash1.bytes, 32) == 0) { + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); + LOG_INFO("Hash1 matches!"); + static const char *optionsArray[] = {"Reject", "Accept"}; + // Don't try to put the array definition in the macro. Does not work with curly braces. + IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; + options.optionsArrayPtr = optionsArray; options.optionsCount = 2; + options.notificationType = graphics::notificationTypeEnum::selection_picker; + options.bannerCallback = + [=](int selected) { + if (selected == 1) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + }; + screen->showOverlayBanner(options);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = false; + service->sendClientNotification(cn); + + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; + return true; + } + } + return false; +} + +bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) +{ + LOG_DEBUG("keyVerification start"); + // generate nonce + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { + IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) + return false; + } + currentNonce = random(); + currentNonceTimestamp = getTime(); + currentRemoteNode = remoteNode; + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 0; + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = remoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + + currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; + return true; +} + +meshtastic_MeshPacket *KeyVerificationModule::allocReply() +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period + LOG_WARN("Key Verification requested, but already in a request"); + return nullptr; + } else if (!currentRequest->pki_encrypted) { + LOG_WARN("Key Verification requested, but not in a PKI packet"); + return nullptr; + } + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; + + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_KeyVerification scratch; + meshtastic_KeyVerification response; + meshtastic_MeshPacket *responsePacket = nullptr; + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); + + currentNonce = scratch.nonce; + response.nonce = scratch.nonce; + currentRemoteNode = req.from; + currentNonceTimestamp = getTime(); + currentSecurityNumber = random(1, 999999); + + // generate hash1 + hash.reset(); + hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); + hash.update(owner.public_key.bytes, owner.public_key.size); + hash.finalize(hash1, 32); + + // generate hash2 + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(hash2, 32); + response.hash1.size = 0; + response.hash2.size = 32; + memcpy(response.hash2.bytes, hash2, 32); + + responsePacket = allocDataProtobuf(response); + + responsePacket->pki_encrypted = true; + IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); + screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, + currentSecurityNumber % 1000); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; + cn->payload_variant.key_verification_number_inform.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); + cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; + service->sendClientNotification(cn); + LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); + return responsePacket; +} + +void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + uint8_t scratch_hash[32] = {0}; + LOG_WARN("received security number: %u", incomingNumber); + meshtastic_NodeInfoLite *remoteNodePtr = nullptr; + remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { + currentState = KEY_VERIFICATION_IDLE; + return; // should we throw an error here? + } + LOG_WARN("hashing "); + // calculate hash1 + hash.reset(); + hash.update(&incomingNumber, sizeof(incomingNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(owner.public_key.bytes, owner.public_key.size); + + hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); + hash.finalize(hash1, 32); + + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(scratch_hash, 32); + + if (memcmp(scratch_hash, hash2, 32) != 0) { + LOG_WARN("Hash2 did not match"); + return; // should probably throw an error of some sort + } + currentSecurityNumber = incomingNumber; + + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 32; + memcpy(KeyVerification.hash1.bytes, hash1, 32); + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = currentRemoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; + IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = true; + service->sendClientNotification(cn); + LOG_INFO(message); + + return; +} + +void KeyVerificationModule::updateState() +{ + if (currentState != KEY_VERIFICATION_IDLE) { + // check for the 60 second timeout + if (currentNonceTimestamp < getTime() - 60) { + resetToIdle(); + } else { + currentNonceTimestamp = getTime(); + } + } +} + +void KeyVerificationModule::resetToIdle() +{ + memset(hash1, 0, 32); + memset(hash2, 0, 32); + currentNonce = 0; + currentNonceTimestamp = 0; + currentSecurityNumber = 0; + currentRemoteNode = 0; + currentState = KEY_VERIFICATION_IDLE; +} + +void KeyVerificationModule::generateVerificationCode(char *readableCode) +{ + for (int i = 0; i < 4; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } + readableCode[4] = ' '; + for (int i = 5; i < 9; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } +} +#endif \ No newline at end of file diff --git a/src/modules/KeyVerificationModule.h b/src/modules/KeyVerificationModule.h new file mode 100644 index 000000000..d5dba01d7 --- /dev/null +++ b/src/modules/KeyVerificationModule.h @@ -0,0 +1,65 @@ +#pragma once + +#include "ProtobufModule.h" +#include "SinglePortModule.h" + +enum KeyVerificationState { + KEY_VERIFICATION_IDLE, + KEY_VERIFICATION_SENDER_HAS_INITIATED, + KEY_VERIFICATION_SENDER_AWAITING_NUMBER, + KEY_VERIFICATION_SENDER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, +}; + +class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // +{ + // CallbackObserver nodeStatusObserver = + // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); + + public: + KeyVerificationModule(); + /* : concurrency::OSThread("KeyVerification"), + ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) + { + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + }*/ + virtual bool wantUIFrame() { return false; }; + bool sendInitialRequest(NodeNum remoteNode); + void generateVerificationCode(char *); // fills char with the user readable verification code + uint32_t getCurrentRemoteNode() { return currentRemoteNode; } + + protected: + /* Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); + // virtual meshtastic_MeshPacket *allocReply() override; + + // rather than add to the craziness that is the admin module, just handle those requests here. + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + /* + * Send our Telemetry into the mesh + */ + bool sendMetrics(); + virtual meshtastic_MeshPacket *allocReply() override; + + private: + uint64_t currentNonce = 0; + uint32_t currentNonceTimestamp = 0; + NodeNum currentRemoteNode = 0; + uint32_t currentSecurityNumber = 0; + KeyVerificationState currentState = KEY_VERIFICATION_IDLE; + uint8_t hash1[32] = {0}; // + uint8_t hash2[32] = {0}; // + char message[40] = {0}; + + void processSecurityNumber(uint32_t); + void updateState(); // check the timeouts and maybe reset the state to idle + void resetToIdle(); // Zero out module state +}; + +extern KeyVerificationModule *keyVerificationModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 001db1969..4437c6ee8 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -1,18 +1,22 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "buzz/BuzzerFeedbackThread.h" #include "input/ExpressLRSFiveWay.h" #include "input/InputBroker.h" #include "input/RotaryEncoderInterruptImpl1.h" -#include "input/ScanAndSelect.h" #include "input/SerialKeyboardImpl.h" #include "input/TrackballInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" +#include "modules/SystemCommandsModule.h" #if !MESHTASTIC_EXCLUDE_I2C #include "input/cardKbI2cImpl.h" #endif #include "input/kbMatrixImpl.h" #include "input/peMatrixImpl.h" #endif +#if !MESHTASTIC_EXCLUDE_PKI +#include "KeyVerificationModule.h" +#endif #if !MESHTASTIC_EXCLUDE_ADMIN #include "modules/AdminModule.h" #endif @@ -50,6 +54,7 @@ #endif #if ARCH_PORTDUINO #include "input/LinuxInputImpl.h" +#include "input/SeesawRotary.h" #include "modules/Telemetry/HostMetrics.h" #if !MESHTASTIC_EXCLUDE_STOREFORWARD #include "modules/StoreForwardModule.h" @@ -63,6 +68,7 @@ #include "modules/Telemetry/AirQualityTelemetry.h" #include "modules/Telemetry/EnvironmentTelemetry.h" #include "modules/Telemetry/HealthTelemetry.h" +#include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY #include "modules/Telemetry/PowerTelemetry.h" @@ -105,7 +111,11 @@ void setupModules() { if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - inputBroker = new InputBroker(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + inputBroker = new InputBroker(); + systemCommandsModule = new SystemCommandsModule(); + buzzerFeedbackThread = new BuzzerFeedbackThread(); + } #endif #if !MESHTASTIC_EXCLUDE_ADMIN adminModule = new AdminModule(); @@ -134,7 +144,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_ATAK atakPluginModule = new AtakPluginModule(); #endif - +#if !MESHTASTIC_EXCLUDE_PKI + keyVerificationModule = new KeyVerificationModule(); +#endif #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif @@ -153,55 +165,57 @@ void setupModules() // Example: Put your module here // new ReplyModule(); #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } - -#if HAS_SCREEN - // In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class - scanAndSelectInput = new ScanAndSelectInput(); - if (!scanAndSelectInput->init()) { - delete scanAndSelectInput; - scanAndSelectInput = nullptr; - } -#endif - - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); #ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE peMatrixImpl = new PeMatrixImpl(); peMatrixImpl->init(); #ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE - + } #endif // HAS_BUTTON -#if ARCH_PORTDUINO && !HAS_TFT - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; + } + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } #endif -#if HAS_TRACKBALL && !MESHTASTIC_EXCLUDE_INPUTBROKER - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); + } #endif #ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE expressLRSFiveWayInput = new ExpressLRSFiveWay(); #endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES - cannedMessageModule = new CannedMessageModule(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + cannedMessageModule = new CannedMessageModule(); + } #endif #if ARCH_PORTDUINO new HostMetricsModule(); @@ -209,11 +223,14 @@ void setupModules() #if HAS_TELEMETRY new DeviceTelemetryModule(); #endif +// TODO: How to improve this? #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR new EnvironmentTelemetryModule(); +#if __has_include("Adafruit_PM25AQI.h") if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { new AirQualityTelemetryModule(); } +#endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { @@ -227,7 +244,9 @@ void setupModules() #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL - new SerialModule(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + new SerialModule(); + } #endif #endif #ifdef ARCH_ESP32 diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index e072fcb0f..cf9940e25 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -21,13 +21,6 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes bool wasBroadcast = isBroadcast(mp.to); - // Show new nodes on LCD screen - if (wasBroadcast) { - String lcd = String("Joined: ") + p.long_name + "\n"; - if (screen) - screen->print(lcd.c_str()); - } - // if user has changed while packet was not for us, inform phone if (hasChanged && !wasBroadcast && !isToUs(&mp)) service->sendToPhone(packetPool.allocCopy(mp)); diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 0b1bdcc46..93c65ecc1 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -265,7 +265,6 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() } LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); - lastSentToMesh = millis(); // 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) @@ -276,13 +275,18 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() meshtastic_MeshPacket *PositionModule::allocReply() { - if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentToMesh && - Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) { - LOG_DEBUG("Skip Position reply since we sent it <3min ago"); + if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && + Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); ignoreRequest = true; // Mark it as ignored for MeshModule return nullptr; } - return allocPositionPacket(); + + meshtastic_MeshPacket *reply = allocPositionPacket(); + if (reply) { + lastSentReply = millis(); // Track when we sent this reply + } + return reply; } meshtastic_MeshPacket *PositionModule::allocAtakPli() @@ -328,7 +332,13 @@ void PositionModule::sendOurPosition() // If we changed channels, ask everyone else for their latest info LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); - sendOurPosition(NODENUM_BROADCAST, requestReplies); + for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { + if (channels.getByIndex(channelNum).settings.has_module_settings && + channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { + sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); + return; + } + } } void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) @@ -340,11 +350,6 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha // Set's the class precision value for this particular packet if (channels.getByIndex(channel).settings.has_module_settings) { precision = channels.getByIndex(channel).settings.module_settings.position_precision; - } else if (channels.getByIndex(channel).role == meshtastic_Channel_Role_PRIMARY) { - // backwards compatibility for Primary channels created before position_precision was set by default - precision = 13; - } else { - precision = 0; } meshtastic_MeshPacket *p = allocPositionPacket(); diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index dc732a3db..b9fd527c9 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -63,7 +63,7 @@ class PositionModule : public ProtobufModule, private concu void sendLostAndFoundText(); bool hasQualityTimesource(); bool hasGPS(); - uint32_t lastSentToMesh = 0; // Last time we sent our position to the mesh + uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); diff --git a/src/modules/RemoteHardwareModule.cpp b/src/modules/RemoteHardwareModule.cpp index 9bc8512b6..04cfeb651 100644 --- a/src/modules/RemoteHardwareModule.cpp +++ b/src/modules/RemoteHardwareModule.cpp @@ -83,9 +83,6 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r switch (p.type) { case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { - // Print notification to LCD screen - screen->print("Write GPIOs\n"); - pinModes(p.gpio_mask, OUTPUT, availablePins); for (uint8_t i = 0; i < NUM_GPIOS; i++) { uint64_t mask = 1ULL << i; @@ -98,10 +95,6 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r } case meshtastic_HardwareMessage_Type_READ_GPIOS: { - // Print notification to LCD screen - if (screen) - screen->print("Read GPIOs\n"); - uint64_t res = digitalReads(p.gpio_mask, availablePins); // Send the reply diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp index c4f63c6b1..8892aaa97 100644 --- a/src/modules/ReplyModule.cpp +++ b/src/modules/ReplyModule.cpp @@ -15,8 +15,6 @@ meshtastic_MeshPacket *ReplyModule::allocReply() LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); #endif - screen->print("Send reply\n"); - const char *replyStr = "Message Received"; auto reply = allocDataPacket(); // Allocate a packet for sending reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 8d280581c..f3921ef19 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -341,7 +341,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp serialPrint->write(p.payload.bytes, p.payload.size); } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - String sender = (node && node->has_user) ? node->user.short_name : "???"; + const char *sender = (node && node->has_user) ? node->user.short_name : "???"; serialPrint->println(); serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->println(); @@ -410,8 +410,8 @@ uint32_t SerialModule::getBaudRate() // Add this structure to help with parsing WindGust = 24.4 serial lines. struct ParsedLine { - String name; - String value; + char name[64]; + char value[128]; }; /** @@ -438,16 +438,30 @@ ParsedLine parseLine(const char *line) strncpy(nameBuf, line, nameLen); nameBuf[nameLen] = '\0'; - // Create trimmed name string - String name = String(nameBuf); - name.trim(); + // Trim whitespace from name + char *nameStart = nameBuf; + while (*nameStart && isspace(*nameStart)) + nameStart++; + char *nameEnd = nameStart + strlen(nameStart) - 1; + while (nameEnd > nameStart && isspace(*nameEnd)) + *nameEnd-- = '\0'; - // Extract value after equals sign - String value = String(equals + 1); - value.trim(); + // Copy trimmed name + strncpy(result.name, nameStart, sizeof(result.name) - 1); + result.name[sizeof(result.name) - 1] = '\0'; + + // Extract value part (after equals) + const char *valueStart = equals + 1; + while (*valueStart && isspace(*valueStart)) + valueStart++; + strncpy(result.value, valueStart, sizeof(result.value) - 1); + result.value[sizeof(result.value) - 1] = '\0'; + + // Trim trailing whitespace from value + char *valueEnd = result.value + strlen(result.value) - 1; + while (valueEnd > result.value && isspace(*valueEnd)) + *valueEnd-- = '\0'; - result.name = name; - result.value = value; return result; } @@ -517,16 +531,16 @@ void SerialModule::processWXSerial() memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); ParsedLine parsed = parseLine(line); - if (parsed.name.length() > 0) { - if (parsed.name == "WindDir") { - strlcpy(windDir, parsed.value.c_str(), sizeof(windDir)); + if (strlen(parsed.name) > 0) { + if (strcmp(parsed.name, "WindDir") == 0) { + strlcpy(windDir, parsed.value, sizeof(windDir)); double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); dir_sum_sin += sin(radians); dir_sum_cos += cos(radians); dirCount++; gotwind = true; - } else if (parsed.name == "WindSpeed") { - strlcpy(windVel, parsed.value.c_str(), sizeof(windVel)); + } else if (strcmp(parsed.name, "WindSpeed") == 0) { + strlcpy(windVel, parsed.value, sizeof(windVel)); float newv = strtof(windVel, nullptr); velSum += newv; velCount++; @@ -534,28 +548,28 @@ void SerialModule::processWXSerial() lull = newv; } gotwind = true; - } else if (parsed.name == "WindGust") { - strlcpy(windGust, parsed.value.c_str(), sizeof(windGust)); + } else if (strcmp(parsed.name, "WindGust") == 0) { + strlcpy(windGust, parsed.value, sizeof(windGust)); float newg = strtof(windGust, nullptr); if (newg > gust) { gust = newg; } gotwind = true; - } else if (parsed.name == "BatVoltage") { - strlcpy(batVoltage, parsed.value.c_str(), sizeof(batVoltage)); + } else if (strcmp(parsed.name, "BatVoltage") == 0) { + strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); batVoltageF = strtof(batVoltage, nullptr); break; // last possible data we want so break - } else if (parsed.name == "CapVoltage") { - strlcpy(capVoltage, parsed.value.c_str(), sizeof(capVoltage)); + } else if (strcmp(parsed.name, "CapVoltage") == 0) { + strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); capVoltageF = strtof(capVoltage, nullptr); - } else if (parsed.name == "GXTS04Temp" || parsed.name == "Temperature") { - strlcpy(temperature, parsed.value.c_str(), sizeof(temperature)); + } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { + strlcpy(temperature, parsed.value, sizeof(temperature)); temperatureF = strtof(temperature, nullptr); - } else if (parsed.name == "RainIntSum") { - strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr)); + } else if (strcmp(parsed.name, "RainIntSum") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); rainSum = int(strtof(rainStr, nullptr)); - } else if (parsed.name == "Rain") { - strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr)); + } else if (strcmp(parsed.name, "Rain") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); rain = strtof(rainStr, nullptr); } } diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp new file mode 100644 index 000000000..ab9439b39 --- /dev/null +++ b/src/modules/SystemCommandsModule.cpp @@ -0,0 +1,122 @@ +#include "SystemCommandsModule.h" +#include "meshUtils.h" +#if HAS_SCREEN +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#endif +#include "GPS.h" +#include "MeshService.h" +#include "Module.h" +#include "NodeDB.h" +#include "main.h" +#include "modules/AdminModule.h" +#include "modules/ExternalNotificationModule.h" + +SystemCommandsModule *systemCommandsModule; + +SystemCommandsModule::SystemCommandsModule() +{ + if (inputBroker) + inputObserver.observe(inputBroker); +} + +int SystemCommandsModule::handleInputEvent(const InputEvent *event) +{ + LOG_INFO("Input event %u! kb %u", event->inputEvent, event->kbchar); + // System commands (all others fall through) + switch (event->kbchar) { + // Fn key symbols + case INPUT_BROKER_MSG_FN_SYMBOL_ON: + IF_SCREEN(screen->setFunctionSymbol("Fn")); + return 0; + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: + IF_SCREEN(screen->removeFunctionSymbol("Fn")); + return 0; + // Brightness + case INPUT_BROKER_MSG_BRIGHTNESS_UP: + IF_SCREEN(screen->increaseBrightness()); + LOG_DEBUG("Increase Screen Brightness"); + return 0; + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: + IF_SCREEN(screen->decreaseBrightness()); + LOG_DEBUG("Decrease Screen Brightness"); + return 0; + // Mute + case INPUT_BROKER_MSG_MUTE_TOGGLE: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + bool isMuted = externalNotificationModule->getMute(); + externalNotificationModule->setMute(!isMuted); + IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow(); + screen->showSimpleBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + } + return 0; + // Bluetooth + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: + config.bluetooth.enabled = !config.bluetooth.enabled; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); +#if defined(ARDUINO_ARCH_NRF52) + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; + } else { + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#else + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#endif + return 0; + case INPUT_BROKER_MSG_REBOOT: + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; + } + + switch (event->inputEvent) { + // GPS + case INPUT_BROKER_GPS_TOGGLE: + LOG_WARN("GPS Toggle"); +#if !MESHTASTIC_EXCLUDE_GPS + if (gps) { + LOG_WARN("GPS Toggle2"); + gps->toggleGpsMode(); + const char *msg = + (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; + IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);) + } +#endif + return true; + // Mesh ping + case INPUT_BROKER_SEND_PING: + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } + 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; + return true; + + default: + // No other input events handled here + break; + } + return false; +} diff --git a/src/modules/SystemCommandsModule.h b/src/modules/SystemCommandsModule.h new file mode 100644 index 000000000..44910f443 --- /dev/null +++ b/src/modules/SystemCommandsModule.h @@ -0,0 +1,19 @@ +#pragma once + +#include "MeshModule.h" +#include "configuration.h" +#include "input/InputBroker.h" +#include +#include + +class SystemCommandsModule +{ + CallbackObserver inputObserver = + CallbackObserver(this, &SystemCommandsModule::handleInputEvent); + + public: + SystemCommandsModule(); + int handleInputEvent(const InputEvent *event); +}; + +extern SystemCommandsModule *systemCommandsModule; \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index fafb28699..2472b95b1 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "AirQualityTelemetry.h" diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 6d29fecb2..d1b10fa82 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -11,20 +11,27 @@ #include "RTC.h" #include "Router.h" #include "UnitConversions.h" +#include "buzz.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" #include "target_specific.h" #include -#include #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL -// Sensors +// Sensors #include "Sensor/CGRadSensSensor.h" #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); +} #if __has_include() #include "Sensor/AHT10.h" AHT10Sensor aht10Sensor; @@ -101,6 +108,13 @@ SHTC3Sensor shtc3Sensor; NullSensor shtc3Sensor; #endif +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 +#include "Sensor/RAK12035Sensor.h" +RAK12035Sensor rak12035Sensor; +#else +NullSensor rak12035Sensor; +#endif + #if __has_include() #include "Sensor/VEML7700Sensor.h" VEML7700Sensor veml7700Sensor; @@ -173,6 +187,7 @@ NullSensor pct2075Sensor; RCWL9620Sensor rcwl9620Sensor; CGRadSensSensor cgRadSens; + #endif #ifdef T1000X_SENSOR_EN #include "Sensor/T1000xSensor.h" @@ -182,6 +197,7 @@ T1000xSensor t1000xSensor; #include "Sensor/IndicatorSensor.h" IndicatorSensor indicatorSensor; #endif + #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true @@ -288,6 +304,11 @@ int32_t EnvironmentTelemetryModule::runOnce() result = rak9154Sensor.runOnce(); #endif +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 + if (rak12035Sensor.hasSensor()) { + result = rak12035Sensor.runOnce(); + } +#endif #endif } // it's possible to have this module enabled, only for displaying values on the screen. @@ -330,120 +351,152 @@ bool EnvironmentTelemetryModule::wantUIFrame() void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Setup display === + display->clear(); display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; - if (lastMeasurementPacket == nullptr) { - // If there's no valid packet, display "Environment" - display->drawString(x, y, "Environment"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + // === Set Title + const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); return; } - // Decode the last measurement packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); - + // Decode the telemetry message from the latest received packet const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); return; } - // Display "Env. From: ..." on its own - display->drawString(x, y, "Env. From: " + String(lastSender) + " (" + String(agoSecs) + "s)"); + const auto &m = telemetry.variant.environment_metrics; - // Prepare sensor data strings - String sensorData[10]; - int sensorCount = 0; + // Check if any telemetry field has valid data + bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || + m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; - if (lastMeasurement.variant.environment_metrics.has_temperature || - lastMeasurement.variant.environment_metrics.has_relative_humidity) { - String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "ยฐC"; - if (moduleConfig.telemetry.environment_display_fahrenheit) { - last_temp = - String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.environment_metrics.temperature), 0) + "ยฐF"; + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_temperature) { + String tempStr = moduleConfig.telemetry.environment_display_fahrenheit + ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "ยฐF" + : "Tmp: " + String(m.temperature, 1) + "ยฐC"; + entries.push_back(tempStr); + } + if (m.has_relative_humidity) + entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); + if (m.barometric_pressure != 0) + entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + const char *bannerMsg = nullptr; // Default: no banner + + if (m.iaq <= 25) + aqi += " (Excellent)"; + else if (m.iaq <= 50) + aqi += " (Good)"; + else if (m.iaq <= 100) + aqi += " (Moderate)"; + else if (m.iaq <= 150) + aqi += " (Poor)"; + else if (m.iaq <= 200) { + aqi += " (Unhealthy)"; + bannerMsg = "Unhealthy IAQ"; + } else if (m.iaq <= 300) { + aqi += " (Very Unhealthy)"; + bannerMsg = "Very Unhealthy IAQ"; + } else { + aqi += " (Hazardous)"; + bannerMsg = "Hazardous IAQ"; } - sensorData[sensorCount++] = - "Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%"; - } + entries.push_back(aqi); - if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) { - sensorData[sensorCount++] = - "Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA"; - } + // === IAQ alert logic === + static uint32_t lastAlertTime = 0; + uint32_t now = millis(); - if (lastMeasurement.variant.environment_metrics.voltage != 0) { - sensorData[sensorCount++] = "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " + - String(lastMeasurement.variant.environment_metrics.current, 0) + "mA"; - } + bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); + bool isCooldownOver = (now - lastAlertTime > 60000); - if (lastMeasurement.variant.environment_metrics.iaq != 0) { - sensorData[sensorCount++] = "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq); - } + if (isOwnTelemetry && bannerMsg && isCooldownOver) { + LOG_INFO("drawFrame: IAQ %d (own) โ€” showing banner: %s", m.iaq, bannerMsg); + screen->showSimpleBanner(bannerMsg, 3000); - if (lastMeasurement.variant.environment_metrics.distance != 0) { - sensorData[sensorCount++] = "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm"; - } - - if (lastMeasurement.variant.environment_metrics.weight != 0) { - sensorData[sensorCount++] = "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg"; - } - - if (lastMeasurement.variant.environment_metrics.radiation != 0) { - sensorData[sensorCount++] = "Rad: " + String(lastMeasurement.variant.environment_metrics.radiation, 2) + "ยตR/h"; - } - - if (lastMeasurement.variant.environment_metrics.lux != 0) { - sensorData[sensorCount++] = "Illuminance: " + String(lastMeasurement.variant.environment_metrics.lux, 2) + "lx"; - } - - if (lastMeasurement.variant.environment_metrics.white_lux != 0) { - sensorData[sensorCount++] = "W_Lux: " + String(lastMeasurement.variant.environment_metrics.white_lux, 2) + "lx"; - } - - static int scrollOffset = 0; - static bool scrollingDown = true; - static uint32_t lastScrollTime = millis(); - - // Determine how many lines we can fit on display - // Calculated once only: display dimensions don't change during runtime. - static int maxLines = 0; - if (!maxLines) { - const int16_t paddingTop = _fontHeight(FONT_SMALL); // Heading text - const int16_t paddingBottom = 8; // Indicator dots - maxLines = (display->getHeight() - paddingTop - paddingBottom) / _fontHeight(FONT_SMALL); - assert(maxLines > 0); - } - - // Draw as many lines of data as we can fit - int linesToShow = min(maxLines, sensorCount); - for (int i = 0; i < linesToShow; i++) { - int index = (scrollOffset + i) % sensorCount; - display->drawString(x, y += _fontHeight(FONT_SMALL), sensorData[index]); - } - - // Only scroll if there are more than 3 sensor data lines - if (sensorCount > 3) { - // Update scroll offset every 5 seconds - if (millis() - lastScrollTime > 5000) { - if (scrollingDown) { - scrollOffset++; - if (scrollOffset + linesToShow >= sensorCount) { - scrollingDown = false; - } - } else { - scrollOffset--; - if (scrollOffset <= 0) { - scrollingDown = true; - } + // Only buzz if IAQ is over 200 + if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); } - lastScrollTime = millis(); + + lastAlertTime = now; } } + if (m.voltage != 0 || m.current != 0) + entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); + if (m.lux != 0) + entries.push_back("Light: " + String(m.lux, 0) + "lx"); + if (m.white_lux != 0) + entries.push_back("White: " + String(m.white_lux, 0) + "lx"); + if (m.weight != 0) + entries.push_back("Weight: " + String(m.weight, 0) + "kg"); + if (m.distance != 0) + entries.push_back("Level: " + String(m.distance, 0) + "mm"); + if (m.radiation != 0) + entries.push_back("Rad: " + String(m.radiation, 2) + " ยตR/h"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); + } + + currentY += rowHeight; + } } bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) @@ -625,6 +678,14 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m valid = valid && rak9154Sensor.getMetrics(m); hasSensor = true; #endif +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && \ + RAK_4631 == \ + 1 // Not really needed, but may as well just skip at a lower level it if no library or not a RAK_4631 + if (rak12035Sensor.hasSensor()) { + valid = valid && rak12035Sensor.getMetrics(m); + hasSensor = true; + } +#endif #endif return valid && hasSensor; } @@ -679,6 +740,9 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_INFO("Send: radiation=%fยตR/h", m.variant.environment_metrics.radiation); + LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, + m.variant.environment_metrics.soil_moisture); + sensor_read_error_count = 0; meshtastic_MeshPacket *p = allocDataProtobuf(m); @@ -850,8 +914,17 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && \ + RAK_4631 == \ + 1 // Not really needed, but may as well just skip it at a lower level if no library or not a RAK_4631 + if (rak12035Sensor.hasSensor()) { + result = rak12035Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } +#endif #endif return result; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index a2a18ba03..3a735b1fa 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -118,22 +118,31 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * } // Display "Health From: ..." on its own - display->drawString(x, y, "Health From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + char headerStr[64]; + snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); + display->drawString(x, y, headerStr); - String last_temp = String(lastMeasurement.variant.health_metrics.temperature, 0) + "ยฐC"; + char last_temp[16]; if (moduleConfig.telemetry.environment_display_fahrenheit) { - last_temp = String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature), 0) + "ยฐF"; + snprintf(last_temp, sizeof(last_temp), "%.0fยฐF", + UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); + } else { + snprintf(last_temp, sizeof(last_temp), "%.0fยฐC", lastMeasurement.variant.health_metrics.temperature); } // Continue with the remaining details - display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp: " + last_temp); + char tempStr[32]; + snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); + display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Heart Rate: " + String(lastMeasurement.variant.health_metrics.heart_bpm, 0) + " bpm"); + char heartStr[32]; + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "spO2: " + String(lastMeasurement.variant.health_metrics.spO2, 0) + " %"); + char spo2Str[32]; + snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp index 9a9d8fecc..6a92b15f8 100644 --- a/src/modules/Telemetry/HostMetrics.cpp +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -29,13 +29,15 @@ bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { #ifdef DEBUG_PORT 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'; - LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f, %s", - sender, t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, + LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, + t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, t->variant.host_metrics.freemem_bytes, static_cast(t->variant.host_metrics.load1) / 100, static_cast(t->variant.host_metrics.load5) / 100, - static_cast(t->variant.host_metrics.load15) / 100, - t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); + static_cast(t->variant.host_metrics.load15) / 100); + // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); #endif } return false; // Let others look at this message also if they want @@ -111,7 +113,8 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics() if (settingsStrings[hostMetrics_user_command] != "") { std::string userCommandResult = exec(settingsStrings[hostMetrics_user_command].c_str()); if (userCommandResult.length() > 1) { - strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), 200); + strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); + t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; t.variant.host_metrics.has_user_string = true; } } @@ -121,12 +124,12 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics() bool HostMetricsModule::sendMetrics() { meshtastic_Telemetry telemetry = getHostMetrics(); - LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f %s", + LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes, telemetry.variant.host_metrics.freemem_bytes, static_cast(telemetry.variant.host_metrics.load1) / 100, static_cast(telemetry.variant.host_metrics.load5) / 100, - static_cast(telemetry.variant.host_metrics.load15) / 100, - telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); + static_cast(telemetry.variant.host_metrics.load15) / 100); + // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); p->to = NODENUM_BROADCAST; @@ -137,4 +140,4 @@ bool HostMetricsModule::sendMetrics() service->sendToMesh(p, RX_SRC_LOCAL, true); return true; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 54ec90dae..a92013d01 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerTelemetry.h" #include "RTC.h" #include "Router.h" +#include "graphics/SharedUIDisplay.h" #include "main.h" #include "power.h" #include "sleep.h" @@ -21,6 +22,11 @@ #include "graphics/ScreenFonts.h" #include +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); +} + int32_t PowerTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -103,13 +109,20 @@ bool PowerTelemetryModule::wantUIFrame() void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); if (lastMeasurementPacket == nullptr) { - // In case of no valid packet, display "Power Telemetry", "No measurement" - display->drawString(x, y, "Power Telemetry"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + // In case of no valid packet, display "Power Telemetry", "No measurement" + display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); return; } @@ -120,29 +133,35 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s const meshtastic_Data &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); + display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } // Display "Pow. From: ..." - display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + char fromStr[64]; + snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); + display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags - if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); + const auto &m = lastMeasurement.variant.power_metrics; + int lineY = textSecondLine; + + auto drawLine = [&](const char *label, float voltage, float current) { + char lineStr[64]; + snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); + display->drawString(x, lineY, lineStr); + lineY += _fontHeight(FONT_SMALL); + }; + + if (m.has_ch1_voltage || m.has_ch1_current) { + drawLine("Ch1", m.ch1_voltage, m.ch1_current); } - if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); + if (m.has_ch2_voltage || m.has_ch2_current) { + drawLine("Ch2", m.ch2_voltage, m.ch2_current); } - if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); + if (m.has_ch3_voltage || m.has_ch3_current) { + drawLine("Ch3", m.ch3_voltage, m.ch3_current); } } diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 0e0212bc5..fce029deb 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -137,17 +137,17 @@ void BME680Sensor::updateState() #endif } -void BME680Sensor::checkStatus(String functionName) +void BME680Sensor::checkStatus(const char *functionName) { if (bme680.status < BSEC_OK) - LOG_ERROR("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); else if (bme680.status > BSEC_OK) - LOG_WARN("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); if (bme680.sensor.status < BME68X_OK) - LOG_ERROR("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); else if (bme680.sensor.status > BME68X_OK) - LOG_WARN("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index 249c4b3e7..ce1fa4f3b 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -34,7 +34,7 @@ class BME680Sensor : public TelemetrySensor BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; void loadState(); void updateState(); - void checkStatus(String functionName); + void checkStatus(const char *functionName); public: BME680Sensor(); diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.h b/src/modules/Telemetry/Sensor/INA3221Sensor.h index 69edf8c50..0581f92f6 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.h +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.h @@ -8,16 +8,24 @@ #include "VoltageSensor.h" #include +#ifndef INA3221_ENV_CH +#define INA3221_ENV_CH INA3221_CH1 +#endif + +#ifndef INA3221_BAT_CH +#define INA3221_BAT_CH INA3221_CH1 +#endif + class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { private: INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); // channel to report voltage/current for environment metrics - ina3221_ch_t ENV_CH = INA3221_CH1; + static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; // channel to report battery voltage for device_battery_ina_address - ina3221_ch_t BAT_CH = INA3221_CH1; + static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; // get a single measurement for a channel struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp index 6ab96aa57..1a6792d3a 100644 --- a/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp @@ -1,6 +1,6 @@ #include "MAX17048Sensor.h" -#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_STM32WL) && __has_include() +#if !MESHTASTIC_EXCLUDE_I2C && __has_include() MAX17048Singleton *MAX17048Singleton::GetInstance() { diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.h b/src/modules/Telemetry/Sensor/MAX17048Sensor.h index 6f61421dc..d27169406 100644 --- a/src/modules/Telemetry/Sensor/MAX17048Sensor.h +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.h @@ -5,7 +5,7 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_STM32WL) && __has_include() +#if !MESHTASTIC_EXCLUDE_I2C && __has_include() // Samples to store in a buffer to determine if the battery is charging or discharging #define MAX17048_CHARGING_SAMPLES 3 diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp new file mode 100644 index 000000000..7a1bb01ce --- /dev/null +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp @@ -0,0 +1,109 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RAK12035Sensor.h" + +RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {} + +int32_t RAK12035Sensor::runOnce() +{ + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + // TODO:: check for up to 2 additional sensors and start them if present. + sensor.set_sensor_addr(RAK120351_ADDR); + delay(100); + sensor.begin(nodeTelemetrySensorsMap[sensorType].first); + + // Get sensor firmware version + uint8_t data = 0; + sensor.get_sensor_version(&data); + if (data != 0) { + LOG_INFO("Init sensor: %s", sensorName); + LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); + status = true; + sensor.sensor_sleep(); + } else { + // If we reach here, it means the sensor did not initialize correctly. + LOG_INFO("Init sensor: %s", sensorName); + LOG_ERROR("RAK12035Sensor Init Failed"); + status = false; + } + + return initI2CSensor(); +} + +void RAK12035Sensor::setup() +{ + // Set the calibration values + // Reading the saved calibration values from the sensor. + // TODO:: Check for and run calibration check for up to 2 additional sensors if present. + uint16_t zero_val = 0; + uint16_t hundred_val = 0; + uint16_t default_zero_val = 550; + uint16_t default_hundred_val = 420; + sensor.sensor_on(); + delay(200); + sensor.get_dry_cal(&zero_val); + sensor.get_wet_cal(&hundred_val); + delay(200); + if (zero_val == 0 || zero_val <= hundred_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); + sensor.set_dry_cal(default_zero_val); + sensor.get_dry_cal(&zero_val); + LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); + } + if (hundred_val == 0 || hundred_val >= zero_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); + sensor.set_wet_cal(default_hundred_val); + sensor.get_wet_cal(&hundred_val); + LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); + } + sensor.sensor_sleep(); + delay(200); + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); +} + +bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + // TODO:: read and send metrics for up to 2 additional soil monitors if present. + // -- how to do this.. this could get a little complex.. + // ie - 1> we combine them into an average and send that, 2> we send them as separate metrics + // ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting in the + // device ui and an additional proto for that? + measurement->variant.environment_metrics.has_soil_temperature = true; + measurement->variant.environment_metrics.has_soil_moisture = true; + + uint8_t moisture = 0; + uint16_t temp = 0; + bool success = false; + + sensor.sensor_on(); + delay(200); + success = sensor.get_sensor_moisture(&moisture); + delay(200); + success &= sensor.get_sensor_temperature(&temp); + delay(200); + sensor.sensor_sleep(); + + if (success == false) { + LOG_ERROR("Failed to read sensor data"); + return false; + } + measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); + measurement->variant.environment_metrics.soil_moisture = moisture; + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.h b/src/modules/Telemetry/Sensor/RAK12035Sensor.h new file mode 100644 index 000000000..2c32a840d --- /dev/null +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() && defined(RAK_4631) +#ifndef _MT_RAK12035VBSENSOR_H +#define _MT_RAK12035VBSENSOR_H +#endif + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RAK12035_SoilMoisture.h" +#include "TelemetrySensor.h" +#include + +class RAK12035Sensor : public TelemetrySensor +{ + private: + RAK12035 sensor; + + protected: + virtual void setup() override; + + public: + RAK12035Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; +#endif diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp index e352dda8d..9f7a55cc5 100644 --- a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp @@ -41,21 +41,36 @@ void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl float RCWL9620Sensor::getDistance() { - uint32_t data; - _wire->beginTransmission(_addr); // Transfer data to addr. - _wire->write(0x01); - _wire->endTransmission(); // Stop data transmission with the Ultrasonic - // Unit. + uint32_t data = 0; + uint8_t b1 = 0, b2 = 0, b3 = 0; - _wire->requestFrom(_addr, - (uint8_t)3); // Request 3 bytes from Ultrasonic Unit. + LOG_DEBUG("[RCWL9620] Start measure command"); + + _wire->beginTransmission(_addr); + _wire->write(0x01); // ร€ tester aussi sans cette ligne si besoin + uint8_t result = _wire->endTransmission(); + LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); + delay(100); // dรฉlai pour laisser le capteur rรฉpondre + + LOG_DEBUG("[RCWL9620] Read i2c data:"); + _wire->requestFrom(_addr, (uint8_t)3); + + if (_wire->available() < 3) { + LOG_DEBUG("[RCWL9620] less than 3 octets !"); + return 0.0; + } + + b1 = _wire->read(); + b2 = _wire->read(); + b3 = _wire->read(); + + data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; + + float Distance = float(data) / 1000.0; + + LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); + LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); - data = _wire->read(); - data <<= 8; - data |= _wire->read(); - data <<= 8; - data |= _wire->read(); - float Distance = float(data) / 1000; if (Distance > 4500.00) { return 4500.00; } else { @@ -63,4 +78,4 @@ float RCWL9620Sensor::getDistance() } } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/Sensor/nullSensor.cpp b/src/modules/Telemetry/Sensor/nullSensor.cpp index 9522c7fcc..c84b9d27f 100644 --- a/src/modules/Telemetry/Sensor/nullSensor.cpp +++ b/src/modules/Telemetry/Sensor/nullSensor.cpp @@ -20,4 +20,15 @@ bool NullSensor::getMetrics(meshtastic_Telemetry *measurement) { return false; } + +uint16_t NullSensor::getBusVoltageMv() +{ + return 0; +} + +int16_t NullSensor::getCurrentMa() +{ + return 0; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/nullSensor.h b/src/modules/Telemetry/Sensor/nullSensor.h index 94dbcc7f8..a400acf97 100644 --- a/src/modules/Telemetry/Sensor/nullSensor.h +++ b/src/modules/Telemetry/Sensor/nullSensor.h @@ -4,9 +4,12 @@ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -class NullSensor : public TelemetrySensor +#include "CurrentSensor.h" +#include "TelemetrySensor.h" +#include "VoltageSensor.h" + +class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor { protected: @@ -17,6 +20,9 @@ class NullSensor : public TelemetrySensor virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; int32_t runTrigger() { return 0; } + + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index f1d01ad16..970f4429c 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -4,6 +4,7 @@ #include "PowerFSM.h" #include "buzz.h" #include "configuration.h" +#include "graphics/Screen.h" TextMessageModule *textMessageModule; ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) @@ -17,7 +18,10 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp devicestate.rx_text_message = mp; devicestate.has_rx_text_message = true; - powerFSM.trigger(EVENT_RECEIVED_MSG); + // Only trigger screen wake if configuration allows it + if (shouldWakeOnReceivedMessage()) { + powerFSM.trigger(EVENT_RECEIVED_MSG); + } notifyObservers(&mp); return ProcessMessage::CONTINUE; // Let others look at this message also if they want diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 479a973c2..aab3ed6bc 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -2,9 +2,13 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" +#include "graphics/draw/CompassRenderer.h" + #if HAS_SCREEN #include "gps/RTC.h" #include "graphics/Screen.h" +#include "graphics/TimeFormatters.h" +#include "graphics/draw/NodeListRenderer.h" #include "main.h" #endif @@ -48,6 +52,8 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) bool WaypointModule::shouldDraw() { #if !MESHTASTIC_EXCLUDE_WAYPOINT + if (screen == nullptr) + return false; // If no waypoint to show if (!devicestate.has_rx_waypoint) return false; @@ -79,13 +85,15 @@ bool WaypointModule::shouldDraw() /// Draw the last waypoint we received void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (screen == nullptr) + return; // Prepare to draw display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // Handle inverted display // Unsure of expected behavior: for now, copy drawNodeInfo - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); // Decode the waypoint @@ -101,7 +109,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Get timestamp info. Will pass as a field to drawColumns static char lastStr[20]; - screen->getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); + getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns static char distStr[20]; @@ -115,7 +123,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Dimensions / co-ordinates for the compass/circle int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { compassX = x + display->getWidth() - compassDiam / 2 - 5; @@ -129,20 +137,24 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { const meshtastic_PositionLite &op = ourNode->position; float myHeading; - if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = 0; + } else { + if (screen->hasHeading()) + myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); // Compass bearing to waypoint float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (!config.display.compass_north_top) + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) bearingToOther -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; @@ -180,11 +192,11 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Undo color-inversion, if set prior to drawing header // Unsure of expected behavior? For now: copy drawNodeInfo - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } // Must be after distStr is populated - screen->drawColumns(display, x, y, fields); + graphics::NodeListRenderer::drawColumns(display, x, y, fields); } #endif diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp index b27586771..8b1fc5302 100644 --- a/src/modules/esp32/PaxcounterModule.cpp +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -3,6 +3,9 @@ #include "Default.h" #include "MeshService.h" #include "PaxcounterModule.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include PaxcounterModule *paxcounterModule; @@ -112,20 +115,32 @@ int32_t PaxcounterModule::runOnce() #if HAS_SCREEN #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = "Pax"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + 0, y + 0, "PAX"); libpax_counter_count(&count_from_libpax); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, "WiFi: %d\nBLE: %d\nuptime: %ds", - count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); + display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, + "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, + millis() / 1000); } #endif // HAS_SCREEN diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 02e5b0bd4..f08ee00f9 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -10,6 +10,7 @@ #ifdef HAS_BMA423 #include "BMA423Sensor.h" #endif +#include "BMM150Sensor.h" #include "BMX160Sensor.h" #include "ICM20948Sensor.h" #include "LIS3DHSensor.h" @@ -107,6 +108,9 @@ class AccelerometerThread : public concurrency::OSThread case ScanI2C::DeviceType::ICM20948: sensor = new ICM20948Sensor(device); break; + case ScanI2C::DeviceType::BMM150: + sensor = new BMM150Sensor(device); + break; #ifdef HAS_QMA6100P case ScanI2C::DeviceType::QMA6100P: sensor = new QMA6100PSensor(device); diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp new file mode 100644 index 000000000..4b3a1215c --- /dev/null +++ b/src/motion/BMM150Sensor.cpp @@ -0,0 +1,93 @@ +#include "BMM150Sensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + +// screen is defined in main.cpp +extern graphics::Screen *screen; +#endif + +// Flag when an interrupt has been detected +volatile static bool BMM150_IRQ = false; + +BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +bool BMM150Sensor::init() +{ + // Initialise the sensor + sensor = BMM150Singleton::GetInstance(device); + return sensor->init(device); +} + +int32_t BMM150Sensor::runOnce() +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + float heading = sensor->getCompassDegree(); + + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); +#endif + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +// ---------------------------------------------------------------------- +// BMM150Singleton +// ---------------------------------------------------------------------- + +// Get a singleton wrapper for an Sparkfun BMM_150_I2C +BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) +{ +#if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); +#else + TwoWire &bus = Wire; // fallback if only one I2C interface +#endif + if (pinstance == nullptr) { + pinstance = new BMM150Singleton(&bus, device.address.address); + } + return pinstance; +} + +BMM150Singleton::~BMM150Singleton() {} + +BMM150Singleton *BMM150Singleton::pinstance{nullptr}; + +// Initialise the BMM150 Sensor +// https://github.com/DFRobot/DFRobot_BMM150/blob/master/examples/getGeomagneticData/getGeomagneticData.ino +bool BMM150Singleton::init(ScanI2C::FoundDevice device) +{ + + // startup + LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); + uint8_t status = begin(); + if (status != 0) { + LOG_DEBUG("BMM150 init error %u", status); + return false; + } + + // SW reset to make sure the device starts in a known state + setOperationMode(BMM150_POWERMODE_NORMAL); + setPresetMode(BMM150_PRESETMODE_LOWPOWER); + setRate(BMM150_DATA_RATE_02HZ); + setMeasurementXYZ(); + return true; +} + +#endif \ No newline at end of file diff --git a/src/motion/BMM150Sensor.h b/src/motion/BMM150Sensor.h new file mode 100644 index 000000000..879045400 --- /dev/null +++ b/src/motion/BMM150Sensor.h @@ -0,0 +1,57 @@ +#pragma once +#ifndef _BMM_150_SENSOR_H_ +#define _BMM_150_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include "Fusion/Fusion.h" +#include + +// The I2C address of the Accelerometer (if found) from main.cpp +extern ScanI2C::DeviceAddress accelerometer_found; + +// Singleton wrapper +class BMM150Singleton : public DFRobot_BMM150_I2C +{ + private: + static BMM150Singleton *pinstance; + + protected: + BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} + ~BMM150Singleton(); + + public: + // Create a singleton instance (not thread safe) + static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); + + // Singletons should not be cloneable. + BMM150Singleton(BMM150Singleton &other) = delete; + + // Singletons should not be assignable. + void operator=(const BMM150Singleton &) = delete; + + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); +}; + +class BMM150Sensor : public MotionSensor +{ + private: + BMM150Singleton *sensor = nullptr; + bool showingScreen = false; + + public: + explicit BMM150Sensor(ScanI2C::FoundDevice foundDevice); + + // Initialise the motion sensor + virtual bool init() override; + + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index a3909ea3a..003ee850c 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -37,7 +37,8 @@ int32_t BMX160Sensor::runOnce() if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; - screen->startAlert((FrameCallback)drawFrameCalibration); + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } if (magAccel.x > highestX) @@ -58,7 +59,8 @@ int32_t BMX160Sensor::runOnce() doCalibration = false; endCalibrationAt = 0; showingScreen = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, @@ -103,8 +105,8 @@ int32_t BMX160Sensor::runOnce() heading += 270; break; } - - screen->setHeading(heading); + if (screen) + screen->setHeading(heading); #endif return MOTION_SENSOR_CHECK_INTERVAL_MS; @@ -118,7 +120,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds) doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; - screen->setEndCalibration(endCalibrationAt); + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index ecc48d39b..76ba8e8cf 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -60,7 +60,8 @@ int32_t ICM20948Sensor::runOnce() if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; - screen->startAlert((FrameCallback)drawFrameCalibration); + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } if (magX > highestX) @@ -81,7 +82,8 @@ int32_t ICM20948Sensor::runOnce() doCalibration = false; endCalibrationAt = 0; showingScreen = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, @@ -124,8 +126,8 @@ int32_t ICM20948Sensor::runOnce() heading += 270; break; } - - screen->setHeading(heading); + if (screen) + screen->setHeading(heading); #endif // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) @@ -159,7 +161,8 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds) doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; - screen->setEndCalibration(endCalibrationAt); + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } // ---------------------------------------------------------------------- diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 56738d355..b00460aff 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -1,4 +1,5 @@ #include "MotionSensor.h" +#include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C @@ -34,6 +35,8 @@ ScanI2C::I2CPort MotionSensor::devicePort() #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (screen == nullptr) + return; // int x_offset = display->width() / 2; // int y_offset = display->height() <= 80 ? 0 : 32; display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -46,7 +49,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState display->drawString(x, y + 40, timeRemainingBuffer); int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { @@ -57,7 +60,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; } display->drawCircle(compassX, compassY, compassDiam / 2); - screen->drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } #endif diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index dca8a3b44..137c92056 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -355,7 +355,7 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length) // if another "/" was added, parse string up to that character channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; // We allow downlink JSON packets only on a channel named "mqtt" - meshtastic_Channel &sendChannel = channels.getByName(channelName); + const meshtastic_Channel &sendChannel = channels.getByName(channelName); if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && sendChannel.settings.downlink_enabled)) { LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); @@ -491,7 +491,7 @@ void MQTT::reconnect() return; // Don't try to connect directly to the server } #if HAS_NETWORKING - const PubSubConfig config(moduleConfig.mqtt); + const PubSubConfig ps_config(moduleConfig.mqtt); MQTTClient *clientConnection = mqttClient.get(); #if MQTT_SUPPORTS_TLS if (moduleConfig.mqtt.tls_enabled) { @@ -502,7 +502,7 @@ void MQTT::reconnect() LOG_INFO("Use non-TLS-encrypted session"); } #endif - if (connectPubSub(config, pubSub, *clientConnection)) { + if (connectPubSub(ps_config, pubSub, *clientConnection)) { enabled = true; // Start running background process again runASAP = true; reconnectCount = 0; @@ -763,7 +763,10 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me } entry->topic = std::move(topic); entry->envBytes.assign(bytes, numBytes); - assert(mqttQueue.enqueue(entry, 0)); + if (mqttQueue.enqueue(entry, 0) == false) { + LOG_CRIT("Failed to add a message to mqttQueue!"); + abort(); + } } } diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 009439f25..834184292 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -9,6 +9,7 @@ #include "mesh/mesh-pb-constants.h" #include "sleep.h" #include +#include NimBLECharacteristic *fromNumCharacteristic; NimBLECharacteristic *BatteryCharacteristic; @@ -17,8 +18,37 @@ NimBLEServer *bleServer; static bool passkeyShowing; -class BluetoothPhoneAPI : public PhoneAPI +class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { + public: + BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { nimble_queue.resize(3); } + std::vector nimble_queue; + std::mutex nimble_mutex; + uint8_t queue_size = 0; + bool has_fromRadio = false; + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; + size_t numBytes = 0; + bool hasChecked = false; + bool phoneWants = false; + + protected: + virtual int32_t runOnce() override + { + std::lock_guard guard(nimble_mutex); + if (queue_size > 0) { + for (uint8_t i = 0; i < queue_size; i++) { + handleToRadio(nimble_queue.at(i).data(), nimble_queue.at(i).length()); + } + LOG_DEBUG("Queue_size %u", queue_size); + queue_size = 0; + } + if (hasChecked == false && phoneWants == true) { + numBytes = getFromRadio(fromRadioBytes); + hasChecked = true; + } + + return 100; + } /** * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) */ @@ -51,15 +81,16 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { virtual void onWrite(NimBLECharacteristic *pCharacteristic) { - LOG_DEBUG("To Radio onwrite"); auto val = pCharacteristic->getValue(); if (memcmp(lastToRadio, val.data(), val.length()) != 0) { - LOG_DEBUG("New ToRadio packet"); - memcpy(lastToRadio, val.data(), val.length()); - bluetoothPhoneAPI->handleToRadio(val.data(), val.length()); - } else { - LOG_DEBUG("Drop dup ToRadio packet we just saw"); + if (bluetoothPhoneAPI->queue_size < 3) { + memcpy(lastToRadio, val.data(), val.length()); + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); + bluetoothPhoneAPI->nimble_queue.at(bluetoothPhoneAPI->queue_size) = val; + bluetoothPhoneAPI->queue_size++; + bluetoothPhoneAPI->setIntervalFromNow(0); + } } } }; @@ -68,12 +99,23 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { virtual void onRead(NimBLECharacteristic *pCharacteristic) { - uint8_t fromRadioBytes[meshtastic_FromRadio_size]; - size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); - - std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes); - + int tries = 0; + bluetoothPhoneAPI->phoneWants = true; + while (!bluetoothPhoneAPI->hasChecked && tries < 100) { + bluetoothPhoneAPI->setIntervalFromNow(0); + delay(20); + tries++; + } + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); + std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes, + bluetoothPhoneAPI->fromRadioBytes + bluetoothPhoneAPI->numBytes); pCharacteristic->setValue(fromRadioByteString); + + if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload + bluetoothPhoneAPI->setIntervalFromNow(0); + bluetoothPhoneAPI->numBytes = 0; + bluetoothPhoneAPI->hasChecked = false; + bluetoothPhoneAPI->phoneWants = false; } }; @@ -94,31 +136,33 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", passkey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", passkey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + char pin[8]; + snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); + display->setFont(FONT_SMALL); + char deviceName[64]; + snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif passkeyShowing = true; @@ -134,7 +178,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { passkeyShowing = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } } @@ -146,7 +191,12 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); if (bluetoothPhoneAPI) { + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); bluetoothPhoneAPI->close(); + bluetoothPhoneAPI->hasChecked = false; + bluetoothPhoneAPI->phoneWants = false; + bluetoothPhoneAPI->numBytes = 0; + bluetoothPhoneAPI->queue_size = 0; } } }; @@ -171,6 +221,11 @@ void NimbleBluetooth::deinit() { #ifdef ARCH_ESP32 LOG_INFO("Disable bluetooth until reboot"); + +#ifdef BLE_LED + digitalWrite(BLE_LED, LOW); +#endif + NimBLEDevice::deinit(); #endif } diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/WiFiOTA.cpp index eac124dda..4cf157b4c 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/WiFiOTA.cpp @@ -80,13 +80,13 @@ bool trySwitchToOTA() return true; } -String getVersion() +const char *getVersion() { const esp_partition_t *part = getAppPartition(); - esp_app_desc_t app_desc; + static esp_app_desc_t app_desc; if (!getAppDesc(part, &app_desc)) - return String(); - return String(app_desc.version); + return ""; + return app_desc.version; } } // namespace WiFiOTA diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h index 61860ed5e..5a7ee348a 100644 --- a/src/platform/esp32/WiFiOTA.h +++ b/src/platform/esp32/WiFiOTA.h @@ -12,7 +12,7 @@ bool isUpdated(); void recoverConfig(meshtastic_Config_NetworkConfig *network); void saveConfig(meshtastic_Config_NetworkConfig *network); bool trySwitchToOTA(); -String getVersion(); +const char *getVersion(); } // namespace WiFiOTA #endif // WIFIOTA_H diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 3763bce1e..baefbc4eb 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -184,6 +184,8 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_SENSOR_HUB #elif defined(ELECROW_PANEL) #define HW_VENDOR meshtastic_HardwareModel_CROWPANEL +#elif defined(RAK3312) +#define HW_VENDOR meshtastic_HardwareModel_RAK3312 #elif defined(LINK_32) #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #endif diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 3c4faac3e..cdea53c9a 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -56,9 +56,11 @@ void updateBatteryLevel(uint8_t level) {} void getMacAddr(uint8_t *dmac) { #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_SOC_IEEE802154_SUPPORTED) - assert(esp_base_mac_addr_get(dmac) == ESP_OK); + auto res = esp_base_mac_addr_get(dmac); + assert(res == ESP_OK); #else - assert(esp_efuse_mac_get_default(dmac) == ESP_OK); + auto res = esp_efuse_mac_get_default(dmac); + assert(res == ESP_OK); #endif } diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 4f6fe7c6b..6f0e7250f 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -314,7 +314,9 @@ void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) } bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { - LOG_INFO("BLE pair process started with passkey %.3s %.3s", passkey, passkey + 3); + char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; + char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; + LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); // Get passkey as string @@ -327,31 +329,33 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif if (match_request) { uint32_t start_time = millis(); @@ -394,8 +398,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) @@ -404,7 +407,9 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->endAlert(); + if (screen) { + screen->endAlert(); + } } void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index eea3aee45..684d20e84 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -49,6 +49,8 @@ #define HW_VENDOR meshtastic_HardwareModel_RAK2560 #elif defined(WISMESH_TAP) #define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP +#elif defined(GAT562_MESH_TRIAL_TRACKER) +#define HW_VENDOR meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER #elif defined(RAK4630) #define HW_VENDOR meshtastic_HardwareModel_RAK4631 #elif defined(TTGO_T_ECHO) @@ -85,6 +87,8 @@ #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) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 #else @@ -117,10 +121,6 @@ #define BUTTON_PIN PIN_BUTTON1 #endif -#ifdef PIN_BUTTON2 -#define BUTTON_PIN_ALT PIN_BUTTON2 -#endif - #ifdef PIN_BUTTON_TOUCH #define BUTTON_PIN_TOUCH PIN_BUTTON_TOUCH #endif @@ -139,4 +139,4 @@ #if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) // No serial ports on this board - ONLY use segger in memory console #define USE_SEGGER -#endif \ No newline at end of file +#endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 9accd2a02..1bf9a39fd 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -308,6 +308,9 @@ void cpuDeepSleep(uint32_t msecToWake) nrf_gpio_cfg_default(SCREEN_TOUCH_INT); nrf_gpio_cfg_default(WB_I2C1_SCL); nrf_gpio_cfg_default(WB_I2C1_SDA); + + // nrf_gpio_cfg_default(WB_I2C2_SCL); + // nrf_gpio_cfg_default(WB_I2C2_SDA); #endif #endif #ifdef MESHLINK diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index cc0c417d3..685f0d077 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -35,6 +35,8 @@ char *configPath = nullptr; char *optionMac = nullptr; bool forceSimulated = false; +const char *argp_program_version = optstr(APP_VERSION); + // FIXME - move setBluetoothEnable into a HALPlatform class void setBluetoothEnable(bool enable) { @@ -143,10 +145,26 @@ void portduinoSetup() { printf("Set up Meshtastic on Portduino...\n"); int max_GPIO = 0; - const configNames GPIO_lines[] = { - cs_pin, irq_pin, busy_pin, reset_pin, sx126x_ant_sw_pin, txen_pin, - rxen_pin, displayDC, displayCS, displayBacklight, displayBacklightPWMChannel, displayReset, - touchscreenCS, touchscreenIRQ, user}; + const configNames GPIO_lines[] = {cs_pin, + irq_pin, + busy_pin, + reset_pin, + sx126x_ant_sw_pin, + txen_pin, + rxen_pin, + displayDC, + displayCS, + displayBacklight, + displayBacklightPWMChannel, + displayReset, + touchscreenCS, + touchscreenIRQ, + userButtonPin, + tbUpPin, + tbDownPin, + tbLeftPin, + tbRightPin, + tbPressPin}; std::string gpioChipName = "gpiochip"; settingsStrings[i2cdev] = ""; @@ -159,6 +177,11 @@ void portduinoSetup() settingsMap[ascii_logs] = !isatty(1); settingsMap[displayPanel] = no_screen; settingsMap[touchscreenModule] = no_touchscreen; + settingsMap[tbUpPin] = RADIOLIB_NC; + settingsMap[tbDownPin] = RADIOLIB_NC; + settingsMap[tbLeftPin] = RADIOLIB_NC; + settingsMap[tbRightPin] = RADIOLIB_NC; + settingsMap[tbPressPin] = RADIOLIB_NC; YAML::Node yamlConfig; @@ -313,9 +336,34 @@ void portduinoSetup() // Need to bind all the configured GPIO pins so they're not simulated // TODO: If one of these fails, we should log and terminate - if (settingsMap.count(user) > 0 && settingsMap[user] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[user], defaultGpioChipName, settingsMap[user]) != ERRNO_OK) { - settingsMap[user] = RADIOLIB_NC; + if (settingsMap.count(userButtonPin) > 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[userButtonPin], defaultGpioChipName, settingsMap[userButtonPin]) != ERRNO_OK) { + settingsMap[userButtonPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbUpPin) > 0 && settingsMap[tbUpPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbUpPin], defaultGpioChipName, settingsMap[tbUpPin]) != ERRNO_OK) { + settingsMap[tbUpPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbDownPin) > 0 && settingsMap[tbDownPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbDownPin], defaultGpioChipName, settingsMap[tbDownPin]) != ERRNO_OK) { + settingsMap[tbDownPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbLeftPin) > 0 && settingsMap[tbLeftPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbLeftPin], defaultGpioChipName, settingsMap[tbLeftPin]) != ERRNO_OK) { + settingsMap[tbLeftPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbRightPin) > 0 && settingsMap[tbRightPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbRightPin], defaultGpioChipName, settingsMap[tbRightPin]) != ERRNO_OK) { + settingsMap[tbRightPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbPressPin) > 0 && settingsMap[tbPressPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbPressPin], defaultGpioChipName, settingsMap[tbPressPin]) != ERRNO_OK) { + settingsMap[tbPressPin] = RADIOLIB_NC; } } if (settingsMap[displayPanel] != no_screen) { @@ -377,6 +425,8 @@ int initGPIOPin(int pinNum, const std::string gpioChipName, int line) { #ifdef PORTDUINO_LINUX_HARDWARE std::string gpio_name = "GPIO" + std::to_string(pinNum); + std::cout << gpio_name; + printf("\n"); try { GPIOPin *csPin; csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); @@ -498,7 +548,7 @@ bool loadConfig(const char *configPath) } } if (yamlConfig["GPIO"]) { - settingsMap[user] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC); + settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC); } if (yamlConfig["GPS"]) { std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); @@ -588,6 +638,17 @@ bool loadConfig(const char *configPath) if (yamlConfig["Input"]) { settingsStrings[keyboardDevice] = (yamlConfig["Input"]["KeyboardDevice"]).as(""); settingsStrings[pointerDevice] = (yamlConfig["Input"]["PointerDevice"]).as(""); + settingsMap[userButtonPin] = yamlConfig["Input"]["User"].as(RADIOLIB_NC); + settingsMap[tbUpPin] = yamlConfig["Input"]["TrackballUp"].as(RADIOLIB_NC); + settingsMap[tbDownPin] = yamlConfig["Input"]["TrackballDown"].as(RADIOLIB_NC); + settingsMap[tbLeftPin] = yamlConfig["Input"]["TrackballLeft"].as(RADIOLIB_NC); + settingsMap[tbRightPin] = yamlConfig["Input"]["TrackballRight"].as(RADIOLIB_NC); + settingsMap[tbPressPin] = yamlConfig["Input"]["TrackballPress"].as(RADIOLIB_NC); + if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { + settingsMap[tbDirection] = 4; + } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { + settingsMap[tbDirection] = 3; + } } if (yamlConfig["Webserver"]) { @@ -606,6 +667,21 @@ bool loadConfig(const char *configPath) settingsStrings[hostMetrics_user_command] = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); } + if (yamlConfig["Config"]) { + if (yamlConfig["Config"]["DisplayMode"]) { + settingsMap[has_configDisplayMode] = true; + if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { + settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; + } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { + settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { + settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + } else { + settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + } + } + } + if (yamlConfig["General"]) { settingsMap[maxnodes] = (yamlConfig["General"]["MaxNodes"]).as(200); settingsMap[maxtophone] = (yamlConfig["General"]["MaxMessageQueue"]).as(100); diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index d324aaf47..288870eef 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -11,7 +11,7 @@ inline const std::unordered_map configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, {"MESHSTICK", "lora-meshstick-1262.yaml"}, {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, - {"MESHADV-MINI", "lora-MeshAdv-Mini-900M22S.yaml"}, + {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, {"POWERPI", "lora-MeshAdv-900M30S.yaml"}}; enum configNames { @@ -57,7 +57,13 @@ enum configNames { lora_usb_serial_num, lora_usb_pid, lora_usb_vid, - user, + userButtonPin, + tbUpPin, + tbDownPin, + tbLeftPin, + tbRightPin, + tbPressPin, + tbDirection, spidev, spiSpeed, i2cdev, @@ -103,7 +109,9 @@ enum configNames { mac_address, hostMetrics_interval, hostMetrics_channel, - hostMetrics_user_command + hostMetrics_user_command, + configDisplayMode, + has_configDisplayMode }; enum { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; enum { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h index a5e263d5a..07d0aeee0 100644 --- a/src/platform/portduino/architecture.h +++ b/src/platform/portduino/architecture.h @@ -8,6 +8,9 @@ #define HW_VENDOR meshtastic_HardwareModel_PORTDUINO +#ifndef HAS_BUTTON +#define HAS_BUTTON 1 +#endif #ifndef HAS_WIFI #define HAS_WIFI 1 #endif @@ -22,4 +25,12 @@ #endif #ifndef HAS_SENSOR #define HAS_SENSOR 1 +#endif +#ifndef HAS_TRACKBALL +#define HAS_TRACKBALL 1 +#define TB_DOWN (uint8_t) settingsMap[tbDownPin] +#define TB_UP (uint8_t) settingsMap[tbUpPin] +#define TB_LEFT (uint8_t) settingsMap[tbLeftPin] +#define TB_RIGHT (uint8_t) settingsMap[tbRightPin] +#define TB_PRESS (uint8_t) settingsMap[tbPressPin] #endif \ No newline at end of file diff --git a/src/platform/stm32wl/architecture.h b/src/platform/stm32wl/architecture.h index 325a192a4..ac2bbe5d1 100644 --- a/src/platform/stm32wl/architecture.h +++ b/src/platform/stm32wl/architecture.h @@ -12,6 +12,9 @@ #ifndef HAS_TELEMETRY #define HAS_TELEMETRY 1 #endif +#ifndef HAS_WIRE +#define HAS_WIRE 1 +#endif // // set HW_VENDOR @@ -28,4 +31,4 @@ #define SX126X_CS 1000 #define SX126X_DIO1 1001 #define SX126X_RESET 1003 -#define SX126X_BUSY 1004 \ No newline at end of file +#define SX126X_BUSY 1004 diff --git a/src/power.h b/src/power.h index d7fa7f8a9..c71f96c10 100644 --- a/src/power.h +++ b/src/power.h @@ -25,11 +25,15 @@ #elif defined(CELL_TYPE_LTO) #define OCV_ARRAY 2700, 2560, 2540, 2520, 2500, 2460, 2420, 2400, 2380, 2320, 1500 #elif defined(TRACKER_T1000_E) -#define OCV_ARRAY 4190, 4078, 4017, 3969, 3887, 3818, 3798, 3791, 3766, 3712, 3100 +#define OCV_ARRAY 4190, 4042, 3957, 3885, 3820, 3776, 3746, 3725, 3696, 3644, 3100 #elif defined(HELTEC_MESH_POCKET_BATTERY_5000) #define OCV_ARRAY 4300, 4240, 4120, 4000, 3888, 3800, 3740, 3698, 3655, 3580, 3400 #elif defined(HELTEC_MESH_POCKET_BATTERY_10000) #define OCV_ARRAY 4100, 4060, 3960, 3840, 3729, 3625, 3550, 3500, 3420, 3345, 3100 +#elif defined(SEEED_WIO_TRACKER_L1) +#define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300 +#elif defined(SEEED_SOLAR_NODE) +#define OCV_ARRAY 4200, 3986, 3922, 3812, 3734, 3645, 3527, 3420, 3281, 3087, 2786 #else // LiIon #define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #endif @@ -77,9 +81,9 @@ extern NullSensor ina3221Sensor; #endif -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL) -#include "modules/Telemetry/Sensor/MAX17048Sensor.h" +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #if __has_include() +#include "modules/Telemetry/Sensor/MAX17048Sensor.h" extern MAX17048Sensor max17048Sensor; #else extern NullSensor max17048Sensor; diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 2c1dc0ca7..29a9b6840 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -61,40 +61,101 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - msgPayload["battery_level"] = new JSONValue((unsigned int)decoded->variant.device_metrics.battery_level); + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); + } msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); - msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); - msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); - msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); - msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); - msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); - msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); - msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); - msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); - msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); - msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); - msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); - msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); - msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); + // Avoid sending 0s for sensors that could be 0 + if (decoded->variant.environment_metrics.has_temperature) { + msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + msgPayload["barometric_pressure"] = + new JSONValue(decoded->variant.environment_metrics.barometric_pressure); + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); + } + if (decoded->variant.environment_metrics.has_voltage) { + msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); + } + if (decoded->variant.environment_metrics.has_current) { + msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); + } + if (decoded->variant.environment_metrics.has_lux) { + msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); + } + if (decoded->variant.environment_metrics.has_white_lux) { + msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); + } + if (decoded->variant.environment_metrics.has_iaq) { + msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); + } + if (decoded->variant.environment_metrics.has_wind_speed) { + msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); + } + if (decoded->variant.environment_metrics.has_wind_direction) { + msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); + } + if (decoded->variant.environment_metrics.has_wind_gust) { + msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); + } + if (decoded->variant.environment_metrics.has_wind_lull) { + msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); + } + if (decoded->variant.environment_metrics.has_radiation) { + msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); + } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); - msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); - msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); - msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); - msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); - msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + msgPayload["pm10_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + msgPayload["pm25_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + msgPayload["pm100_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); - msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); - msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); - msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); - msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); - msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); + if (decoded->variant.power_metrics.has_ch1_voltage) { + msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); + } + if (decoded->variant.power_metrics.has_ch1_current) { + msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); + } + if (decoded->variant.power_metrics.has_ch2_current) { + msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); + } + if (decoded->variant.power_metrics.has_ch3_current) { + msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); + } } jsonObj["payload"] = new JSONValue(msgPayload); } else if (shouldLog) { diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 89ecddfad..e0daa1a88 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -58,40 +58,96 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - jsonObj["payload"]["battery_level"] = (unsigned int)decoded->variant.device_metrics.battery_level; + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; + } jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; - jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; - jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; - jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; - jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; - jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; - jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; - jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; - jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; - jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; - jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; - jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; - jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; - jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; + if (decoded->variant.environment_metrics.has_temperature) { + jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; + } + if (decoded->variant.environment_metrics.has_voltage) { + jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; + } + if (decoded->variant.environment_metrics.has_current) { + jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; + } + if (decoded->variant.environment_metrics.has_lux) { + jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; + } + if (decoded->variant.environment_metrics.has_white_lux) { + jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; + } + if (decoded->variant.environment_metrics.has_iaq) { + jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; + } + if (decoded->variant.environment_metrics.has_wind_speed) { + jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; + } + if (decoded->variant.environment_metrics.has_wind_direction) { + jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; + } + if (decoded->variant.environment_metrics.has_wind_gust) { + jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; + } + if (decoded->variant.environment_metrics.has_wind_lull) { + jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; + } + if (decoded->variant.environment_metrics.has_radiation) { + jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; + } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; - jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; - jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; - jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; - jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; - jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; - jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; - jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + if (decoded->variant.power_metrics.has_ch1_voltage) { + jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; + } + if (decoded->variant.power_metrics.has_ch1_current) { + jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; + } + if (decoded->variant.power_metrics.has_ch2_current) { + jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; + } + if (decoded->variant.power_metrics.has_ch3_current) { + jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + } } } else if (shouldLog) { LOG_ERROR("Error decoding proto for telemetry message!"); diff --git a/src/shutdown.h b/src/shutdown.h index f02cb7964..7e2120149 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -41,8 +41,8 @@ void powerCommandsCheck() } #if defined(ARCH_ESP32) || defined(ARCH_NRF52) - if (shutdownAtMsec) { - screen->startAlert("Shutting down..."); + if (shutdownAtMsec && screen) { + screen->showSimpleBanner("Shutting Down...", 0); // stays on screen } #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 8ffb08b04..09484f46e 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -221,8 +221,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #endif powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); - - screen->doDeepSleep(); // datasheet says this will draw only 10ua + if (screen) + screen->doDeepSleep(); // datasheet says this will draw only 10ua if (!skipSaveNodeDb) { nodeDB->saveToDisk(); @@ -332,7 +332,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN } #endif -#if defined(ARCH_ESP32) && defined(I2C_SDA) +#if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA) // Added by https://github.com/meshtastic/firmware/pull/4418 // Possibly to support Heltec Capsule Sensor? Wire.end(); @@ -542,4 +542,4 @@ void enableLoraInterrupt() } #endif } -#endif \ No newline at end of file +#endif diff --git a/suppressions.txt b/suppressions.txt index 04937523d..ab57c9298 100644 --- a/suppressions.txt +++ b/suppressions.txt @@ -53,4 +53,8 @@ internalAstError:*/CrossPlatformCryptoEngine.cpp uninitMemberVar:*/AudioThread.h // False positive constVariableReference:*/Channels.cpp -constParameterPointer:*/unishox2.c \ No newline at end of file +constParameterPointer:*/unishox2.c + +useStlAlgorithm + +variableScope \ No newline at end of file diff --git a/userPrefs.jsonc b/userPrefs.jsonc index a349a5700..3da8e7ba6 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -21,6 +21,7 @@ // "USERPREFS_CONFIG_LORA_REGION": "meshtastic_Config_LoRaConfig_RegionCode_US", // "USERPREFS_CONFIG_OWNER_LONG_NAME": "My Long Name", // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", + // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, LOST AND FOUND, and REPEATER roles are restricted. // "USERPREFS_EVENT_MODE": "1", // "USERPREFS_FIXED_BLUETOOTH": "121212", // "USERPREFS_FIXED_GPS": "", @@ -30,6 +31,7 @@ // "USERPREFS_CONFIG_SMART_POSITION_ENABLED": "false", // "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600", // "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800", + // "USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL": "900", // Device telemetry update interval in seconds // "USERPREFS_LORACONFIG_CHANNEL_NUM": "31", // "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST", // "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }", @@ -51,5 +53,7 @@ // "USERPREFS_MQTT_ENCRYPTION_ENABLED": "true", // "USERPREFS_MQTT_TLS_ENABLED": "false", // "USERPREFS_MQTT_ROOT_TOPIC": "event/REPLACEME", + // "USERPREFS_RINGTONE_NAG_SECS": "60", + "USERPREFS_RINGTONE_RTTTL": "24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p", "USERPREFS_TZ_STRING": "tzplaceholder " } diff --git a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h index c2c351925..f64de9d07 100644 --- a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -22,6 +22,9 @@ #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -53,7 +56,8 @@ void setupNicheGraphics() inkhud->setDisplayResilience(10, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings @@ -98,8 +102,14 @@ void setupNicheGraphics() buttons->setWiring(1, PIN_BUTTON1); buttons->setTiming(1, 50, 500); // 500ms before latch buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); - buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); - buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + buttons->setHandlerLongPress(1, [backlight]() { + backlight->latch(); + playBoop(); + }); + buttons->setHandlerShortPress(1, [backlight]() { + backlight->off(); + playChirp(); + }); // Begin handling button events buttons->start(); diff --git a/variants/ELECROW-ThinkNode-M1/variant.h b/variants/ELECROW-ThinkNode-M1/variant.h index 2e91e378d..79e31c54a 100644 --- a/variants/ELECROW-ThinkNode-M1/variant.h +++ b/variants/ELECROW-ThinkNode-M1/variant.h @@ -63,6 +63,9 @@ extern "C" { * Buttons */ #define PIN_BUTTON2 (32 + 10) +#define ALT_BUTTON_PIN PIN_BUTTON2 +#define ALT_BUTTON_ACTIVE_LOW true +#define ALT_BUTTON_ACTIVE_PULLUP true #define PIN_BUTTON1 (32 + 7) // #define PIN_BUTTON1 (0 + 11) diff --git a/variants/ELECROW-ThinkNode-M2/variant.h b/variants/ELECROW-ThinkNode-M2/variant.h index a6bb40f1a..cd8d43555 100644 --- a/variants/ELECROW-ThinkNode-M2/variant.h +++ b/variants/ELECROW-ThinkNode-M2/variant.h @@ -3,6 +3,9 @@ #define PIN_BUTTON1 47 // ๅŠŸ่ƒฝ้”ฎ #define PIN_BUTTON2 4 // ็”ตๆบ้”ฎ +#define ALT_BUTTON_PIN PIN_BUTTON2 +#define ALT_BUTTON_ACTIVE_LOW false +#define ALT_BUTTON_ACTIVE_PULLUP false #define LED_POWER 6 #define ADC_V 42 @@ -60,4 +63,3 @@ #define HAS_GPS 0 #define BUTTON_PIN PIN_BUTTON1 -#define BUTTON_PIN_ALT PIN_BUTTON2 diff --git a/variants/diy/esp32c3_super_mini/pins_arduino.h b/variants/diy/esp32c3_super_mini/pins_arduino.h new file mode 100644 index 000000000..a325b81eb --- /dev/null +++ b/variants/diy/esp32c3_super_mini/pins_arduino.h @@ -0,0 +1,24 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 21; +static const uint8_t RX = 20; + +static const uint8_t SDA = 1; +static const uint8_t SCL = 0; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 7; +static const uint8_t MISO = 6; +static const uint8_t SCK = 10; + +static const uint8_t A0 = 0; +static const uint8_t A1 = 1; +static const uint8_t A2 = 2; +static const uint8_t A3 = 3; +static const uint8_t A4 = 4; +static const uint8_t A5 = 5; + +#endif /* Pins_Arduino_h */ diff --git a/variants/diy/esp32c3_super_mini/variant.h b/variants/diy/esp32c3_super_mini/variant.h new file mode 100644 index 000000000..48c275912 --- /dev/null +++ b/variants/diy/esp32c3_super_mini/variant.h @@ -0,0 +1,61 @@ +#ifndef _VARIANT_ESP32C3_SUPER_MINI_ +#define _VARIANT_ESP32C3_SUPER_MINI_ + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// I2C (Wire) & OLED +#define WIRE_INTERFACES_COUNT (1) +#define I2C_SDA (1) +#define I2C_SCL (0) + +#define USE_SSD1306 + +// GPS +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN (20) +#define GPS_TX_PIN (21) + +// Button +#define BUTTON_PIN (9) // BOOT button + +// LoRa +#define USE_LLCC68 +#define USE_SX1262 +// #define USE_RF95 +#define USE_SX1268 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET (5) +#define LORA_DIO1 (3) +#define LORA_RXEN (2) +#define LORA_BUSY (4) +#define LORA_SCK (10) +#define LORA_MISO (6) +#define LORA_MOSI (7) +#define LORA_CS (8) + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN LORA_RXEN + +#define SX126X_DIO3_TCXO_VOLTAGE (1.8) +#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/diy/hydra/variant.h b/variants/diy/hydra/variant.h index 60bb60beb..0d64c1b5e 100644 --- a/variants/diy/hydra/variant.h +++ b/variants/diy/hydra/variant.h @@ -8,7 +8,9 @@ #define PIN_GPS_EN 4 #define GPS_POWER_TOGGLE // Moved definition from platformio.ini to here -#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +// Note: On the ESP32 base version, gpio34-39 are input-only, and do not have internal pull-ups. +// If 39 is not being used for a button, it is suggested to remove the #define. #define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO35_CHANNEL #define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) @@ -34,8 +36,12 @@ #define SX126X_TXEN 13 // Schematic connects EBYTE module's TXEN pin to MCU #define SX126X_RXEN 14 // Schematic connects EBYTE module's RXEN pin to MCU -#define LORA_CS SX126X_CS // Compatibility with variant file configuration structure -#define LORA_SCK SX126X_SCK // Compatibility with variant file configuration structure -#define LORA_MOSI SX126X_MOSI // Compatibility with variant file configuration structure -#define LORA_MISO SX126X_MISO // Compatibility with variant file configuration structure -#define LORA_DIO1 SX126X_DIO1 // Compatibility with variant file configuration structure +#define LORA_CS SX126X_CS // Compatibility with variant file configuration structure +#define LORA_SCK SX126X_SCK // Compatibility with variant file configuration structure +#define LORA_MOSI SX126X_MOSI // Compatibility with variant file configuration structure +#define LORA_MISO SX126X_MISO // Compatibility with variant file configuration structure +#define LORA_DIO1 SX126X_DIO1 // Compatibility with variant file configuration structure +#define LORA_TXEN SX126X_TXEN // Compatibility with variant file configuration structure +#define LORA_RXEN SX126X_RXEN // Compatibility with variant file configuration structure +#define LORA_RESET SX126X_RESET // Compatibility with variant file configuration structure +#define LORA_DIO2 SX126X_BUSY // Compatibility with variant file configuration structure \ No newline at end of file diff --git a/variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py b/variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py new file mode 100644 index 000000000..00896e21f --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py @@ -0,0 +1,62 @@ +# Simplifies DIY InkHUD builds, with presets for several common E-Ink displays +# - build using custom task in Platformio's "Project Tasks" panel +# - build with `pio run -e -t build_weact_154` (or similar) + +# Silence trunk's objections to the import statements +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821) + +from SCons.Script import COMMAND_LINE_TARGETS + +Import("env") +Import("projenv") + +# Custom targets +# These wrappers just run the normal build task under a different target name +# We intercept the build later on, based on the target name +env.AddTarget( + name="build_weact_154", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 1.54")', +) +env.AddTarget( + name="build_weact_213", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.13")', +) +env.AddTarget( + name="build_weact_290", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.9")', +) +env.AddTarget( + name="build_weact_420", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 4.2")', +) + +# Check whether a build was started via one of our custom targets above + +if "build_weact_154" in COMMAND_LINE_TARGETS: + print('Building for WeAct 1.54" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY200200_0154DAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_213" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.13" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E0213A289")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "10")) + +elif "build_weact_290" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.9" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY128296_029EAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_420" in COMMAND_LINE_TARGETS: + print('Building for WeAct 4.2" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E042A87")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) diff --git a/variants/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h b/variants/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h new file mode 100644 index 000000000..8f30a244f --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h @@ -0,0 +1,95 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/HINK_E0213A289.h" // WeAct 2.13" +#include "graphics/niche/Drivers/EInk/HINK_E042A87.h" // WeAct 4.2" +#include "graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h" // WeAct 2.9" +#include "graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h" // WeACt 1.54" + +#include "graphics/niche/Inputs/TwoButton.h" + +#if !defined(INKHUD_BUILDCONF_DRIVER) || !defined(INKHUD_BUILDCONF_DISPLAYRESILIENCE) +#error If not using a DIY preset, display model and resilience must be set manually +#endif + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + SPI.begin(); + + // Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + + // Setup the main user button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/diy/nrf52_promicro_diy_tcxo/variant.h index de49018f4..e93442c7e 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.h @@ -22,26 +22,26 @@ extern "C" { /* NRF52 PRO MICRO PIN ASSIGNMENT -| Pin   | Function   |   | Pin     | Function     | RF95 | +| Pin ย  | Function ย  | ย  | Pin ย  ย  | Function ย  ย  | RF95 | | ----- | ----------- | --- | -------- | ------------ | ----- | -| Gnd   |             |   | vbat     |             | | -| P0.06 | Serial2 RX |   | vbat     |             | | -| P0.08 | Serial2 TX |   | Gnd     |             | | -| Gnd   |             |   | reset   |             | | -| Gnd   |             |   | ext_vcc | *see 0.13   | | -| P0.17 | RXEN       |   | P0.31   | BATTERY_PIN | | -| P0.20 | GPS_RX     |   | P0.29   | BUSY         | DIO0 | -| P0.22 | GPS_TX     |   | P0.02   | MISO | MISO | -| P0.24 | GPS_EN     |   | P1.15   | MOSI         | MOSI | -| P1.00 | BUTTON_PIN |   | P1.13   | CS           | CS   | -| P0.11 | SCL         |   | P1.11   | SCK         | SCK | -| P1.04 | SDA         |   | P0.10   | DIO1/IRQ     | DIO1 | -| P1.06 | Free pin   |   | P0.09   | RESET       | RST | -|       |             |   |         |             | | -|       | Mid board   |   |         | Internal     | | -| P1.01 | Free pin   |   | 0.15     | LED         | | -| P1.02 | Free pin   |   | 0.13     | 3V3_EN       | | -| P1.07 | Free pin   |   |         |             | | +| Gnd ย  | ย  ย  ย  ย  ย  ย  | ย  | vbat ย  ย  | ย  ย  ย  ย  ย  ย  | | +| P0.06 | Serial2 RX | ย  | vbat ย  ย  | ย  ย  ย  ย  ย  ย  | | +| P0.08 | Serial2 TX | ย  | Gnd ย  ย  | ย  ย  ย  ย  ย  ย  | | +| Gnd ย  | ย  ย  ย  ย  ย  ย  | ย  | reset ย  | ย  ย  ย  ย  ย  ย  | | +| Gnd ย  | ย  ย  ย  ย  ย  ย  | ย  | ext_vcc | *see 0.13 ย  | | +| P0.17 | RXEN ย  ย  ย  | ย  | P0.31 ย  | BATTERY_PIN | | +| P0.20 | GPS_RX ย  ย  | ย  | P0.29 ย  | BUSY ย  ย  ย  ย  | DIO0 | +| P0.22 | GPS_TX ย  ย  | ย  | P0.02 ย  | MISO | MISO | +| P0.24 | GPS_EN ย  ย  | ย  | P1.15 ย  | MOSI ย  ย  ย  ย  | MOSI | +| P1.00 | BUTTON_PIN | ย  | P1.13 ย  | CS ย  ย  ย  ย  ย  | CS ย  | +| P0.11 | SCL ย  ย  ย  ย  | ย  | P1.11 ย  | SCK ย  ย  ย  ย  | SCK | +| P1.04 | SDA ย  ย  ย  ย  | ย  | P0.10 ย  | DIO1/IRQ ย  ย  | DIO1 | +| P1.06 | Free pin ย  | ย  | P0.09 ย  | RESET ย  ย  ย  | RST | +| ย  ย  ย  | ย  ย  ย  ย  ย  ย  | ย  | ย  ย  ย  ย  | ย  ย  ย  ย  ย  ย  | | +| ย  ย  ย  | Mid board ย  | ย  | ย  ย  ย  ย  | Internal ย  ย  | | +| P1.01 | Free pin ย  | ย  | 0.15 ย  ย  | LED ย  ย  ย  ย  | | +| P1.02 | Free pin ย  | ย  | 0.13 ย  ย  | 3V3_EN ย  ย  ย  | | +| P1.07 | Free pin ย  | ย  | ย  ย  ย  ย  | ย  ย  ย  ย  ย  ย  | | */ // Number of pins defined in PinDescription array @@ -185,6 +185,12 @@ settings. #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL +// E-Ink DIY +#define PIN_EINK_CS (32 + 7) +#define PIN_EINK_DC (32 + 2) +#define PIN_EINK_RES (32 + 1) +#define PIN_EINK_BUSY (32 + 6) + #ifdef __cplusplus } #endif diff --git a/variants/diy/platformio.ini b/variants/diy/platformio.ini index d8ceee9cc..1f0f6d126 100644 --- a/variants/diy/platformio.ini +++ b/variants/diy/platformio.ini @@ -68,6 +68,48 @@ lib_deps = ${nrf52840_base.lib_deps} debug_tool = jlink +; NRF52 ProMicro w/ E-Ink display +[env:nrf52_promicro_diy-inkhud] +board_level = extra +extends = nrf52840_base, inkhud +board = promicro-nrf52840 +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/diy/nrf52_promicro_diy_tcxo + -D NRF52_PROMICRO_DIY +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} + +<../variants/diy/nrf52_promicro_diy_tcxo> +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${nrf52840_base.lib_deps} +extra_scripts = + ${env.extra_scripts} + variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays + +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DXIAO_BLE_LEGACY_PINOUT -DEBYTE_E22 -DEBYTE_E22_900M30S +build_unflags = -DGPS_L76K + +; Seeed XIAO nRF52840 + EBYTE E22-900M30S - Pinout matching Wio-SX1262 (SKU 113010003) +[env:seeed_xiao_nrf52840_e22_900m30s] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M30S +build_unflags = -DGPS_L76K + +; Seeed XIAO nRF52840 + EBYTE E22-900M33S - Pinout matching Wio-SX1262 (SKU 113010003) +[env:seeed_xiao_nrf52840_e22_900m33s] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M33S +build_unflags = -DGPS_L76K + ; Seeed XIAO nRF52840 + XIAO Wio SX1262 DIY [env:seeed-xiao-nrf52840-wio-sx1262] board = xiao_ble_sense @@ -99,3 +141,16 @@ build_flags = -D ARDUINO_USB_MODE=0 -D ARDUINO_USB_CDC_ON_BOOT=1 -I variants/diy/t-energy-s3_e22 + +; ESP32 C3 Super Mini Development Board +; https://www.espboards.dev/esp32/esp32-c3-super-mini/ +[env:esp32c3_super_mini] +extends = esp32c3_base +board = esp32-c3-devkitm-1 +build_flags = + ${esp32_base.build_flags} + -D PRIVATE_HW + -I variants/diy/esp32c3_super_mini + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 +board_level = extra diff --git a/variants/diy/xiao_ble/README.md b/variants/diy/xiao_ble/README.md new file mode 100644 index 000000000..fe6dcba2d --- /dev/null +++ b/variants/diy/xiao_ble/README.md @@ -0,0 +1,168 @@ +# XIAO nrf52840/nrf52840 Sense + Ebyte E22-900M30S + +_A step-by-step guide for macOS and Linux._ + +## Introduction + +This guide will walk you through everything needed to get the XIAO nrf52840 (or XIAO nrf52840 Sense) running Meshtastic using an Ebyte E22-900M30S LoRa module. The combination of the E22 with an nRF52840 MCU is desirable because it allows for both very low idle (Rx) power draw _and_ high transmit power. + +The XIAO nrf52840 is a small but surprisingly well-appointed nRF52840 board, with enough GPIO for most Meshtastic applications and a built-in LiPo charger. + +The E22, on the other hand, is a famously inscrutable and mysterious beast. It is one of the more readily available LoRa modules capable of transmitting at 30 dBm, and includes an LNA to boost its Rx sensitivity a few dB beyond that of the SX1262. + +However, its documentation is relatively sparse overall, and seems to merely hint at (or completely omit) several key details regarding its functionality. Thus, much of what follows is a synthesis of my observations and inferences over the course of many hours of trial and error. + +### Acknowledgement and Friendly Disclaimer + +Huge thanks to those in the community who have forged the way with the E22, without whose hard work none of this would have been possible! (thebentern, riddick, rainer_vie, beegee-tokyo, geeksville, caveman99, Der_Bear, PlumRugOfDoom, BigCorvus, and many others.) + +Please take the conclusions here as a tentative work in progress, representing my current (and fairly limited) understanding of the E22 when paired with this particular MCU. It is my hope that this guide will be helpful to others who are interested in trying a DIY Meshtastic build, and also be subject to revision by folks with more experience and better test equipment. + +### Obligatory Liability Disclaimer + +This guide and all associated content is for informational purposes only. The information presented is intended for consumption only by persons having appropriate technical skill and judgement, to be used entirely at their own discretion and risk. The authors of this guide in no way provide any warranty, express or implied, toward the content herein, nor its correctness, safety, or suitability to any particular purpose. By following the instructions in this guide in part or in full, you assume all responsibility for all potential risks, including but not limited to fire, property damage, bodily injury, and death. + +## 1. Wire the board + +Connecting the E22 to the XIAO nrf52840 is straightforward, but there are a few gotchas to be mindful of. + +### On the XIAO nrf52840 + +- Pins D4 and D5 are currently mapped to `PIN_WIRE_SDA` and `PIN_WIRE_SCL`, respectively. If you are not using IยฒC and would like to free up pins D4 and D5 for use as GPIO, `PIN_WIRE_SDA` and `PIN_WIRE_SCL` can be reassigned to any two other unused pin numbers. +- Pins D6 and D7 were originally mapped to the TX and RX pins for serial interface 1 (`PIN_SERIAL1_RX` and `PIN_SERIAL1_TX`) but are currently set to -1 in `variant.h`. If you need to expose a serial interface, you can restore these pins and move e.g. `SX126X_RXEN` to pin 4 or 5 (the opposite should work too). + +### On the E22 + +- There are two options for the E22's `TXEN` pin: + 1. It can be connected to the MCU on the pin defined as `SX126X_TXEN` in `variant.h`. In this configuration, the MCU will control Tx/Rx switching "manually". As long as `SX126X_TXEN` and `SX126X_RXEN` are both defined in `variant.h` (and neither is set to `RADIOLIB_NC`), `SX126xInterface.cpp` will initialize the E22 correctly for this mode. + 2. Alternately, it can be connected to the E22's `DIO2` pin only, with neither `TXEN` nor `DIO2` being connected to the MCU. In this configuration, the E22 will control Tx/Rx switching automatically. In `variant.h`, as long as `SX126X_TXEN` is defined as `RADIOLIB_NC`, and `SX126X_RXEN` is defined and connected to the E22's `RXEN` pin, and `E22_TXEN_CONNECTED_TO_DIO2` is defined, `SX126xInterface.cpp` will initialize the E22 correctly for this mode. This configuration frees up a GPIO, and presents no drawbacks that I have found. +- Note that any combination other than the two described above will likely result in unexpected behavior. In my testing, some of these other configurations appeared to "work" at first glance, but every one I tried had at least one of the following flaws: weak Tx power, extremely poor Rx sensitivity, or the E22 overheating because TXEN was never pulled low, causing its PA to stay on indefinitely. +- Along the same lines, it is a good idea to check the E22's temperature frequently by lightly touching the shield. If you feel the shield getting hot (i.e. approaching uncomfortable to touch) near pins 1, 2, and 3, something is probably misconfigured; disconnect both the XIAO nrf52840 and E22 from power and double check wiring and pin mapping. +- Whether you opt to let the E22 control Rx and Tx or handle this manually, **the E22's `RXEN` pin must always be connected to the MCU** on the pin defined as `SX126X_RXEN` in `variant.h`. + +#### Note + +The default pin mapping in `variant.h` uses "Automatic Tx/Rx switching" mode. + +If you wire your board for Manual Tx/Rx Switching Mode, `SX126X_TXEN` must be defined (`#define #define SX126X_TXEN D6`) in `variants/seeed_xiao_nrf52840_kit/variant.h` in the code block following: + +```c +#ifdef XIAO_BLE_LEGACY_PINOUT +// Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 +``` + +### Example Wiring for Automatic Tx/Rx Switching Mode + +#### MCU -> E22 Connections + +| XIAO nrf52840 pin | variant.h definition | E22 pin | Notes | +| :---------------- | :------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D7 | SX126X_RXEN | 6 (RXEN) | These pins must still be connected, and `SX126X_RXEN` defined in `variant.h`, otherwise Rx sensitivity will be poor. | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +#### E22 -> E22 Connections + +| E22 pin | E22 pin | Notes | +| :------ | :------ | :------------------------------------------------------------------------ | +| TXEN | DIO2 | These must be physically connected for automatic Tx/Rx switching to work. | + +#### Note + +The schematic (`xiao-ble-e22-schematic.png`) in the `eagle-project` directory uses this wiring. + +### Example Wiring for Manual Tx/Rx Switching Mode + +#### MCU -> E22 Connections + +| XIAO nrf52840 pin | variant.h definition | E22 pin | Notes | +| :---------------- | :------------------- | :-------- | :---- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D6 | SX126X_TXEN | 7 (TXEN) | | +| D7 | SX126X_RXEN | 6 (RXEN) | | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +#### E22 -> E22 connections + +_(none)_ + +## 2. Build Meshtastic + +1. Follow the [Building Meshtastic Firmware](https://meshtastic.org/docs/development/firmware/build/) documentation, stop after **Build** โ†’ **Step 2** +2. For **Build** โ†’ **Step 3**, select `xiao_ble` as your target +3. Adjust source code if you: + - Wired your board for Manual Tx/Rx Switching Mode: see [Wire the Board](#1-wire-the-board) + - Used an E22-900M33S module + (this step is important to avoid **damaging the power amplifier** in the M33S module and **transmitting power above legal limits**!): + 1. Open `variants/diy/platformio.ini` + 2. Search for `[env:xiao_ble]` + 3. In the line starting with `build_flags` within this section, change `-DEBYTE_E22_900M30S` to `-DEBYTE_E22_900M33S` +4. Follow **Build** โ†’ **Step 4** to build the firmware +5. Stop here, because the **PlatformIO: Upload** step does not work for factory-fresh XIAO nrf52840 (the automatic reset to bootloader only works if Meshtastic firmware is already running) +6. The built `firmware.uf2` binary can be found in the folder `.pio/build/xiao_ble/firmware.uf2` (relative to where you cloned the Git repository to), we will need it for [flashing the firmware](#3-flash-the-firmware-to-the-xiao-nrf52840) (manually) + +## 3. Flash the Firmware to the XIAO nrf52840 + +1. Double press the XIAO nrf52840's `reset` button to put it in bootloader mode, and a USB volume named `XIAO SENSE` will appear +2. Copy the `firmware.uf2` file to the `XIAO SENSE` volume (refer to the last step of [Build Meshtastic](#2-build-meshtastic)) +3. The XIAO nrf52840's red LED will flash for several seconds as the firmware is copied +4. Once Meshtastic firmware succesfully boots, the: + 1. Green LED will turn on + 2. Red LED will flash several times to indicate flash memory writes during initial settings file creation + 3. Green LED will blink every second once the firmware is running normally +5. If you do not see the above LED patters, proceed to [Troubleshooting](#4-troubleshooting) + +## 4. Troubleshooting + +- If after flashing Meshtastic, the XIAO is bootlooped, look at the serial output (you can see this by running `meshtastic --noproto` with the device connected to your computer via USB). + - If you see that the SX1262 init result was -2, this likely indicates a wiring problem; double check your wiring and pin mapping in `variant.h`. + - If you see an error mentioning tinyFS, this may mean you need to reformat the XIAO's storage: + 1. Open the [Meshtastic web flasher](https://flasher.meshtastic.org/) + 2. Select the **_Seeed XIAO NRF52840 Kit_** + 3. Click the **_trash can icon_** to the right of **_Flash_** + 4. Follow the instructions on the screen + **Do not flash the Seeed XIAO NRF52840 Kit firmware** if you have wired the LoRa module according to this variant, as the Seeed XIAO NRF52840 Kit uses different wiring for the SX1262 LoRa chip + - If you don't see any specific error message, but the boot process is stuck or not proceeding as expected, this might also mean there is a conflict in `variant.h`. If you have made any changes to the pin mapping, ensure they do not result in a conflict. If all else fails, try reverting your changes and using the known-good configuration included here. + - The above might also mean something is wired incorrectly. Try reverting to one of the known-good example wirings in section 4. +- If the E22 gets hot to the touch: + - The power amplifier is likely running continually. Disconnect it and the XIAO from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertenly high (usually due to a pin mapping conflict). + +## 5. Notes + +- **Transmit Power** + - There is a power amplifier after the SX1262's Tx, so the actual Tx power is just over 7 dB greater than the SX1262's set Tx power (the E22-900M30S actually tops out just over 29dB at 5V according to the datasheet) + - Meshtastic firmware is aware of the gain of the E22-900M30S module, so the Meshtastic clients' Tx power setting reflects the actual output power, i.e. setting 30 dBm in the Meshtastic app programs the E22 module to correctly output 30 dBm, setting 24 dBm will output 24 dBm, etc. +- **Adequate 5V Power Supply to the E22 Module** + - Have a bypass capacitor from its 5V supply to ground; 100 ยตF works well + - Voltage must be between 5Vโ€“5.5V, lower supply voltage results in less output power; for example, with a fully charged LiPo at 4.2V, Tx power appears to max out around 26-27 dBm + +### Additional Reading + +- [S5NC/CDEBYTE_Modules](https://github.com/S5NC/CDEBYTE_Modules) has additional information about EBYTE E22 modules' internal workings, including photographs +- [RadioLib High power Radio Modules Guide](https://github.com/jgromes/RadioLib/wiki/High-power-Radio-Modules-Guide) + +## 6. Testing Methodology + +During what became a fairly long trial-and-error process, I did a lot of careful testing of Tx power and Rx sensitivity. My methodology in these tests was as follows: + +- All tests were conducted between two nodes: + 1. The XIAO nrf52840 + E22 coupled with an [Abracon ARRKP4065-S915A](https://www.digikey.com/en/products/detail/abracon-llc/ARRKP4065-S915A/8593263") ceramic patch antenna + 2. A RAK 5005/4631 coupled with a [Laird MA9-5N](https://www.streakwave.com/laird-technologies-ma9-5n-55dbi-900mhz-mobile-omni-select-mount) antenna via a 4" U.FL to Type N pigtail. + - No other nodes were powered up onsite or nearby. +- Each node and its antenna was kept in exactly the same position and orientation throughout testing. +- Other environmental factors (e.g. the location and resting position of my body in the room while testing) were controlled as carefully as possible. +- Each test comprised at least five (and often ten) runs, after which the results were averaged. +- All testing was done by sending single-character messages between nodes and observing the received RSSI reported in the message acknowledgement. Messages were sent one by one, waiting for each to be acknowledged or time out before sending the next. +- The E22's Tx power was observed by sending messages from the RAK to the XIAO nrf52840 + E22 and recording the received RSSI. +- The opposite was done to observe the E22's Rx sensitivity: messages were sent from the XIAO nrf52840 + E22 to the RAK, and the received RSSI was recorded. + While this cannot match the level of accuracy achievable with actual test equipment in a lab setting, it was nonetheless sufficient to demonstrate the (sometimes very large) differences in Tx power and Rx sensitivity between various configurations. diff --git a/variants/elecrow_panel/platformio.ini b/variants/elecrow_panel/platformio.ini index 963174560..de7f28a83 100644 --- a/variants/elecrow_panel/platformio.ini +++ b/variants/elecrow_panel/platformio.ini @@ -48,7 +48,7 @@ lib_deps = ${esp32s3_base.lib_deps} lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality hideakitai/TCA9534@0.1.1 -[crowpanel_small] ; 2.4, 2.8, 3.5 inch +[crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch extends = crowpanel_base build_flags = ${crowpanel_base.build_flags} @@ -62,7 +62,7 @@ build_flags = -D VIEW_320x240 -D MAP_FULL_REDRAW -[crowpanel_large] ; 4.3, 5.0, 7.0 inch +[crowpanel_large_esp32s3_base] ; 4.3, 5.0, 7.0 inch extends = crowpanel_base build_flags = ${crowpanel_base.build_flags} @@ -73,12 +73,13 @@ build_flags = -D DISPLAY_SET_RESOLUTION [env:elecrow-adv-24-28-tft] -extends = crowpanel_small +extends = crowpanel_small_esp32s3_base build_flags = - ${crowpanel_small.build_flags} + ${crowpanel_small_esp32s3_base.build_flags} -D SPI_FREQUENCY=80000000 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_CFG_HOST=SPI2_HOST @@ -96,13 +97,14 @@ build_flags = -D LGFX_TOUCH_ROTATION=0 [env:elecrow-adv-35-tft] -extends = crowpanel_small +extends = crowpanel_small_esp32s3_base build_flags = - ${crowpanel_small.build_flags} + ${crowpanel_small_esp32s3_base.build_flags} -D LV_CACHE_DEF_SIZE=2097152 -D SPI_FREQUENCY=60000000 -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=ILI9488 -D LGFX_ROTATION=0 -D LGFX_CFG_HOST=SPI2_HOST @@ -122,7 +124,8 @@ build_flags = ; 4.3, 5.0, 7.0 inch 800x480 IPS (V1) [env:elecrow-adv1-43-50-70-tft] -extends = crowpanel_large +extends = crowpanel_large_esp32s3_base build_flags = - ${crowpanel_large.build_flags} + ${crowpanel_large_esp32s3_base.build_flags} -D VIEW_320x240 + -D DISPLAY_SIZE=800x480 ; landscape mode diff --git a/variants/gat562_mesh_trial_tracker/platformio.ini b/variants/gat562_mesh_trial_tracker/platformio.ini new file mode 100644 index 000000000..e67f3ec8d --- /dev/null +++ b/variants/gat562_mesh_trial_tracker/platformio.ini @@ -0,0 +1,13 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:gat562_mesh_trial_tracker] +extends = nrf52840_base +board = gat562_mesh_trial_tracker +board_check = true +build_flags = ${nrf52840_base.build_flags} -Ivariants/gat562_mesh_trial_tracker -D GAT562_MESH_TRIAL_TRACKER + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/gat562_mesh_trial_tracker> +lib_deps = + ${nrf52840_base.lib_deps} diff --git a/variants/gat562_mesh_trial_tracker/variant.cpp b/variants/gat562_mesh_trial_tracker/variant.cpp new file mode 100644 index 000000000..e84b60b3b --- /dev/null +++ b/variants/gat562_mesh_trial_tracker/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/gat562_mesh_trial_tracker/variant.h b/variants/gat562_mesh_trial_tracker/variant.h new file mode 100644 index 000000000..2af0bc76d --- /dev/null +++ b/variants/gat562_mesh_trial_tracker/variant.h @@ -0,0 +1,288 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_GAT562_MESH_TRIAL_TRACKER_ +#define _VARIANT_GAT562_MESH_TRIAL_TRACKER_ + +#define GAT562_MESH_TRIAL_TRACKER + +// led pin 2 (blue), see https://github.com/meshtastic/firmware/blob/master/src/mesh/NodeDB.cpp#L723 +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +// #define PIN_EINK_CS (0 + 26) +// #define PIN_EINK_BUSY (0 + 4) +// #define PIN_EINK_DC (0 + 17) +// #define PIN_EINK_RES (-1) +// #define PIN_EINK_SCLK (0 + 3) +// #define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// Display - OLED connected via I2C +#define HAS_SCREEN 1 +#define USE_SSD1306 + +// RAKRGB +// #define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +Important for successful SX1262 initialization: + +* Setup DIO2 to control the antenna switch +* Setup DIO3 to control the TCXO power supply +* Setup the SX1262 to use it's DCDC regulator and not the LDO +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +// configure the SET pin on the RAK12039 sensor board to disable the sensor while not reading +// air quality telemetry. PIN_NFC2 doesn't seem to be used anywhere else in the codebase, but if +// you're having problems with your node behaving weirdly when a RAK12039 board isn't connected, +// try disabling this. +// #define PMSA003I_ENABLE_PIN PIN_NFC2 + +// #define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_BAUDRATE 9600 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +// #define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +// #define HAS_RTC 1 + +// #define HAS_ETHERNET 1 + +// #define RAK_4631 1 + +// #define PIN_ETHERNET_RESET 21 +// #define PIN_ETHERNET_SS PIN_EINK_CS +// #define ETH_SPI_PORT SPI1 +// #define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/heltec_capsule_sensor_v3/variant.h b/variants/heltec_capsule_sensor_v3/variant.h index 415de0559..b30b7fc3e 100644 --- a/variants/heltec_capsule_sensor_v3/variant.h +++ b/variants/heltec_capsule_sensor_v3/variant.h @@ -3,6 +3,8 @@ #define EXT_PWR_DETECT 35 #define BUTTON_PIN 18 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false #define BATTERY_PIN 7 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO7_CHANNEL diff --git a/variants/heltec_mesh_node_t114-inkhud/custom_build_tasks.py b/variants/heltec_mesh_node_t114-inkhud/custom_build_tasks.py new file mode 100644 index 000000000..00896e21f --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/custom_build_tasks.py @@ -0,0 +1,62 @@ +# Simplifies DIY InkHUD builds, with presets for several common E-Ink displays +# - build using custom task in Platformio's "Project Tasks" panel +# - build with `pio run -e -t build_weact_154` (or similar) + +# Silence trunk's objections to the import statements +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821) + +from SCons.Script import COMMAND_LINE_TARGETS + +Import("env") +Import("projenv") + +# Custom targets +# These wrappers just run the normal build task under a different target name +# We intercept the build later on, based on the target name +env.AddTarget( + name="build_weact_154", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 1.54")', +) +env.AddTarget( + name="build_weact_213", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.13")', +) +env.AddTarget( + name="build_weact_290", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.9")', +) +env.AddTarget( + name="build_weact_420", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 4.2")', +) + +# Check whether a build was started via one of our custom targets above + +if "build_weact_154" in COMMAND_LINE_TARGETS: + print('Building for WeAct 1.54" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY200200_0154DAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_213" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.13" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E0213A289")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "10")) + +elif "build_weact_290" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.9" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY128296_029EAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_420" in COMMAND_LINE_TARGETS: + print('Building for WeAct 4.2" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E042A87")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) diff --git a/variants/heltec_mesh_node_t114-inkhud/nicheGraphics.h b/variants/heltec_mesh_node_t114-inkhud/nicheGraphics.h new file mode 100644 index 000000000..b6be70ff4 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/nicheGraphics.h @@ -0,0 +1,96 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/HINK_E0213A289.h" // WeAct 2.13" +#include "graphics/niche/Drivers/EInk/HINK_E042A87.h" // WeAct 4.2" +#include "graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h" // WeAct 2.9" +#include "graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h" // WeACt 1.54" + +#include "graphics/niche/Inputs/TwoButton.h" + +#if !defined(INKHUD_BUILDCONF_DRIVER) || !defined(INKHUD_BUILDCONF_DISPLAYRESILIENCE) +// cppcheck-suppress preprocessorErrorDirective +#error If not using a DIY preset, display model and resilience must be set manually +#endif + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + SPI1.begin(); + + // Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/heltec_mesh_node_t114-inkhud/platformio.ini b/variants/heltec_mesh_node_t114-inkhud/platformio.ini new file mode 100644 index 000000000..9a5673040 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/platformio.ini @@ -0,0 +1,19 @@ +[env:heltec-mesh-node-t114-inkhud] +board_level = extra +extends = nrf52840_base, inkhud +board = heltec_mesh_node_t114 +board_check = true +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/heltec_mesh_node_t114-inkhud +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 +extra_scripts = + ${env.extra_scripts} + variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays \ No newline at end of file diff --git a/variants/heltec_mesh_node_t114-inkhud/variant.cpp b/variants/heltec_mesh_node_t114-inkhud/variant.cpp new file mode 100644 index 000000000..85c9f4a72 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); +} diff --git a/variants/heltec_mesh_node_t114-inkhud/variant.h b/variants/heltec_mesh_node_t114-inkhud/variant.h new file mode 100644 index 000000000..39cbc8f01 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/variant.h @@ -0,0 +1,175 @@ +// Unlike many other InkHUD variants, this environment does require its own variant.h file +// This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken out + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define HELTEC_MESH_NODE_T114 + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (32 + 3) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_BUILTIN LED_GREEN +#define LED_STATE_ON 0 // State when LED is lit + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 2 // How many neopixels are connected +#define NEOPIXEL_DATA 14 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) +// #define PIN_SERIAL2_EN (0 + 17) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +// Routed to footprint for PCF8563TS RTC +// Not populated on T114 V1, maybe in future? +#define PIN_WIRE_SDA (0 + 26) // P0.26 +#define PIN_WIRE_SCL (0 + 27) // P0.27 + +// I2C bus 1 +// Available on header pins, for general use +#define PIN_WIRE1_SDA (0 + 16) // P0.16 +#define PIN_WIRE1_SCL (0 + 13) // P0.13 + +/* + * Lora radio + */ + +#define USE_SX1262 +// #define USE_SX1268 +#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 24) +#define SX126X_DIO1 (0 + 20) +// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching +// #define SX1262_DIO3 (0 + 21) +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the +// main +// CPU? +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +// Not really an E22 but TTGO seems to be trying to clone that +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * E-Ink DIY + */ +#define PIN_EINK_MOSI (0 + 8) // also called SDA +#define PIN_EINK_SCLK (0 + 7) +#define PIN_EINK_CS (32 + 12) +#define PIN_EINK_DC (32 + 14) +#define PIN_EINK_RES (0 + 5) +#define PIN_EINK_BUSY (32 + 15) + +/* + * GPS pins + */ + +#define GPS_L76K + +// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K +#define GPS_RESET_MODE LOW +// #define PIN_GPS_EN (21) +#define VEXT_ENABLE (0 + 21) +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define VEXT_ON_VALUE HIGH +// #define GPS_EN_ACTIVE HIGH +#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_PPS (32 + 4) +// Seems to be missing on this new board +// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +#define PIN_SPI1_MISO -1 +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (4) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution + +#define ADC_CTRL 6 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 4 +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.90F) + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/heltec_mesh_node_t114/variant.h b/variants/heltec_mesh_node_t114/variant.h index 426085a26..f4f0baf13 100644 --- a/variants/heltec_mesh_node_t114/variant.h +++ b/variants/heltec_mesh_node_t114/variant.h @@ -56,6 +56,10 @@ extern "C" { #define TFT_WIDTH 240 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 + +// T114 gets a muted yellow on black display +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 128) + // #define TFT_OFFSET_ROTATION 0 // #define SCREEN_ROTATE // #define SCREEN_TRANSITION_FRAMERATE 5 diff --git a/variants/heltec_mesh_pocket/nicheGraphics.h b/variants/heltec_mesh_pocket/nicheGraphics.h index 271a35d6d..f8202debb 100644 --- a/variants/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/heltec_mesh_pocket/nicheGraphics.h @@ -50,7 +50,8 @@ void setupNicheGraphics() inkhud->setDisplayResilience(10, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings diff --git a/variants/heltec_mesh_pocket/platformio.ini b/variants/heltec_mesh_pocket/platformio.ini index 6632c10fe..2f3886887 100644 --- a/variants/heltec_mesh_pocket/platformio.ini +++ b/variants/heltec_mesh_pocket/platformio.ini @@ -28,7 +28,7 @@ lib_deps = https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d -[env:heltec-mesh-pocket-inkhud-5000] +[env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/heltec_mesh_pocket> ${inkhud.build_src_filter} @@ -73,7 +73,7 @@ lib_deps = https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d -[env:heltec-mesh-pocket-inkhud-10000] +[env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/heltec_mesh_pocket> ${inkhud.build_src_filter} diff --git a/variants/heltec_sensor_hub/variant.h b/variants/heltec_sensor_hub/variant.h index 771cefee3..8c5d31c9a 100644 --- a/variants/heltec_sensor_hub/variant.h +++ b/variants/heltec_sensor_hub/variant.h @@ -1,6 +1,8 @@ #define EXT_PWR_DETECT 20 #define BUTTON_PIN 17 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false #define BATTERY_PIN 7 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO7_CHANNEL diff --git a/variants/heltec_vision_master_e213/einkDetect.h b/variants/heltec_vision_master_e213/einkDetect.h new file mode 100644 index 000000000..35140db60 --- /dev/null +++ b/variants/heltec_vision_master_e213/einkDetect.h @@ -0,0 +1,35 @@ +#pragma once + +#include "configuration.h" + +enum class EInkDetectionResult : uint8_t { + LCMEN213EFC1 = 0, // Initial version + E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) +}; + +EInkDetectionResult detectEInk() +{ + // Test 1: Logic of BUSY pin + + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH + + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); + + delay(10); + + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); + + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); + + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; +} \ No newline at end of file diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h index 7eccb2955..1b1291424 100644 --- a/variants/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -18,13 +18,22 @@ // Shared NicheGraphics components // -------------------------------- +#include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" +#include "buzz.h" // Button feedback +#include "einkDetect.h" // Detect display model at runtime + void setupNicheGraphics() { using namespace NicheGraphics; + // Detect E-Ink Model + // ------------------- + + EInkDetectionResult displayModel = detectEInk(); + // SPI // ----------------------------- @@ -35,7 +44,13 @@ void setupNicheGraphics() // E-Ink Driver // ----------------------------- - Drivers::EInk *driver = new Drivers::LCMEN213EFC1; + Drivers::EInk *driver; + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + driver = new Drivers::E0213A367; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD @@ -48,10 +63,15 @@ void setupNicheGraphics() // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + inkhud->setDisplayResilience(15, 3); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings @@ -84,8 +104,11 @@ void setupNicheGraphics() buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button - buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); }); + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playChirp(); + }); // Begin handling button events buttons->start(); diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini index 037d10168..028caaeff 100644 --- a/variants/heltec_vision_master_e213/platformio.ini +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -7,7 +7,8 @@ build_flags = -Ivariants/heltec_vision_master_e213 -DHELTEC_VISION_MASTER_E213 -DUSE_EINK - -DEINK_DISPLAY_MODEL=GxEPD2_213_FC1 + -DGXEPD2_DRIVER_0=GxEPD2_213_FC1 + -DGXEPD2_DRIVER_1=GxEPD2_213_E0213A367 -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk @@ -16,7 +17,7 @@ build_flags = -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 @@ -32,7 +33,6 @@ build_flags = ${inkhud.build_flags} -I variants/heltec_vision_master_e213 -D HELTEC_VISION_MASTER_E213 - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} diff --git a/variants/heltec_vision_master_e213/variant.h b/variants/heltec_vision_master_e213/variant.h index ebb2c341f..60f4e00cc 100644 --- a/variants/heltec_vision_master_e213/variant.h +++ b/variants/heltec_vision_master_e213/variant.h @@ -1,7 +1,7 @@ #define LED_PIN 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 -#define BUTTON_PIN_SECONDARY 21 // Second built-in button -#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input +#define PIN_BUTTON2 21 // Second built-in button +#define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA diff --git a/variants/heltec_vision_master_e290/nicheGraphics.h b/variants/heltec_vision_master_e290/nicheGraphics.h index af78df746..61b08c740 100644 --- a/variants/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/heltec_vision_master_e290/nicheGraphics.h @@ -34,6 +34,9 @@ Different NicheGraphics UIs and different hardware variants will each have their #include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -64,7 +67,8 @@ void setupNicheGraphics() inkhud->setDisplayResilience(7, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings @@ -97,8 +101,11 @@ void setupNicheGraphics() buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button - buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); }); + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playChirp(); + }); // Begin handling button events buttons->start(); diff --git a/variants/heltec_vision_master_e290/platformio.ini b/variants/heltec_vision_master_e290/platformio.ini index 6952e9f9e..cda3fde00 100644 --- a/variants/heltec_vision_master_e290/platformio.ini +++ b/variants/heltec_vision_master_e290/platformio.ini @@ -36,7 +36,6 @@ build_flags = ${inkhud.build_flags} -I variants/heltec_vision_master_e290 -D HELTEC_VISION_MASTER_E290 - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} diff --git a/variants/heltec_vision_master_e290/variant.h b/variants/heltec_vision_master_e290/variant.h index 02986d26b..d7bae7dc2 100644 --- a/variants/heltec_vision_master_e290/variant.h +++ b/variants/heltec_vision_master_e290/variant.h @@ -1,7 +1,7 @@ #define LED_PIN 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 -#define BUTTON_PIN_SECONDARY 21 // Second built-in button -#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input +#define PIN_BUTTON2 21 // Second built-in button +#define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA diff --git a/variants/heltec_vision_master_t190/variant.h b/variants/heltec_vision_master_t190/variant.h index 788466919..a6a809207 100644 --- a/variants/heltec_vision_master_t190/variant.h +++ b/variants/heltec_vision_master_t190/variant.h @@ -1,6 +1,7 @@ +#ifndef HAS_TFT #define BUTTON_PIN 0 -#define BUTTON_PIN_SECONDARY 21 // Second built-in button -#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input +#define PIN_BUTTON2 21 // Second built-in button +#define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA @@ -68,4 +69,5 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif // HAS_TFT \ No newline at end of file diff --git a/variants/heltec_wireless_bridge/platformio.ini b/variants/heltec_wireless_bridge/platformio.ini index 45c3aba74..ab30eb744 100644 --- a/variants/heltec_wireless_bridge/platformio.ini +++ b/variants/heltec_wireless_bridge/platformio.ini @@ -3,4 +3,21 @@ extends = esp32_base board = heltec_wifi_lora_32 build_flags = - ${esp32_base.build_flags} -D HELTEC_WIRELESS_BRIDGE -I variants/heltec_wireless_bridge \ No newline at end of file + ${esp32_base.build_flags} + -I variants/heltec_wireless_bridge + -D HELTEC_WIRELESS_BRIDGE + -D BOARD_HAS_PSRAM + -D RADIOLIB_EXCLUDE_LR11X0=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 + -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 + -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 + -D MESHTASTIC_EXCLUDE_GPS=1 + -D MESHTASTIC_EXCLUDE_I2C=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_POWER_FSM=1 + -D MESHTASTIC_EXCLUDE_SERIAL=1 + -D MESHTASTIC_EXCLUDE_SCREEN=1 + -D MESHTASTIC_EXCLUDE_WAYPOINT=1 diff --git a/variants/heltec_wireless_bridge/variant.h b/variants/heltec_wireless_bridge/variant.h index 7c4f41660..5ad16d0e2 100644 --- a/variants/heltec_wireless_bridge/variant.h +++ b/variants/heltec_wireless_bridge/variant.h @@ -1,29 +1,41 @@ -// the default ESP32 Pin of 15 is the Oled SCL, set to 36 and 37 and works fine. -// Tested on Neo6m module. + +// updated variant 20250420 berlincount, tested with HTIT-TB +// +// connections in HTIT-WB +// per https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf +// md5: a0e6ae10ff76611aa61433366b2e4f5c esp32_datasheet_en.pdf +// per https://resource.heltec.cn/download/Wireless_Bridge/Schematic_Diagram_HTIT-WB_V0.2.pdf +// md5: d5c1b0219ece347dd8cee866d7d3ab0a Schematic_Diagram_HTIT-WB_V0.2.pdf + +#define NO_EXT_GPIO 1 +#define NO_GPS 1 + +#define HAS_GPS 0 // GPS is not equipped #undef GPS_RX_PIN #undef GPS_TX_PIN -#define GPS_RX_PIN 36 -#define GPS_TX_PIN 33 -#ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag -#define I2C_SDA 4 // I2C pins for this board -#define I2C_SCL 15 -#endif - -#define LED_PIN 25 // If defined we will blink this LED -#define BUTTON_PIN 0 // If defined, this will be used for user button presses +// Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16 +#define LED_PIN 22 +#define WIFI_LED 23 +#define BLE_LED 25 +// ESP32-D0WDQ6 direct pins SX1276 #define USE_RF95 -#define LORA_DIO0 26 // a No connect on the SX1262 module -#ifndef USE_JTAG -#define LORA_RESET 14 -#endif +#define LORA_DIO0 26 #define LORA_DIO1 35 -#define LORA_DIO2 34 // Not really used +#define LORA_DIO2 34 +#define LORA_SCK 05 +#define LORA_MISO 19 +#define LORA_MOSI 27 +#define LORA_CS 18 -// ratio of voltage divider = 3.20 (R1=100k, R2=220k) -#define ADC_MULTIPLIER 3.2 +// several things are not possible with JTAG enabled +#ifndef USE_JTAG +#define LORA_RESET 14 // LoRa Reset shares a pin with MTMS +#define I2C_SDA 4 // SD_DATA1 going to W25Q64, but +#define I2C_SCL 15 // SD_CMD shared a pin with MTD0 +#endif -#define BATTERY_PIN 13 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage -#define ADC_CHANNEL ADC2_GPIO13_CHANNEL -#define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file +// user button is present on device, but currently untested & unconfigured - couldn't figure out how it's connected + +// battery support is present within device, but currently untested & unconfigured - couldn't find reliable information yet diff --git a/variants/heltec_wireless_paper/einkDetect.h b/variants/heltec_wireless_paper/einkDetect.h new file mode 100644 index 000000000..93b3f86e3 --- /dev/null +++ b/variants/heltec_wireless_paper/einkDetect.h @@ -0,0 +1,35 @@ +#pragma once + +#include "configuration.h" + +enum class EInkDetectionResult : uint8_t { + LCMEN213EFC1 = 0, // V1.1 + E0213A367 = 1, // V1.1.1, V1.2 +}; + +EInkDetectionResult detectEInk() +{ + // Test 1: Logic of BUSY pin + + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH + + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); + + delay(10); + + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); + + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); + + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; +} \ No newline at end of file diff --git a/variants/heltec_wireless_paper/nicheGraphics.h b/variants/heltec_wireless_paper/nicheGraphics.h index c8994b7f1..445b57714 100644 --- a/variants/heltec_wireless_paper/nicheGraphics.h +++ b/variants/heltec_wireless_paper/nicheGraphics.h @@ -18,13 +18,21 @@ // Shared NicheGraphics components // -------------------------------- +#include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" +#include "einkDetect.h" // Detect display model at runtime + void setupNicheGraphics() { using namespace NicheGraphics; + // Detect E-Ink Model + // ------------------- + + EInkDetectionResult displayModel = detectEInk(); + // SPI // ----------------------------- @@ -35,7 +43,13 @@ void setupNicheGraphics() // E-Ink Driver // ----------------------------- - Drivers::EInk *driver = new Drivers::LCMEN213EFC1; + Drivers::EInk *driver; + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + driver = new Drivers::E0213A367; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD @@ -48,10 +62,15 @@ void setupNicheGraphics() // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + inkhud->setDisplayResilience(15, 3); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 51430ebff..790646056 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -7,7 +7,8 @@ build_flags = ${esp32s3_base.build_flags} -I variants/heltec_wireless_paper -D HELTEC_WIRELESS_PAPER - -D EINK_DISPLAY_MODEL=GxEPD2_213_FC1 + -D GXEPD2_DRIVER_0=GxEPD2_213_FC1 + -D GXEPD2_DRIVER_1=GxEPD2_213_E0213A367 -D EINK_WIDTH=250 -D EINK_HEIGHT=122 -D USE_EINK @@ -17,7 +18,7 @@ build_flags = -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 @@ -33,7 +34,6 @@ build_flags = ${inkhud.build_flags} -I variants/heltec_wireless_paper -D HELTEC_WIRELESS_PAPER - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} diff --git a/variants/link32_s3_v1/variant.h b/variants/link32_s3_v1/variant.h index 1f8a7435a..a16c0ff68 100644 --- a/variants/link32_s3_v1/variant.h +++ b/variants/link32_s3_v1/variant.h @@ -6,7 +6,9 @@ #define USE_SSD1306 #define BUTTON_PIN 0 // Button pin for this board -#define BUTTON_PIN_ALT 36 +#define CANCEL_BUTTON_PIN 36 +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP true #define HAS_NEOPIXEL // If defined, we will use the neopixel library #define NEOPIXEL_DATA 35 // Neopixel pin for this board diff --git a/variants/mesh-tab/platformio.ini b/variants/mesh-tab/platformio.ini index 728fa5100..52f9fc13c 100644 --- a/variants/mesh-tab/platformio.ini +++ b/variants/mesh-tab/platformio.ini @@ -28,7 +28,6 @@ build_flags = ${esp32s3_base.build_flags} -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D RADIOLIB_SPI_PARANOID=0 - -D MAX_THREADS=40 -D HAS_SCREEN=0 -D HAS_TFT=1 -D USE_PIN_BUZZER @@ -86,6 +85,7 @@ build_flags = ${mesh_tab_xpt2046.build_flags} -D SPI_FREQUENCY=60000000 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ST7789 -D LGFX_INVERT_COLOR=false -D LGFX_ROTATION=3 @@ -98,6 +98,7 @@ build_flags = ${mesh_tab_xpt2046.build_flags} -D SPI_FREQUENCY=60000000 ; if image is distorted then lower to 40 MHz -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ILI9341 -D LGFX_ROTATION=1 -D LGFX_TOUCH_ROTATION=4 @@ -110,6 +111,7 @@ build_flags = ${mesh_tab_xpt2046.build_flags} -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=ILI9488 -D LGFX_ROTATION=0 -D LGFX_TOUCH_ROTATION=0 @@ -122,6 +124,7 @@ build_flags = ${mesh_tab_xpt2046.build_flags} -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=HX8357B -D LGFX_INVERT_COLOR=false -D LGFX_ROTATION=4 @@ -134,6 +137,7 @@ build_flags = ${mesh_tab_ft5x06.build_flags} -D SPI_FREQUENCY=75000000 ; may go higher upto 60/80 MHz -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ILI9341 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 @@ -150,6 +154,7 @@ build_flags = ${mesh_tab_ft5x06.build_flags} -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=ILI9488 -D LGFX_ROTATION=2 -D LGFX_TOUCH_X_MIN=0 @@ -166,6 +171,7 @@ build_flags = ${mesh_tab_ft5x06.build_flags} -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=HX8357B -D LGFX_ROTATION=4 -D LGFX_TOUCH_X_MIN=0 diff --git a/variants/nano-g1-explorer/variant.h b/variants/nano-g1-explorer/variant.h index 3d5d71acc..f3640241a 100644 --- a/variants/nano-g1-explorer/variant.h +++ b/variants/nano-g1-explorer/variant.h @@ -3,9 +3,7 @@ #define I2C_SDA 21 #define I2C_SCL 22 -#define BUTTON_PIN 36 // The user button (information button) GPIO on the Nano G1 explorer -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_PIN 36 // The user button (information button) GPIO on the Nano G1 explorer #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. // common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if diff --git a/variants/nano-g1/variant.h b/variants/nano-g1/variant.h index dd8355492..2521c3ffe 100644 --- a/variants/nano-g1/variant.h +++ b/variants/nano-g1/variant.h @@ -3,9 +3,7 @@ #define I2C_SDA 21 #define I2C_SCL 22 -#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. // common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if diff --git a/variants/nugget_s3_lora/platformio.ini b/variants/nugget_s3_lora/platformio.ini index 729a3ef23..1085d633b 100644 --- a/variants/nugget_s3_lora/platformio.ini +++ b/variants/nugget_s3_lora/platformio.ini @@ -2,5 +2,5 @@ extends = esp32s3_base board = lolin_s3_mini board_level = extra -build_flags = - ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/nugget_s3_lora \ No newline at end of file +build_flags = + ${esp32s3_base.build_flags} -D ARDUINO_USB_CDC_ON_BOOT=1 -D PRIVATE_HW -I variants/nugget_s3_lora diff --git a/variants/nugget_s3_lora/variant.h b/variants/nugget_s3_lora/variant.h index 488fe4e44..8e6057d5b 100644 --- a/variants/nugget_s3_lora/variant.h +++ b/variants/nugget_s3_lora/variant.h @@ -1,5 +1,8 @@ -#define I2C_SDA 34 // I2C pins for this board -#define I2C_SCL 38 +#define I2C_SDA 35 // I2C pins for this board +#define I2C_SCL 36 + +#define USE_SSD1306 +#define DISPLAY_FLIP_SCREEN #define LED_PIN 15 // If defined we will blink this LED @@ -8,7 +11,8 @@ #define NEOPIXEL_DATA 10 // gpio pin used to send data to the neopixels #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use -#define BUTTON_PIN 0 // If defined, this will be used for user button presses +// Button A (44), B (43), R (12), U (13), L (11), D (18) +#define BUTTON_PIN 44 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define USE_RF95 diff --git a/variants/picomputer-s3/platformio.ini b/variants/picomputer-s3/platformio.ini index df2d0dfdc..cb5e829b4 100644 --- a/variants/picomputer-s3/platformio.ini +++ b/variants/picomputer-s3/platformio.ini @@ -26,21 +26,15 @@ extends = env:picomputer-s3 build_flags = ${env:picomputer-s3.build_flags} - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_MATRIX_TYPE=1 -D USE_PIN_BUZZER=PIN_BUZZER -D USE_SX127x - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1024 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D RAM_SIZE=1560 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 -D LV_USE_PERF_MONITOR=0 @@ -48,10 +42,13 @@ build_flags = -D LV_USE_LOG=0 -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D LGFX_SCREEN_WIDTH=240 + -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_DRIVER=LGFX_PICOMPUTER_S3 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_PICOMPUTER_S3.h\" -D VIEW_320x240 -; -D USE_DOUBLE_BUFFER +; -D USE_DOUBLE_BUFFER -D USE_PACKET_API lib_deps = diff --git a/variants/picomputer-s3/variant.h b/variants/picomputer-s3/variant.h index ff8faa6f4..8252e841c 100644 --- a/variants/picomputer-s3/variant.h +++ b/variants/picomputer-s3/variant.h @@ -49,7 +49,7 @@ #define SCREEN_TRANSITION_FRAMERATE 5 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) #define CANNED_MESSAGE_MODULE_ENABLE 1 diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini index fe89ad6e6..5293b12b9 100644 --- a/variants/portduino/platformio.ini +++ b/variants/portduino/platformio.ini @@ -22,11 +22,11 @@ lib_deps = ${native_base.lib_deps} ${device-ui_base.lib_deps} build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D RAM_SIZE=16384 -D USE_X11=1 -D HAS_TFT=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 + -D LV_CACHE_DEF_SIZE=6291456 -D LV_BUILD_TEST=0 -D LV_USE_LIBINPUT=1 @@ -41,7 +41,6 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio !pkg-config --libs openssl --silence-errors || : build_src_filter = ${native_base.build_src_filter} - - [env:native-fb] extends = native_base @@ -51,12 +50,11 @@ lib_deps = ${device-ui_base.lib_deps} board_level = extra build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D RAM_SIZE=8192 -D USE_FRAMEBUFFER=1 -D LV_COLOR_DEPTH=32 -D HAS_TFT=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D LV_BUILD_TEST=0 -D LV_USE_LOG=0 -D LV_USE_EVDEV=1 @@ -72,7 +70,6 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections !pkg-config --libs openssl --silence-errors || : build_src_filter = ${native_base.build_src_filter} - - [env:native-tft-debug] extends = native_base @@ -82,7 +79,6 @@ lib_deps = ${device-ui_base.lib_deps} board_level = extra build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D DEBUG_HEAP -D RAM_SIZE=16384 -D USE_X11=1 diff --git a/variants/rak3172/platformio.ini b/variants/rak3172/platformio.ini index 456697aef..99610b17c 100644 --- a/variants/rak3172/platformio.ini +++ b/variants/rak3172/platformio.ini @@ -5,6 +5,8 @@ board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/rak3172 + -DPIN_WIRE_SDA=PA11 + -DPIN_WIRE_SCL=PA12 -DHAL_DAC_MODULE_ONLY -DHAL_RNG_MODULE_ENABLED -DRADIOLIB_EXCLUDE_SX128X=1 @@ -18,6 +20,5 @@ build_flags = -DMESHTASTIC_EXCLUDE_SCREEN=1 -DMESHTASTIC_EXCLUDE_MQTT=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 - ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ;-DCFG_DEBUG upload_port = stlink diff --git a/variants/rak3312/pins_arduino.h b/variants/rak3312/pins_arduino.h new file mode 100644 index 000000000..15a26e991 --- /dev/null +++ b/variants/rak3312/pins_arduino.h @@ -0,0 +1,28 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "variant.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 9; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 12; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 10; +static const uint8_t SCK = 13; + +#define SPI_MOSI (11) +#define SPI_SCK (13) +#define SPI_MISO (10) +#define SPI_CS (12) + +// LEDs +#define LED_BUILTIN LED_GREEN + +#endif /* Pins_Arduino_h */ diff --git a/variants/rak3312/platformio.ini b/variants/rak3312/platformio.ini new file mode 100644 index 000000000..d2877b3f7 --- /dev/null +++ b/variants/rak3312/platformio.ini @@ -0,0 +1,8 @@ +[env:rak3312] +extends = esp32s3_base +board = wiscore_rak3312 +board_check = true +upload_protocol = esptool + +build_flags = + ${esp32_base.build_flags} -D RAK3312 -I variants/rak3312 diff --git a/variants/rak3312/variant.h b/variants/rak3312/variant.h new file mode 100644 index 000000000..dfdf4de71 --- /dev/null +++ b/variants/rak3312/variant.h @@ -0,0 +1,44 @@ +#define I2C_SDA 9 +#define I2C_SCL 40 + +#define USE_SX1262 + +#define LORA_SCK 5 +#define LORA_MISO 3 +#define LORA_MOSI 6 +#define LORA_CS 7 +#define LORA_RESET 8 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 47 +#define SX126X_BUSY 48 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +#define SX126X_POWER_EN (4) + +#define PIN_POWER_EN PIN_3V3_EN +#define PIN_3V3_EN (14) + +#define LED_GREEN 46 +#define LED_BLUE 45 + +#define PIN_LED1 LED_GREEN +#define PIN_LED2 LED_BLUE + +#define LED_CONN LED_BLUE +#define LED_PIN LED_GREEN +#define ledOff(pin) pinMode(pin, INPUT) + +#define LED_STATE_ON 1 // State when LED is litted + +#define HAS_GPS 1 +#define GPS_TX_PIN 43 +#define GPS_RX_PIN 44 + +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_MULTIPLIER 1.667 \ No newline at end of file diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index f2d68e704..ee134e87a 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -18,6 +18,7 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) @@ -51,4 +52,4 @@ lib_deps = upload_protocol = stlink ; eventually use platformio/tool-pyocd@^2.3600.0 instad ;upload_protocol = custom -;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index 0da1c04ea..f5ec11ef2 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -88,8 +88,13 @@ static const uint8_t A7 = PIN_A7; #define ADC_RESOLUTION 14 // Other pins +#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT +#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT + #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -143,8 +148,8 @@ static const uint8_t SCK = PIN_SPI_SCK; */ #define WIRE_INTERFACES_COUNT 1 -#define PIN_WIRE_SDA (13) -#define PIN_WIRE_SCL (14) +#define PIN_WIRE_SDA (WB_I2C1_SDA) +#define PIN_WIRE_SCL (WB_I2C1_SCL) // QSPI Pins #define PIN_QSPI_SCK 3 @@ -219,10 +224,15 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // Testing USB detection #define NRF_APM +// If using a power chip like the INA3221 you can override the default battery voltage channel below +// and comment out NRF_APM to use the INA3221 instead of the USB detection for charging +// #define INA3221_BAT_CH INA3221_CH2 +// #define INA3221_ENV_CH INA3221_CH1 // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base @@ -276,4 +286,4 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_epaper/platformio.ini b/variants/rak4631_epaper/platformio.ini index 7c8a299bb..47e4451c7 100644 --- a/variants/rak4631_epaper/platformio.ini +++ b/variants/rak4631_epaper/platformio.ini @@ -16,6 +16,7 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAKwireless RAK12034@^1.0.0 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/rak4631_epaper/variant.h b/variants/rak4631_epaper/variant.h index 0bb97498c..c1e11bee5 100644 --- a/variants/rak4631_epaper/variant.h +++ b/variants/rak4631_epaper/variant.h @@ -90,6 +90,8 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -188,6 +190,7 @@ static const uint8_t SCK = PIN_SPI_SCK; // enables 3.3V periphery like GPS or IO Module #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base @@ -231,4 +234,4 @@ static const uint8_t SCK = PIN_SPI_SCK; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_epaper_onrxtx/platformio.ini b/variants/rak4631_epaper_onrxtx/platformio.ini index c749fc686..52a13f2a7 100644 --- a/variants/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/rak4631_epaper_onrxtx/platformio.ini @@ -18,6 +18,7 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAKwireless RAK12034@^1.0.0 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/rak4631_epaper_onrxtx/variant.h b/variants/rak4631_epaper_onrxtx/variant.h index 5888cff33..1f8257e8e 100644 --- a/variants/rak4631_epaper_onrxtx/variant.h +++ b/variants/rak4631_epaper_onrxtx/variant.h @@ -69,7 +69,9 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) -// #define PIN_NFC1 (9) +#define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) // #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -160,6 +162,7 @@ static const uint8_t SCK = PIN_SPI_SCK; // enables 3.3V periphery like GPS or IO Module #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // NO GPS #undef GPS_RX_PIN @@ -202,4 +205,4 @@ static const uint8_t SCK = PIN_SPI_SCK; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_eth_gw/variant.h b/variants/rak4631_eth_gw/variant.h index bc5541336..c8a2f83ae 100644 --- a/variants/rak4631_eth_gw/variant.h +++ b/variants/rak4631_eth_gw/variant.h @@ -90,6 +90,8 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -217,6 +219,7 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base @@ -270,4 +273,4 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_nomadstar_meteor_pro/platformio.ini b/variants/rak4631_nomadstar_meteor_pro/platformio.ini new file mode 100644 index 000000000..d5fbe6a16 --- /dev/null +++ b/variants/rak4631_nomadstar_meteor_pro/platformio.ini @@ -0,0 +1,51 @@ +; NomadStar Meteor Pro based on RAK4631 with RGBW LED LP5562 support +[env:rak4631_nomadstar_meteor_pro] +extends = nrf52840_base +board = wiscore_rak4631 +board_check = true +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_nomadstar_meteor_pro -D NOMADSTAR_METEOR_PRO + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + ;-DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_nomadstar_meteor_pro> + + +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library.git#9c366c8 + +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" +[env:rak4631_nomadstar_meteor_pro_dbg] +extends = env:rak4631_nomadstar_meteor_pro +board_level = extra + +; if the builtin version of openocd has a buggy version of semihosting, so use the external version +; platform_packages = platformio/tool-openocd@^3.1200.0 + +build_flags = + ${env:rak4631.build_flags} + -D USE_SEMIHOSTING + +lib_deps = + ${env:rak4631.lib_deps} + https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip + +; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. +; However the built in openocd version in platformio has buggy support for TCP to semihosting. +; +; So I'm now trying the external openocd - but the openocd scripts for nrf52.cfg assume you are using a DAP adapter not an STLINK adapter. +; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. + +upload_protocol = stlink +; eventually use platformio/tool-pyocd@^2.3600.0 instad +;upload_protocol = custom +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file diff --git a/variants/rak4631_nomadstar_meteor_pro/variant.cpp b/variants/rak4631_nomadstar_meteor_pro/variant.cpp new file mode 100644 index 000000000..e84b60b3b --- /dev/null +++ b/variants/rak4631_nomadstar_meteor_pro/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak4631_nomadstar_meteor_pro/variant.h b/variants/rak4631_nomadstar_meteor_pro/variant.h new file mode 100644 index 000000000..51baf3ada --- /dev/null +++ b/variants/rak4631_nomadstar_meteor_pro/variant.h @@ -0,0 +1,271 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// Texas Instrument LP5562 +#define HAS_LP5562 +#define ENABLE_AMBIENTLIGHTING + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +Important for successful SX1262 initialization: + +* Setup DIO2 to control the antenna switch +* Setup DIO3 to control the TCXO power supply +* Setup the SX1262 to use it's DCDC regulator and not the LDO +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 0 + +#define HAS_ETHERNET 0 + +#define RAK_4631 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS PIN_EINK_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak_wismeshtap/platformio.ini b/variants/rak_wismeshtap/platformio.ini index 6ed97c7ad..bfb3ea927 100644 --- a/variants/rak_wismeshtap/platformio.ini +++ b/variants/rak_wismeshtap/platformio.ini @@ -22,6 +22,7 @@ lib_deps = bodmer/TFT_eSPI beegee-tokyo/RAKwireless RAK12034@^1.0.0 beegee-tokyo/RAK14014-FT6336U @ 1.0.1 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/rak_wismeshtap/variant.h b/variants/rak_wismeshtap/variant.h index 1980dc4a1..f961ddf6e 100644 --- a/variants/rak_wismeshtap/variant.h +++ b/variants/rak_wismeshtap/variant.h @@ -90,6 +90,8 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -176,11 +178,11 @@ static const uint8_t SCK = PIN_SPI_SCK; // No reason not to have the RAK Wireless pin defs here too. This allows code from example RAK sketches to run without // modification. -static const uint8_t WB_IO1 = 17; // SLOT_A SLOT_B -static const uint8_t WB_IO2 = 34; // SLOT_A SLOT_B -static const uint8_t WB_IO3 = 21; // SLOT_C -static const uint8_t WB_IO4 = 4; // SLOT_C -static const uint8_t WB_IO5 = 9; // SLOT_D +static const uint8_t WB_IO1 = 17; // SLOT_A SLOT_B +static const uint8_t WB_IO2 = 34; // SLOT_A SLOT_B +static const uint8_t WB_IO3 = 21; // SLOT_C +// static const uint8_t WB_IO4 = 4; // SLOT_C <- already defined above (ln. 94) +// static const uint8_t WB_IO5 = 9; // SLOT_D <- already defined above (ln. 93) static const uint8_t WB_IO6 = 10; // SLOT_D static const uint8_t WB_SW1 = 33; // IO_SLOT static const uint8_t WB_A0 = 5; // IO_SLOT @@ -314,4 +316,4 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index b643288a6..63f814b57 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -36,17 +36,9 @@ upload_speed = 460800 build_flags = ${env:seeed-sensecap-indicator.build_flags} - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_SCREEN=1 - -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_BUTTON_TYPE=38 - -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=4096 @@ -61,6 +53,9 @@ build_flags = -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D CUSTOM_TOUCH_DRIVER + -D LGFX_SCREEN_WIDTH=480 + -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=480x480 -D LGFX_DRIVER=LGFX_INDICATOR -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_INDICATOR.h\" -D VIEW_320x240 diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h index 1be93f642..73fc0473b 100644 --- a/variants/seeed-sensecap-indicator/variant.h +++ b/variants/seeed-sensecap-indicator/variant.h @@ -7,9 +7,10 @@ #define SENSOR_PORT_NUM 2 #define SENSOR_BAUD_RATE 115200 -#if !HAS_TFT #define BUTTON_PIN 38 -#endif +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true + // #define BUTTON_NEED_PULLUP // #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/seeed_wio_tracker_L1/variant.cpp b/variants/seeed_wio_tracker_L1/variant.cpp index 6c34d63e6..a045b0cf9 100644 --- a/variants/seeed_wio_tracker_L1/variant.cpp +++ b/variants/seeed_wio_tracker_L1/variant.cpp @@ -59,8 +59,8 @@ const uint32_t g_ADigitalPinMap[] = { // D16 - Battery voltage ADC input 31, // D16 P0.31 VBAT_ADC // GROVE - 0, // D17 P0.00 GROVESDA - 1, // D18 P0.01 GROVESCL + 43, // D17 P0.00 GROVESDA + 44, // D18 P0.01 GROVESCL // FLASH 21, // D19 P0.21 (QSPI_SCK) diff --git a/variants/seeed_wio_tracker_L1/variant.h b/variants/seeed_wio_tracker_L1/variant.h index b257fd9b6..0c5964c5a 100644 --- a/variants/seeed_wio_tracker_L1/variant.h +++ b/variants/seeed_wio_tracker_L1/variant.h @@ -33,15 +33,15 @@ // โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” // Button Configuration // โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” -#define BUTTON_PIN D13 // This is the Program Button +#define CANCEL_BUTTON_PIN D13 // This is the Program Button // #define BUTTON_NEED_PULLUP 1 -#define BUTTON_ACTIVE_LOW true -#define BUTTON_ACTIVE_PULLUP false +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false -#define BUTTON_PIN_TOUCH 13 // Touch button -// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” -// Digital Pin Mapping (D0-D10) -// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// #define BUTTON_PIN_TOUCH 13 // Touch button +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Digital Pin Mapping (D0-D10) +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” #define D0 0 // P1.06 GNSS_WAKEUP/IO0 #define D1 1 // P0.07 LORA_DIO1 #define D2 2 // P1.07 LORA_RESET @@ -74,10 +74,12 @@ // Communication Interfaces // โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” // I2C Configuration -#define HAS_WIRE 1 +// #define HAS_WIRE 1 #define PIN_WIRE_SDA D14 // P0.09 #define PIN_WIRE_SCL D15 // P0.10 -#define WIRE_INTERFACES_COUNT 1 +#define WIRE_INTERFACES_COUNT 2 +#define PIN_WIRE1_SDA D18 +#define PIN_WIRE1_SCL D17 #define I2C_NO_RESCAN static const uint8_t SDA = PIN_WIRE_SDA; @@ -167,6 +169,7 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define TB_LEFT 27 #define TB_RIGHT 28 #define TB_PRESS 29 +#define TB_DIRECTION FALLING // โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” // Compatibility Definitions // โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” diff --git a/variants/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/seeed_wio_tracker_L1_eink/nicheGraphics.h new file mode 100644 index 000000000..a32753343 --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -0,0 +1,106 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/GDEY0213B74.h" +#include "graphics/niche/Inputs/TwoButton.h" + +// Special case - fix T-Echo's touch button +// ---------------------------------------- +// On a handful of T-Echos, LoRa TX triggers the capacitive touch +// To avoid this, we lockout the button during TX +#include "mesh/RadioLibInterface.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); + + // E-Ink Driver + // ----------------------------- + + Drivers::EInk *driver = new Drivers::GDEY0213B74; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the E-Ink driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); + + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + // 270 degrees clockwise + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + + // Setup backlight controller + // Note: AUX button attached further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + + inkhud->persistence->settings.rotation = 1; + // inkhud->persistence->printSettings(&inkhud->persistence->settings); + // Start running InkHUD + inkhud->begin(); + // inkhud->persistence->printSettings(&inkhud->persistence->settings); + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + // Begin handling button events + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/seeed_wio_tracker_L1_eink/platformio.ini b/variants/seeed_wio_tracker_L1_eink/platformio.ini new file mode 100644 index 000000000..b84757b9d --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/platformio.ini @@ -0,0 +1,14 @@ +[env:seeed_wio_tracker_L1_eink] +board = seeed_wio_tracker_L1 +extends = nrf52840_base, inkhud +;board_level = extra +build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} + -I $PROJECT_DIR/variants/seeed_wio_tracker_L1_eink + -D SEEED_WIO_TRACKER_L1 + -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/seeed_wio_tracker_L1_eink> ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} + ${nrf52840_base.lib_deps} +debug_tool = jlink diff --git a/variants/seeed_wio_tracker_L1_eink/variant.cpp b/variants/seeed_wio_tracker_L1_eink/variant.cpp new file mode 100644 index 000000000..bcbe20ea5 --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/variant.cpp @@ -0,0 +1,103 @@ +/* + * variant.cpp - Digital pin mapping for TRACKER L1 + * + * This file defines the pin mapping array that maps logical digital pins (D0-D17) + * to physical GPIO ports/pins on the Nordic nRF52 series microcontroller. + * + * Board: [Seeed Studio WIO TRACKER L1] + * Hardware Features: + * - LoRa module (CS/SCK/MISO/MOSI control pins) + * - GNSS module (TX/RX/Reset/Wakeup) + * - User LEDs (D11-D12) + * - User button (D13) + * - Grove/NFC interface (D14-D15) + * - Battery voltage monitoring (D16) + * + * Created [20250521] + * By [Dylan] + */ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +/** + * @brief Digital pin to GPIO port/pin mapping table + * + * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) + * + */ + +extern "C" { +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1.07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 40, // D5 P1.08 (A5/SCL) LORA_SW + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.1 User LED + // Buzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - Grove interface + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVESDA + 44, // D18 P0.01 GROVESCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + 4, // D30 BAT_CTL + + 13, // D31 EINK_SCK + 14, // D32 EINK_RST + 15, // D33 EINK_MOSI + 16, // D34 EINK_DC + 17, // D35 EINK_BUSY + 19, // D36 EINK_CS + +}; +} + +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); + + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); +} \ No newline at end of file diff --git a/variants/seeed_wio_tracker_L1_eink/variant.h b/variants/seeed_wio_tracker_L1_eink/variant.h new file mode 100644 index 000000000..98a7b2c39 --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/variant.h @@ -0,0 +1,194 @@ +#ifndef _SEEED_TRACKER_L1_H_ +#define _SEEED_TRACKER_L1_H_ +#include "WVariant.h" +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Clock Configuration +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#define VARIANT_MCK (64000000ul) // Master clock frequency +#define USE_LFXO // 32.768kHz crystal for LFCLK + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Pin Capacity Definitions +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#define PINS_COUNT (38u) // Total GPIO pins +#define NUM_DIGITAL_PINS (38u) // Digital I/O pins +#define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) +#define NUM_ANALOG_OUTPUTS (0u) + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// LED Configuration +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// LEDs +// LEDs +#define PIN_LED1 (11) // LED P1.15 +#define PIN_LED2 (12) // + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 +// #define LED_PIN PIN_LED2 +#define LED_STATE_ON 1 // State when LED is litted +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Button Configuration +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +#ifdef BUTTON_PIN +#undef BUTTON_PIN +#endif + +#define BUTTON_PIN D13 // This is the Program Button +// #define BUTTON_NEED_PULLUP 1 +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP false + +#define BUTTON_PIN_TOUCH 13 // Touch button +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Digital Pin Mapping (D0-D10) +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#define D0 0 // P1.06 GNSS_WAKEUP/IO0 +#define D1 1 // P0.07 LORA_DIO1 +#define D2 2 // P1.07 LORA_RESET +#define D3 3 // P1.10 LORA_BUSY +#define D4 4 // P1.14 LORA_CS +#define D5 5 // P1.08 LORA_SW +#define D6 6 // P0.27 GNSS_TX +#define D7 7 // P0.26 GNSS_RX +#define D8 8 // P0.30 SPI_SCK +#define D9 9 // P0.03 SPI_MISO +#define D10 10 // P0.28 SPI_MOSI +#define D12 12 // P1.00 Buzzer +#define D13 13 // P0.08 User Button +#define D14 14 // P0.05 OLED SCL +#define D15 15 // P0.06 OLED SDA +#define D16 16 // P0.31 VBAT_ADC +#define D17 17 // P0.00 GROVE SDA +#define D18 18 // P0.01 GROVE_SCL +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Analog Pin Definitions +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#define PIN_A0 0 // P0.02 Analog Input 0 +#define PIN_A1 1 // P0.03 Analog Input 1 +#define PIN_A2 2 // P0.28 Analog Input 2 +#define PIN_A3 3 // P0.29 Analog Input 3 +#define PIN_A4 4 // P0.04 Analog Input 4 +#define PIN_A5 5 // P0.05 Analog Input 5 +#define PIN_VBAT D16 // P0.31 Battery voltage sense +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Communication Interfaces +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// I2C Configuration +#define HAS_WIRE 1 +#define PIN_WIRE_SDA D18 // P0.09 +#define PIN_WIRE_SCL D17 // P0.10 +#define WIRE_INTERFACES_COUNT 1 + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +// SPI Configuration (SX1262) + +// #define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO 9 // P0.03 (D9) +#define PIN_SPI_MOSI 10 // P0.28 (D10) +#define PIN_SPI_SCK 8 // P0.30 (D8) + +// SX1262 LoRa Module Pins +#define USE_SX1262 +#define SX126X_CS D4 // Chip select +#define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) +#define SX126X_BUSY D3 // Busy status +#define SX126X_RESET D2 // Reset control +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage +#define SX126X_RXEN D5 // RX enable control +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// EINK +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#define SPI_INTERFACES_COUNT 2 +#define PIN_EINK_CS 36 +#define PIN_EINK_BUSY 35 +#define PIN_EINK_DC 34 +#define PIN_EINK_RES 32 +#define PIN_EINK_SCLK 31 +#define PIN_EINK_MOSI 33 +#define PIN_EINK_EN 14 // unused +#define PIN_SPI1_MISO 15 // unused +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Power Management +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +#define BAT_READ 30 // D30 = P0.04 Reads battery voltage from divider on signal board. +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define ADC_MULTIPLIER 2.0 +#define BATTERY_PIN PIN_VBAT // PIN_A7 +#define AREF_VOLTAGE 3.6 +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// GPS L76KB +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#define GPS_L76K +#ifdef GPS_L76K +#define PIN_GPS_RX D6 // P0.26 +#define PIN_GPS_TX D7 +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define GPS_RX_PIN PIN_GPS_TX +#define GPS_TX_PIN PIN_GPS_RX +#define PIN_GPS_STANDBY D0 + +// #define GPS_DEBUG +// #define GPS_EN D18 // P1.05 +#endif + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// On-board QSPI Flash +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// On-board QSPI Flash +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Buzzer +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Buzzer + +#define PIN_BUZZER D12 // P1.00, pwm output + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// joystick +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +// Compatibility Definitions +// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +#ifdef __cplusplus +extern "C" { +#endif +// Serial port placeholders + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) +#ifdef __cplusplus +} +#endif + +#endif // _SEEED_TRACKER_L1_H_ \ No newline at end of file diff --git a/variants/seeed_xiao_nrf52840_kit/platformio.ini b/variants/seeed_xiao_nrf52840_kit/platformio.ini index 0a8bee31c..0e1e94cd5 100644 --- a/variants/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/seeed_xiao_nrf52840_kit/platformio.ini @@ -1,8 +1,8 @@ -; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +; Seeed Xiao BLE: https://wiki.seeedstudio.com/XIAO_BLE/ [env:seeed_xiao_nrf52840_kit] extends = nrf52840_base board = xiao_ble_sense -build_flags = ${nrf52840_base.build_flags} -Ivariants/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT +build_flags = ${nrf52840_base.build_flags} -Ivariants/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DGPS_L76K board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/seeed_xiao_nrf52840_kit> lib_deps = diff --git a/variants/seeed_xiao_nrf52840_kit/variant.h b/variants/seeed_xiao_nrf52840_kit/variant.h index e6ef74e2e..a65500612 100644 --- a/variants/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/seeed_xiao_nrf52840_kit/variant.h @@ -1,5 +1,5 @@ -#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ -#define _SEEED_XIAO_NRF52840_SENSE_H_ +#ifndef _SEEED_XIAO_NRF52840_KIT_H_ +#define _SEEED_XIAO_NRF52840_KIT_H_ /** Master clock frequency */ #define VARIANT_MCK (64000000ul) @@ -79,9 +79,8 @@ static const uint8_t A5 = PIN_A5; */ /* - * D0 is shared with PIN_GPS_STANDBY on the L76K GNSS Module. - * There are some technical solutions that can solve this problem, and we are - * currently exploring and researching them. + * D0 is shared with PIN_GPS_STANDBY on the L76K GNSS Module, so refer to + * GPS_L76K definition preventing this conflict */ // #define BUTTON_PIN D0 @@ -93,60 +92,71 @@ static const uint8_t A5 = PIN_A5; #define PIN_SERIAL2_TX (-1) /* - * SPI Interfaces + * Pinout for SX126x */ -#define SPI_INTERFACES_COUNT 1 - -#define PIN_SPI_MISO (9) -#define PIN_SPI_MOSI (10) -#define PIN_SPI_SCK (8) - -static const uint8_t SS = D4; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - #define USE_SX1262 -// Pinout for SX126X +#ifdef XIAO_BLE_LEGACY_PINOUT +// Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 +#define SX126X_CS D0 +#define SX126X_DIO1 D1 +#define SX126X_BUSY D2 +#define SX126X_RESET D3 +#define SX126X_RXEN D7 + +#elif defined(SEEED_XIAO_WIO_BTB) +// Wio-SX1262 for XIAO with 30-pin board-to-board connector +// https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Schematic_Diagram_Wio-SX1262_for_XIAO.pdf +#define SX126X_CS D3 +#define SX126X_DIO1 D0 +#define SX126X_BUSY D1 +#define SX126X_RESET D2 +#define SX126X_RXEN D4 +#else +// Wio-SX1262 for XIAO (standalone SKU 113010003 or nRF52840 kit SKU 102010710) +// https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Wio-SX1262%20for%20XIAO%20V1.0_SCH.pdf #define SX126X_CS D4 #define SX126X_DIO1 D1 #define SX126X_BUSY D3 #define SX126X_RESET D2 +#define SX126X_RXEN D5 +#endif +// Common pinouts for all SX126x pinouts above #define SX126X_TXEN RADIOLIB_NC - -#define SX126X_RXEN D5 // This is used to control the RX side of the RF switch #define SX126X_DIO2_AS_RF_SWITCH // DIO2 is used to control the TX side of the RF switch #define SX126X_DIO3_TCXO_VOLTAGE 1.8 /* - * Wire Interfaces + * SPI Interfaces + * Defined after pinout for SX1262x to factor in CS pinout variations */ -#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much -#define WIRE_INTERFACES_COUNT 1 // 2 +#define SPI_INTERFACES_COUNT 1 -// LSM6DS3TR on XIAO nRF52840 Series -#define PIN_WIRE_SDA (17) -#define PIN_WIRE_SCL (16) +#define PIN_SPI_MISO D9 +#define PIN_SPI_MOSI D10 +#define PIN_SPI_SCK D8 -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; +static const uint8_t SS = SX126X_CS; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; /* * GPS */ -// GPS L76KB -#define GPS_L76K +// GPS L76K #ifdef GPS_L76K #define PIN_GPS_RX D6 #define PIN_GPS_TX D7 #define HAS_GPS 1 -#define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX PIN_GPS_TX #define PIN_SERIAL1_TX PIN_GPS_RX #define PIN_GPS_STANDBY D0 +#else +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) #endif /* @@ -162,6 +172,43 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define BATTERY_SENSE_RESOLUTION_BITS (10) +/* + * Wire Interfaces + * Keep this section after potentially conflicting pin definitions + */ +#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much +#define WIRE_INTERFACES_COUNT 1 + +#if defined(XIAO_BLE_LEGACY_PINOUT) +// Used for I2C by DIY xiao_ble variant +#define PIN_WIRE_SDA D4 +#define PIN_WIRE_SCL D5 +#elif !defined(GPS_L76K) +// If D6 and D7 are free, I2C is probably the most versatile assignment +#define PIN_WIRE_SDA D6 +#define PIN_WIRE_SCL D7 +#else +// Internal LSM6DS3TR on XIAO nRF52840 Series +#define PIN_WIRE_SDA (17) +#define PIN_WIRE_SCL (16) +#endif + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +/* + * Buttons + * Keep this section after potentially conflicting pin definitions + * because D0 has multiple possible conflicts with various XIAO modules: + * - PIN_GPS_STANDBY on the L76K GNSS Module + * - DIO1 on the Wio-SX1262 - 30-pin board-to-board connector version + * - SX1262X CS on XIAO BLE legacy pinout + */ + +#if !defined(GPS_L76K) && !defined(SEEED_XIAO_WIO_BTB) && !defined(XIAO_BLE_LEGACY_PINOUT) +#define BUTTON_PIN D0 +#endif + #ifdef __cplusplus } #endif diff --git a/variants/station-g1/variant.h b/variants/station-g1/variant.h index 9a3c37b73..6c3a39261 100644 --- a/variants/station-g1/variant.h +++ b/variants/station-g1/variant.h @@ -6,9 +6,7 @@ #define I2C_SDA1 14 // Second i2c channel on external IO connector #define I2C_SCL1 15 // Second i2c channel on external IO connector -#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. // common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index 14fbee6cf..c9bd64bc3 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -9,7 +9,6 @@ upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -DT_DECK -DBOARD_HAS_PSRAM - -DMAX_THREADS=40 -DGPS_POWER_TOGGLE -Ivariants/t-deck @@ -25,11 +24,6 @@ extends = env:t-deck build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_I2C_KBD_TYPE=0x55 -D INPUTDRIVER_ENCODER_TYPE=3 -D INPUTDRIVER_ENCODER_LEFT=1 @@ -39,7 +33,7 @@ build_flags = -D INPUTDRIVER_ENCODER_BTN=0 -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SDCARD - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D USE_I2S_BUZZER -D RAM_SIZE=5120 @@ -57,7 +51,10 @@ build_flags = -D RADIOLIB_DEBUG_SPI=0 -D RADIOLIB_DEBUG_PROTOCOL=0 -D RADIOLIB_SPI_PARANOID=0 - -D CALIBRATE_TOUCH=0 +; -D CALIBRATE_TOUCH=0 + -D LGFX_SCREEN_WIDTH=240 + -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_DRIVER=LGFX_TDECK -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_DECK.h\" ; -D LVGL_DRIVER=LVGL_TDECK @@ -67,7 +64,9 @@ build_flags = ; -D USE_DOUBLE_BUFFER -D USE_PACKET_API -D MAP_FULL_REDRAW + -D CUSTOM_TOUCH_DRIVER lib_deps = ${env:t-deck.lib_deps} ${device-ui_base.lib_deps} + https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h index a21c786b3..9b0de631a 100644 --- a/variants/t-deck/variant.h +++ b/variants/t-deck/variant.h @@ -1,7 +1,5 @@ #define TFT_CS 12 -#ifndef HAS_TFT // for TFT-UI the definitions are in device-ui -#define BUTTON_PIN 0 // ST7789 TFT LCD #define ST7789_CS TFT_CS @@ -24,7 +22,6 @@ #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness -#endif #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 @@ -34,10 +31,10 @@ #define USE_POWERSAVE #define SLEEP_TIME 120 -#ifndef HAS_TFT -#define BUTTON_PIN 0 -// #define BUTTON_NEED_PULLUP -#endif +#define TB_PRESS 0 +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true + #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 @@ -71,6 +68,7 @@ #define TB_LEFT 1 #define TB_RIGHT 2 #define TB_PRESS 0 // BUTTON_PIN +#define TB_DIRECTION FALLING // microphone #define ES7210_SCK 47 diff --git a/variants/t-echo/nicheGraphics.h b/variants/t-echo/nicheGraphics.h index 03185cf5b..c89d816b9 100644 --- a/variants/t-echo/nicheGraphics.h +++ b/variants/t-echo/nicheGraphics.h @@ -57,7 +57,8 @@ void setupNicheGraphics() inkhud->setDisplayResilience(20, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings diff --git a/variants/t-echo/variant.h b/variants/t-echo/variant.h index 38b7f4743..4f3a53ebf 100644 --- a/variants/t-echo/variant.h +++ b/variants/t-echo/variant.h @@ -61,8 +61,12 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular GPIO #define PIN_BUTTON_TOUCH (0 + 11) // 0.11 is the soft touch button on T-Echo +#define BUTTON_TOUCH_ACTIVE_LOW true +#define BUTTON_TOUCH_ACTIVE_PULLUP true #define BUTTON_CLICK_MS 400 #define BUTTON_TOUCH_MS 200 @@ -139,6 +143,7 @@ External serial flash WP25R1635FZUIL0 // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) @@ -214,7 +219,7 @@ External serial flash WP25R1635FZUIL0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) -#define NO_EXT_GPIO 1 +// #define NO_EXT_GPIO 1 #define HAS_RTC 1 diff --git a/variants/tbeam-s3-core/variant.h b/variants/tbeam-s3-core/variant.h index cc706459f..dabd52980 100644 --- a/variants/tbeam-s3-core/variant.h +++ b/variants/tbeam-s3-core/variant.h @@ -7,8 +7,6 @@ #define I2C_SCL 18 // For QMC6310 sensors and screens #define BUTTON_PIN 0 // The middle button GPIO on the T-Beam S3 -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. // #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. #define LED_STATE_ON 0 // State when LED is lit diff --git a/variants/tbeam/variant.h b/variants/tbeam/variant.h index 8771c20d2..5b521a2de 100644 --- a/variants/tbeam/variant.h +++ b/variants/tbeam/variant.h @@ -4,8 +4,8 @@ #define I2C_SCL 22 #define BUTTON_PIN 38 // The middle button GPIO on the T-Beam -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. #define LED_STATE_ON 0 // State when LED is lit diff --git a/variants/tlora_t3s3_epaper/nicheGraphics.h b/variants/tlora_t3s3_epaper/nicheGraphics.h index 5184037e8..8f5e63653 100644 --- a/variants/tlora_t3s3_epaper/nicheGraphics.h +++ b/variants/tlora_t3s3_epaper/nicheGraphics.h @@ -51,7 +51,8 @@ void setupNicheGraphics() inkhud->setDisplayResilience(15, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings diff --git a/variants/tlora_t3s3_epaper/platformio.ini b/variants/tlora_t3s3_epaper/platformio.ini index 957c37b95..0750b5bbb 100644 --- a/variants/tlora_t3s3_epaper/platformio.ini +++ b/variants/tlora_t3s3_epaper/platformio.ini @@ -31,7 +31,6 @@ build_flags = ${inkhud.build_flags} -I variants/tlora_t3s3_epaper -D TLORA_T3S3_EPAPER - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} \ No newline at end of file diff --git a/variants/tracksenger/internal/variant.h b/variants/tracksenger/internal/variant.h index 57ead848d..6f75ad0e2 100644 --- a/variants/tracksenger/internal/variant.h +++ b/variants/tracksenger/internal/variant.h @@ -72,7 +72,7 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes diff --git a/variants/tracksenger/lcd/variant.h b/variants/tracksenger/lcd/variant.h index ecf4e854e..843bf3924 100644 --- a/variants/tracksenger/lcd/variant.h +++ b/variants/tracksenger/lcd/variant.h @@ -96,7 +96,7 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes diff --git a/variants/tracksenger/oled/variant.h b/variants/tracksenger/oled/variant.h index 70f0f3209..85cc019c4 100644 --- a/variants/tracksenger/oled/variant.h +++ b/variants/tracksenger/oled/variant.h @@ -74,7 +74,7 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) +#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini index 399d65b03..b9da6d0e5 100644 --- a/variants/unphone/platformio.ini +++ b/variants/unphone/platformio.ini @@ -36,21 +36,16 @@ extends = env:unphone build_flags = ${env:unphone.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_BUTTON_TYPE=21 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D HAS_SDCARD -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=6144 -D LV_CACHE_DEF_SIZE=2097152 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_BUILD_TEST=0 -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 @@ -59,6 +54,9 @@ build_flags = -D LV_USE_LOG=0 -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D LGFX_SCREEN_WIDTH=320 + -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_DRIVER=LGFX_UNPHONE_V9 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_UNPHONE.h\" -D VIEW_320x240 diff --git a/variants/unphone/variant.h b/variants/unphone/variant.h index eaf142721..e186b5740 100644 --- a/variants/unphone/variant.h +++ b/variants/unphone/variant.h @@ -57,9 +57,12 @@ #define LED_PIN 13 // the red part of the RGB LED #define LED_STATE_ON 0 // State when LED is lit -#define BUTTON_PIN 21 // Button 3 - square - top button in landscape mode -#define BUTTON_NEED_PULLUP // we do need a helping hand up -#define BUTTON_PIN_ALT 45 // Button 1 - triangle - bottom button in landscape mode +#define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode +#define BUTTON_PIN 0 // Circle button +#define BUTTON_NEED_PULLUP // we do need a helping hand up +#define CANCEL_BUTTON_PIN 45 // Button 1 - triangle - bottom button in landscape mode +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP true #define I2C_SDA 3 // I2C pins for this board #define I2C_SCL 4 diff --git a/variants/wio-e5/platformio.ini b/variants/wio-e5/platformio.ini index e746ae2f0..1ef7abd78 100644 --- a/variants/wio-e5/platformio.ini +++ b/variants/wio-e5/platformio.ini @@ -8,20 +8,17 @@ build_flags = -DSERIAL_UART_INSTANCE=1 -DPIN_SERIAL_RX=PB7 -DPIN_SERIAL_TX=PB6 + -DPIN_WIRE_SDA=PA15 + -DPIN_WIRE_SCL=PB15 -DHAL_DAC_MODULE_ONLY -DHAL_RNG_MODULE_ENABLED -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 - -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -DMESHTASTIC_EXCLUDE_I2C=1 - -DMESHTASTIC_EXCLUDE_WIFI=1 - -DMESHTASTIC_EXCLUDE_BLUETOOTH=1 - -DMESHTASTIC_EXCLUDE_GPS=1 - -DMESHTASTIC_EXCLUDE_SCREEN=1 - -DMESHTASTIC_EXCLUDE_MQTT=1 - -DMESHTASTIC_EXCLUDE_POWERMON=1 - ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF - ;-DCFG_DEBUG + -DHAS_SENSOR -upload_port = stlink \ No newline at end of file +upload_port = stlink + +lib_deps = + ${stm32_base.lib_deps} + # Add your custom sensor here! \ No newline at end of file diff --git a/variants/xiao_ble/README.md b/variants/xiao_ble/README.md deleted file mode 100644 index 2a08138ba..000000000 --- a/variants/xiao_ble/README.md +++ /dev/null @@ -1,264 +0,0 @@ -# - -

- Xiao BLE/BLE Sense + Ebyte E22-900M30S -

- -

- A step-by-step guide for macOS and Linux -

- -## Introduction - -This guide will walk you through everything needed to get the Xiao BLE (or BLE Sense) running Meshtastic using an Ebyte E22-900M30S LoRa module. The combination of the E22 with an nRF52840 MCU is desirable because it allows for both very low idle (Rx) power draw and high transmit power. The Xiao BLE is a small but surprisingly well-appointed nRF52840 board, with enough GPIO for most Meshtastic applications and a built-in LiPo charger. The E22, on the other hand, is a famously inscrutable and mysterious beast. It is one of the more readily available LoRa modules capable of transmitting at 30 dBm, and includes an LNA to boost its Rx sensitivity a few dB beyond that of the SX1262. However, its documentation is relatively sparse overall, and seems to merely hint at (or completely omit) several key details regarding its functionality. Thus, much of what follows is a synthesis of my observations and inferences over the course of many hours of trial and error. - -

Acknowledgement and friendly disclaimer

- -Huge thanks to those in the community who have forged the way with the E22, without whose hard work none of this would have been possible! (thebentern, riddick, rainer_vie, beegee-tokyo, geeksville, caveman99, Der_Bear, PlumRugOfDoom, BigCorvus, and many others.) - -
- -Please take the conclusions here as a tentative work in progress, representing my current (and fairly limited) understanding of the E22 when paired with this particular MCU. It is my hope that this guide will be helpful to others who are interested in trying a DIY Meshtastic build, and also be subject to revision by folks with more experience and better test equipment. - -### Obligatory liability disclaimer - -This guide and all associated content is for informational purposes only. The information presented is intended for consumption only by persons having appropriate technical skill and judgement, to be used entirely at their own discretion and risk. The authors of this guide in no way provide any warranty, express or implied, toward the content herein, nor its correctness, safety, or suitability to any particular purpose. By following the instructions in this guide in part or in full, you assume all responsibility for all potential risks, including but not limited to fire, property damage, bodily injury, and death. - -### Note - -These instructions assume you are running macOS or Linux, but it should be relatively easy to translate each command for Windows. (In this case, in step 2 below, each line of `xiao_ble.sh` would also need to be converted to the equivalent Windows CLI command and run individually.) - -## 1. Update Bootloader - -The first thing you will need to do is update the Xiao BLE's bootloader. The stock bootloader is functionally very similar to the Adafruit nRF52 UF2 bootloader, but apparently not quite enough so to work with Meshtastic out of the box. - -1. Connect the Xiao BLE to your computer via USB-C. - -2. Install `adafruit-nrfutil` by following the instructions here. - -3. Open a terminal window and navigate to `firmware/variants/xiao_ble` (where `firmware` is the directory into which you have cloned the Meshtastic firmware repo). - -4. Run the following command, replacing `/dev/cu.usbmodem2101` with the serial port your Xiao BLE is connected to: - - ```bash - adafruit-nrfutil --verbose dfu serial --package xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip --port /dev/cu.usbmodem2101 -b 115200 --singlebank --touch 1200 - ``` - -5. If all goes well, the Xiao BLE's red LED should start to pulse slowly, and you should see a new USB storage device called `XIAO-BOOT` appear under `Locations` in Finder. - -  - -## 2. PlatformIO Environment Preparation - -Before building Meshtastic for the Xiao BLE + E22, it is necessary to pull in SoftDevice 7.3.0 and its associated linker script (nrf52840_s140_v7.ld) from Seeed Studio's Arduino core. The `xiao_ble.sh` script does this. - -1. In your terminal window, run the following command: - - ```bash - sudo ./xiao_ble.sh - ``` - -  - -## 3. Build Meshtastic - -At this point, you should be able to build the firmware successfully. - -1. In VS Code, press `Command Shift P` to bring up the command palette. - -2. Search for and run the `Developer: Reload Window` command. - -3. Bring up the command palette again with `Command Shift P`. Search for and run the `PlatformIO: Pick Project Environment` command. - -4. In the list of environments, select `env:xiao_ble`. PlatformIO may update itself for a minute or two, and should let you know once done. - -5. Return to the command palette once again (`Command Shift P`). Search for and run the `PlatformIO: Build` command. - -6. PlatformIO will build the project. After a few minutes you should see a green `SUCCESS` message. - -  - -## 4. Wire the board - -Connecting the E22 to the Xiao BLE is straightforward, but there are a few gotchas to be mindful of. - -- On the Xiao BLE: - - - Pins D4 and D5 are currently mapped to `PIN_WIRE_SDA` and `PIN_WIRE_SCL`, respectively. If you are not using IยฒC and would like to free up pins D4 and D5 for use as GPIO, `PIN_WIRE_SDA` and `PIN_WIRE_SCL` can be reassigned to any two other unused pin numbers. - - - Pins D6 and D7 were originally mapped to the TX and RX pins for serial interface 1 (`PIN_SERIAL1_RX` and `PIN_SERIAL1_TX`) but are currently set to -1 in `variant.h`. If you need to expose a serial interface, you can restore these pins and move e.g. `SX126X_RXEN` to pin 4 or 5 (the opposite should work too). - -- On the E22: - - - There are two options for the E22's `TXEN` pin: - - 1. It can be connected to the MCU on the pin defined as `SX126X_TXEN` in `variant.h`. In this configuration, the MCU will control Tx/Rx switching "manually". As long as `SX126X_TXEN` and `SX126X_RXEN` are both defined in `variant.h` (and neither is set to `RADIOLIB_NC`), `SX126xInterface.cpp` will initialize the E22 correctly for this mode. - - 2. Alternately, it can be connected to the E22's `DIO2` pin only, with neither `TXEN` nor `DIO2` being connected to the MCU. In this configuration, the E22 will control Tx/Rx switching automatically. In `variant.h`, as long as `SX126X_TXEN` is defined as `RADIOLIB_NC`, and `SX126X_RXEN` is defined and connected to the E22's `RXEN` pin, and `E22_TXEN_CONNECTED_TO_DIO2` is defined, `SX126xInterface.cpp` will initialize the E22 correctly for this mode. This configuration frees up a GPIO, and presents no drawbacks that I have found. - - - Note that any combination other than the two described above will likely result in unexpected behavior. In my testing, some of these other configurations appeared to "work" at first glance, but every one I tried had at least one of the following flaws: weak Tx power, extremely poor Rx sensitivity, or the E22 overheating because TXEN was never pulled low, causing its PA to stay on indefinitely. - - - Along the same lines, it is a good idea to check the E22's temperature frequently by lightly touching the shield. If you feel the shield getting hot (i.e. approaching uncomfortable to touch) near pins 1, 2, and 3, something is probably misconfigured; disconnect both the Xiao BLE and E22 from power and double check wiring and pin mapping. - - - Whether you opt to let the E22 control Rx and Tx or handle this manually, the E22's `RXEN` pin must always be connected to the MCU on the pin defined as `SX126X_RXEN` in `variant.h`. - -

Note

- -The default pin mapping in `variant.h` uses 'automatic Tx/Rx switching' mode. If you wire your board for manual Rx/Tx switching, make sure to update `variant.h` accordingly by commenting/uncommenting the necessary lines in the 'E22 Tx/Rx control options' section. - -  - ---- - -  - -

Example wiring for "E22 automatic Tx/Rx switching" mode:

-  - -MCU -> E22 connections - -| Xiao BLE pin | variant.h definition | E22 pin | Notes | -| :----------- | :------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------- | -| D0 | SX126X_CS | 19 (NSS) | | -| D1 | SX126X_DIO1 | 13 (DIO1) | | -| D2 | SX126X_BUSY | 14 (BUSY) | | -| D3 | SX126X_RESET | 15 (NRST) | | -| D7 | SX126X_RXEN | 6 (RXEN) | These pins must still be connected, and `SX126X_RXEN` defined in `variant.h`, otherwise Rx sensitivity will be poor. | -| D8 | PIN_SPI_SCK | 18 (SCK) | | -| D9 | PIN_SPI_MISO | 16 (MISO) | | -| D10 | PIN_SPI_MOSI | 17 (MOSI) | | - -  -  - -E22 -> E22 connections: - -| E22 pin | E22 pin | Notes | -| :------ | :------ | :------------------------------------------------------------------------ | -| TXEN | DIO2 | These must be physically connected for automatic Tx/Rx switching to work. | - -

Note

- -The schematic (`xiao-ble-e22-schematic.png`) in the `eagle-project` directory uses this wiring. - -  - ---- - -  - -

Example wiring for "Manual Tx/Rx switching" mode:

- -MCU -> E22 connections - -| Xiao BLE pin | variant.h definition | E22 pin | Notes | -| :----------- | :------------------- | :-------- | :---- | -| D0 | SX126X_CS | 19 (NSS) | | -| D1 | SX126X_DIO1 | 13 (DIO1) | | -| D2 | SX126X_BUSY | 14 (BUSY) | | -| D3 | SX126X_RESET | 15 (NRST) | | -| D6 | SX126X_TXEN | 7 (TXEN) | | -| D7 | SX126X_RXEN | 6 (RXEN) | | -| D8 | PIN_SPI_SCK | 18 (SCK) | | -| D9 | PIN_SPI_MISO | 16 (MISO) | | -| D10 | PIN_SPI_MOSI | 17 (MOSI) | | - -E22 -> E22 connections: (none) - -  - -## 5. Flash the firmware to the Xiao BLE - -1. Double press the Xiao's `reset` button to put it in bootloader mode. -2. In a terminal window, navigate to the Meshtastic firmware repo's root directory, and from there to `.pio/build/xiao_ble`. -3. Convert the generated `.hex` file into a `.uf2` file: - - ```bash - ../../../bin/uf2conv.py firmware.hex -c -o firmware.uf2 -f 0xADA52840 - ``` - -4. Copy the new `.uf2` file to the Xiao's mass storage volume: - - ```bash - cp firmware.uf2 /Volumes/XIAO-BOOT - ``` - -5. The Xiao's red LED will flash for several seconds as the firmware is copied. -6. Once the firmware is copied, to verify it is running, run the following command: - - ```bash - meshtastic --noproto - ``` - -7. Then, press the Xiao's `reset` button again. You should see a lot of debug output logged in the terminal window. - -  - -## 6. Troubleshooting - -- If after flashing Meshtastic, the Xiao is bootlooped, look at the serial output (you can see this by running `meshtastic --noproto` with the device connected to your computer via USB). - - - If you see that the SX1262 init result was -2, this likely indicates a wiring problem; double check your wiring and pin mapping in `variant.h`. - - - If you see an error mentioning tinyFS, this may mean you need to reformat the Xiao's storage: - - 1. Double press the `reset` button to put the Xiao in bootloader mode. - - 2. In a terminal window, navigate to the Meshtastic firmware repo's root directory, and from there to `variants/xiao_ble`. - - 3. Run the following command:  `cp xiao-ble-internal-format.uf2 /Volumes/XIAO-BOOT` - - 4. The Xiao's red LED will flash briefly as the filesystem format firmware is copied. - - 5. Run the following command:  `meshtastic --noproto` - - 6. In the output of the above command, you should see a message saying "Formatting...done". - - 7. To flash Meshtastic again, repeat the steps in section 5 above. - - - If you don't see any specific error message, but the boot process is stuck or not proceeding as expected, this might also mean there is a conflict in `variant.h`. If you have made any changes to the pin mapping, ensure they do not result in a conflict. If all else fails, try reverting your changes and using the known-good configuration included here. - - - The above might also mean something is wired incorrectly. Try reverting to one of the known-good example wirings in section 4. - -- If the E22 gets hot to the touch: - - The power amplifier is likely running continually. Disconnect it and the Xiao from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertenly high (usually due to a pin mapping conflict). - -  - -## 7. Notes - -- There are several anecdotal recommendations regarding the Tx power the E22's internal SX1262 should be set to in order to achieve the advertised output of 30 dBm, ranging from 4 (per this article in the RadioLib github repo) to 22 (per this conversation from the Meshtastic Discord). When paired with the Xiao BLE in the configurations described above, I observed that the output is at its maximum when Tx power is set to 22. - -- To achieve its full output, the E22 should have a bypass capacitor from its 5V supply to ground. 100 ยตF works well. - -- The E22 will happily run on voltages lower than 5V, but the full output power will not be realized. For example, with a fully charged LiPo at 4.2V, Tx power appears to max out around 26-27 dBm. - -  - -## 8. Testing Methodology - -During what became a fairly long trial-and-error process, I did a lot of careful testing of Tx power and Rx sensitivity. My methodology in these tests was as follows: - -- All tests were conducted between two nodes: - - 1. The Xiao BLE + E22 coupled with an Abracon ARRKP4065-S915A ceramic patch antenna - - 2. A RAK 5005/4631 coupled with a Laird MA9-5N antenna via a 4" U.FL to Type N pigtail. - - - No other nodes were powered up onsite or nearby. - -
- -- Each node and its antenna was kept in exactly the same position and orientation throughout testing. - -- Other environmental factors (e.g. the location and resting position of my body in the room while testing) were controlled as carefully as possible. - -- Each test comprised at least five (and often ten) runs, after which the results were averaged. - -- All testing was done by sending single-character messages between nodes and observing the received RSSI reported in the message acknowledgement. Messages were sent one by one, waiting for each to be acknowledged or time out before sending the next. - -- The E22's Tx power was observed by sending messages from the RAK to the Xiao BLE + E22 and recording the received RSSI. - -- The opposite was done to observe the E22's Rx sensitivity: messages were sent from the Xiao BLE + E22 to the RAK, and the received RSSI was recorded. - -While this cannot match the level of accuracy achievable with actual test equipment in a lab setting, it was nonetheless sufficient to demonstrate the (sometimes very large) differences in Tx power and Rx sensitivity between various configurations. diff --git a/variants/xiao_ble/platformio.ini b/variants/xiao_ble/platformio.ini deleted file mode 100644 index 6fa1dd611..000000000 --- a/variants/xiao_ble/platformio.ini +++ /dev/null @@ -1,13 +0,0 @@ -; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 -[env:xiao_ble] -extends = nrf52840_base -board = xiao_ble_sense -board_level = extra -build_flags = ${nrf52840_base.build_flags} -Ivariants/xiao_ble -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -D EBYTE_E22 -DEBYTE_E22_900M30S -DPRIVATE_HW -board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/xiao_ble> -lib_deps = - ${nrf52840_base.lib_deps} -debug_tool = jlink -; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) -;upload_protocol = jlink diff --git a/variants/xiao_ble/variant.cpp b/variants/xiao_ble/variant.cpp deleted file mode 100644 index 300f69d0b..000000000 --- a/variants/xiao_ble/variant.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // D0 .. D13 - 2, // D0 is P0.02 (A0) - 3, // D1 is P0.03 (A1) - 28, // D2 is P0.28 (A2) - 29, // D3 is P0.29 (A3) - 4, // D4 is P0.04 (A4,SDA) - 5, // D5 is P0.05 (A5,SCL) - 43, // D6 is P1.11 (TX) - 44, // D7 is P1.12 (RX) - 45, // D8 is P1.13 (SCK) - 46, // D9 is P1.14 (MISO) - 47, // D10 is P1.15 (MOSI) - - // LEDs - 26, // D11 is P0.26 (LED RED) - 6, // D12 is P0.06 (LED BLUE) - 30, // D13 is P0.30 (LED GREEN) - 14, // D14 is P0.14 (READ_BAT) - - // LSM6DS3TR - 40, // D15 is P1.08 (6D_PWR) - 27, // D16 is P0.27 (6D_I2C_SCL) - 7, // D17 is P0.07 (6D_I2C_SDA) - 11, // D18 is P0.11 (6D_INT1) - - // MIC - 42, // 17,//42, // D19 is P1.10 (MIC_PWR) - 32, // 26,//32, // D20 is P1.00 (PDM_CLK) - 16, // 25,//16, // D21 is P0.16 (PDM_DATA) - - // BQ25100 - 13, // D22 is P0.13 (HICHG) - 17, // D23 is P0.17 (~CHG) - - // - 21, // D24 is P0.21 (QSPI_SCK) - 25, // D25 is P0.25 (QSPI_CSN) - 20, // D26 is P0.20 (QSPI_SIO_0 DI) - 24, // D27 is P0.24 (QSPI_SIO_1 DO) - 22, // D28 is P0.22 (QSPI_SIO_2 WP) - 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) - - // NFC - 9, // D30 is P0.09 (NFC1) - 10, // D31 is P0.10 (NFC2) - - // VBAT - 31, // D32 is P0.10 (VBAT) -}; - -void initVariant() -{ - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); -} diff --git a/variants/xiao_ble/variant.h b/variants/xiao_ble/variant.h deleted file mode 100644 index e511c6869..000000000 --- a/variants/xiao_ble/variant.h +++ /dev/null @@ -1,212 +0,0 @@ -#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ -#define _SEEED_XIAO_NRF52840_SENSE_H_ - -/** Master clock frequency */ -#define VARIANT_MCK (64000000ul) - -#define USE_LFXO // Board uses 32khz crystal for LF -// #define USE_LFRC // Board uses RC for LF - -/*---------------------------------------------------------------------------- - * Headers - *----------------------------------------------------------------------------*/ - -#include "WVariant.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -#define PINS_COUNT (33) -#define NUM_DIGITAL_PINS (33) -#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference -#define NUM_ANALOG_OUTPUTS (0) - -// LEDs - -#define LED_RED 11 -#define LED_BLUE 12 -#define LED_GREEN 13 - -#define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE -#define PIN_LED3 LED_RED - -#define PIN_LED PIN_LED1 -#define LED_PWR (PINS_COUNT) - -#define LED_BUILTIN PIN_LED - -#define LED_STATE_ON 1 // State when LED is lit - -/* - * Buttons - */ -#define PIN_BUTTON1 (PINS_COUNT) - -// Digital PINs -#define D0 (0ul) -#define D1 (1ul) -#define D2 (2ul) -#define D3 (3ul) -#define D4 (4ul) -#define D5 (5ul) -#define D6 (6ul) -#define D7 (7ul) -#define D8 (8ul) -#define D9 (9ul) -#define D10 (10ul) - -/* - * Analog pins - */ -#define PIN_A0 (0) -#define PIN_A1 (1) -#define PIN_A2 (2) -#define PIN_A3 (3) -#define PIN_A4 (4) -#define PIN_A5 (5) -#define PIN_VBAT (32) -#define VBAT_ENABLE (14) - -static const uint8_t A0 = PIN_A0; -static const uint8_t A1 = PIN_A1; -static const uint8_t A2 = PIN_A2; -static const uint8_t A3 = PIN_A3; -static const uint8_t A4 = PIN_A4; -static const uint8_t A5 = PIN_A5; -#define ADC_RESOLUTION 12 - -// Other pins -#define PIN_NFC1 (30) -#define PIN_NFC2 (31) - -/* - * Serial interfaces - */ -#define PIN_SERIAL1_RX (-1) // (7) -#define PIN_SERIAL1_TX (-1) // (6) - -#define PIN_SERIAL2_RX (-1) -#define PIN_SERIAL2_TX (-1) - -/* - * SPI Interfaces - */ -#define SPI_INTERFACES_COUNT 1 - -#define PIN_SPI_MISO (9) -#define PIN_SPI_MOSI (10) -#define PIN_SPI_SCK (8) - -static const uint8_t SS = D0; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -// supported modules list -#define USE_SX1262 - -// common pinouts for SX126X modules -#define SX126X_CS D0 -#define SX126X_DIO1 D1 -#define SX126X_BUSY D2 -#define SX126X_RESET D3 - -// ---------------------------------------------------------------- - -// E22 Tx/Rx control options: - -// 1. Let the E22 control Tx and Rx automagically via DIO2. - -// * The E22's TXEN and DIO2 pins are connected to each other, but not to the MCU. -// * The E22's RXEN pin *is* connected to the MCU. -// * E22_TXEN_CONNECTED_TO_DIO2 is defined so the logic in SX126XInterface.cpp handles this configuration correctly. - -#define SX126X_TXEN RADIOLIB_NC -#define SX126X_RXEN D7 - -// ------------------------------ OR ------------------------------ - -// 2. Control Tx and Rx manually. - -// * The E22's TXEN and RXEN pins are both connected to the MCU. - -// #define SX126X_TXEN D6 -// #define SX126X_RXEN D7 - -// ---------------------------------------------------------------- - -#ifdef EBYTE_E22 -// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch -// (which is the default for the sx1262interface code) -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#ifdef EBYTE_E22_900M30S -// 10dB PA gain and 30dB rated output; based on measurements from -// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/E22-900M30S%20power%20output%20testing.txt -#define TX_GAIN_LORA 7 -#define SX126X_MAX_POWER 22 -#endif -#ifdef EBYTE_E22_900M33S -// 25dB PA gain and 33dB rated output; based on TX Power Curve from E22-900M33S_UserManual_EN_v1.0.pdf -#define TX_GAIN_LORA 25 -#define SX126X_MAX_POWER 8 -#endif -#endif - -/* - * Wire Interfaces - */ -#define WIRE_INTERFACES_COUNT 1 // 2 - -#define PIN_WIRE_SDA (4) -#define PIN_WIRE_SCL (5) - -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; - -#define PIN_LSM6DS3TR_C_POWER (15) -#define PIN_LSM6DS3TR_C_INT1 (18) - -// PDM Interfaces -// --------------- -#define PIN_PDM_PWR (19) -#define PIN_PDM_CLK (20) -#define PIN_PDM_DIN (21) - -// QSPI Pins -#define PIN_QSPI_SCK (24) -#define PIN_QSPI_CS (25) -#define PIN_QSPI_IO0 (26) -#define PIN_QSPI_IO1 (27) -#define PIN_QSPI_IO2 (28) -#define PIN_QSPI_IO3 (29) - -// On-board QSPI Flash -#define EXTERNAL_FLASH_DEVICES P25Q16H -#define EXTERNAL_FLASH_USE_QSPI - -// Battery - -#define ADC_CTRL VBAT_ENABLE // P0.14: VBAT voltage divider -#define ADC_CTRL_ENABLED LOW // ... sink -#define BATTERY_SENSE_RESOLUTION_BITS 10 -#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED -#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge - -// The battery sense is hooked to pin A0 (5) -#define BATTERY_PIN PIN_VBAT // PIN_A0 - -// ratio of voltage divider = 3.0 (R17=1M, R18=510k) -#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic - -#ifdef __cplusplus -} -#endif - -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ - -#endif diff --git a/variants/xiao_ble/xiao-ble-internal-format.uf2 b/variants/xiao_ble/xiao-ble-internal-format.uf2 deleted file mode 100644 index 59de2c68a..000000000 Binary files a/variants/xiao_ble/xiao-ble-internal-format.uf2 and /dev/null differ diff --git a/variants/xiao_ble/xiao_ble.sh b/variants/xiao_ble/xiao_ble.sh deleted file mode 100755 index 2f3cc5390..000000000 --- a/variants/xiao_ble/xiao_ble.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# adapted from the script linked in this very helpful article: https://enzolombardi.net/low-power-bluetooth-advertising-with-xiao-ble-and-platformio-e8e7d0da80d2 - -# source: https://gist.githubusercontent.com/turing-complete-labs/b3105ee653782183c54b4fdbe18f411f/raw/d86779ba7702775d3b79781da63d85442acd9de6/xiao_ble.sh -# download the core for arduino from seeedstudio. Softdevice 7.3.0, linker and variants folder are what we need -curl https://files.seeedstudio.com/arduino/core/nRF52840/Arduino_core_nRF52840.tar.bz2 -o arduino.core.1.0.0.tar.bz2 -tar -xjf arduino.core.1.0.0.tar.bz2 -rm arduino.core.1.0.0.tar.bz2 - -# copy the needed files -cp 1.0.0/cores/nRF5/linker/nrf52840_s140_v7.ld ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/linker -cp -r 1.0.0/cores/nRF5/nordic/softdevice/s140_nrf52_7.3.0_API ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/nordic/softdevice - -rm -rf 1.0.0 -echo done! diff --git a/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip b/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip deleted file mode 100644 index 40b966baf..000000000 Binary files a/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip and /dev/null differ diff --git a/version.properties b/version.properties index e13094769..5de810523 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 -minor = 6 -build = 11 +minor = 7 +build = 3