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 2f0883fad..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,22 +43,29 @@ runs: id: base uses: ./.github/actions/setup-base - - 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/v2.5.3 + # - 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: Unpack web ui - if: inputs.include-web-ui == 'true' - shell: bash - run: | - tar -xf build.tar -C data/static - rm build.tar + # - 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: Remove debug flags for release shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index b14290be2..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,29 +0,0 @@ -#trunk-ignore-all(yamllint/quoted-strings): required by dependabot syntax check -version: 2 -updates: - - package-ecosystem: docker - directory: /.devcontainer - schedule: - interval: daily - time: "05:00" - timezone: US/Pacific - - package-ecosystem: docker - directory: / - schedule: - interval: daily - time: "05:00" - timezone: US/Pacific - - package-ecosystem: gitsubmodule - directory: / - schedule: - interval: daily - time: "05:00" - timezone: US/Pacific - ignore: - - dependency-name: protobufs - - package-ecosystem: github-actions - directory: /.github/workflows - schedule: - interval: daily - time: "05:00" - timezone: US/Pacific 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/docker_build.yml b/.github/workflows/docker_build.yml index eec0785c0..cde7fd274 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -26,6 +26,11 @@ on: required: false type: boolean default: false + pio_env: + description: PlatformIO environment to build + required: false + type: string + default: native outputs: digest: description: Digest of built image @@ -90,3 +95,5 @@ jobs: push: ${{ inputs.push }} tags: ${{ steps.meta.outputs.tags }} # Tag is only meant to be consumed by the "manifest" job platforms: ${{ inputs.platform }} + build-args: | + PIO_ENV=${{ inputs.pio_env }} diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 5b11926f2..a676efa1e 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -5,14 +5,20 @@ concurrency: on: # # Triggers the workflow on push but only for the master branch push: - branches: [master, develop] + branches: + - master + - develop + - event/* paths-ignore: - "**.md" - version.properties # Note: This is different from "pull_request". Need to specify ref when doing checkouts. pull_request_target: - branches: [master, develop] + branches: + - master + - develop + - event/* paths-ignore: - "**.md" #- "**.yml" @@ -32,12 +38,12 @@ jobs: name: Checkout base - id: jsonStep run: | - if [[ "${{ github.head_ref }}" == "" ]]; then + if [[ "$GITHUB_HEAD_REF" == "" ]]; then TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) else TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} quick) fi - echo "Name: ${{ github.ref_name }} Base: ${{ github.base_ref }} } Ref: ${{ github.ref }} Targets: $TARGETS" + echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS" echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT outputs: esp32: ${{ steps.jsonStep.outputs.esp32 }} @@ -129,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 @@ -143,9 +150,10 @@ jobs: secrets: inherit test-native: + if: ${{ !contains(github.ref_name, 'event/') }} uses: ./.github/workflows/test_native.yml - docker-debian-amd64: + docker-deb-amd64: uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -153,7 +161,16 @@ jobs: runs-on: ubuntu-24.04 push: false - docker-alpine-amd64: + docker-deb-amd64-tft: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-alp-amd64: uses: ./.github/workflows/docker_build.yml with: distro: alpine @@ -161,7 +178,16 @@ jobs: runs-on: ubuntu-24.04 push: false - docker-debian-arm64: + docker-alp-amd64-tft: + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-deb-arm64: uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -169,7 +195,7 @@ jobs: runs-on: ubuntu-24.04-arm push: false - docker-debian-armv7: + docker-deb-armv7: uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -177,17 +203,6 @@ jobs: runs-on: ubuntu-24.04-arm push: false - after-checks: - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_dispatch' }} - needs: [check] - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - gather-artifacts: permissions: contents: write @@ -243,7 +258,6 @@ jobs: ./device-*.sh ./device-*.bat ./littlefs-*.bin - ./littlefswebui-*.bin ./bleota*bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 @@ -332,7 +346,7 @@ jobs: merge-multiple: true path: ./output/pio-deps-native-tft - - name: Zip linux sources + - name: Zip Linux sources working-directory: output run: | zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src @@ -342,7 +356,9 @@ jobs: - name: Display structure of downloaded files run: ls -lR - - name: Add linux sources to release + - name: Add Linux sources to GtiHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} run: | gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip @@ -400,9 +416,53 @@ jobs: - name: Display structure of downloaded files run: ls -lR - - name: Add bins and debug elfs to release + - name: Add bins and debug elfs to GitHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} run: | gh release upload v${{ steps.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip gh release upload v${{ steps.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-firmware: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-firmware] + env: + targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Get release version string + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-{${{ env.targets }}}-${{ steps.version.outputs.long }} + merge-multiple: true + path: ./publish + + - name: Publish firmware to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + env: + # On event/* branches, use the event name as the destination prefix + DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ steps.version.outputs.long }} + keep_files: true + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: ${{ steps.version.outputs.long }} + enable_jekyll: true 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 710e8e51d..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: |- @@ -46,11 +46,14 @@ jobs: # Create a PR to bump version when a release is Published bump-version: - if: ${{ github.event.release.published }} + if: github.event.action == 'published' runs-on: ubuntu-latest permissions: pull-requests: write contents: write + defaults: + run: + shell: bash steps: - name: Checkout uses: actions/checkout@v4 @@ -60,32 +63,45 @@ jobs: with: python-version: 3.x - - name: Get release version string - run: | - echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT - id: version - env: - BUILD_LOCATION: local - - name: Bump version.properties - run: >- - bin/bump_version.py + run: | + # Bump version.properties + chmod +x ./bin/bump_version.py + ./bin/bump_version.py + + - name: Get new release version string + run: | + echo "short=$(./bin/buildinfo.py short)" >> $GITHUB_OUTPUT + id: new_version - name: Ensure debian deps are installed - shell: bash run: | sudo apt-get update -y --fix-missing sudo apt-get install -y devscripts - name: Update debian changelog - run: >- - debian/ci_changelog.sh + run: | + # Update debian changelog + chmod +x ./debian/ci_changelog.sh + ./debian/ci_changelog.sh - - name: Create version.properties pull request + - name: Bump org.meshtastic.meshtasticd.metainfo.xml + run: | + # Bump org.meshtastic.meshtasticd.metainfo.xml + pip install -r bin/bump_metainfo/requirements.txt -q + chmod +x ./bin/bump_metainfo/bump_metainfo.py + ./bin/bump_metainfo/bump_metainfo.py --file bin/org.meshtastic.meshtasticd.metainfo.xml "${{ steps.new_version.outputs.short }}" + env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + + - name: Create Bumps pull request uses: peter-evans/create-pull-request@v7 with: - title: Bump version.properties + base: ${{ github.event.repository.default_branch }} + branch: create-pull-request/bump-version + title: Bump release version + commit-message: automated bumps add-paths: | version.properties debian/changelog + bin/org.meshtastic.meshtasticd.metainfo.xml diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index db308c9f5..e391aa07b 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -13,7 +13,8 @@ permissions: jobs: semgrep-full: - runs-on: ubuntu-22.04 + if: github.repository == 'meshtastic/firmware' + runs-on: ubuntu-24.04 container: image: semgrep/semgrep diff --git a/.github/workflows/sec_sast_semgrep_pull.yml b/.github/workflows/sec_sast_semgrep_pull.yml index 527a5c076..3707c91b8 100644 --- a/.github/workflows/sec_sast_semgrep_pull.yml +++ b/.github/workflows/sec_sast_semgrep_pull.yml @@ -6,7 +6,7 @@ permissions: read-all jobs: semgrep-diff: - runs-on: ubuntu-22.04 + 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 c3643dcbd..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.0.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 0f0ee0af4..34b28b39c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,13 +5,18 @@ on: - cron: 0 0 * * * # Run every day at midnight workflow_dispatch: {} -permissions: read-all +permissions: + contents: read + actions: read + checks: write 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 @@ -44,7 +49,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - name: Setup pnpm uses: pnpm/action-setup@v4 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/configs/.prettierignore b/.trunk/configs/.prettierignore new file mode 100644 index 000000000..a63a557a4 --- /dev/null +++ b/.trunk/configs/.prettierignore @@ -0,0 +1 @@ +renovate.json diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 8f938ce9e..f0271c856 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,34 +1,34 @@ version: 0.1 cli: - version: 1.22.11 + version: 1.24.0 plugins: sources: - id: trunk - ref: v1.6.7 + ref: v1.7.1 uri: https://github.com/trunk-io/plugins lint: enabled: - - prettier@3.5.3 - - trufflehog@3.88.18 - - yamllint@1.37.0 - - bandit@1.8.3 - - checkov@3.2.394 - - terrascan@1.19.9 - - trivy@0.60.0 + - checkov@3.2.450 + - renovate@41.29.1 + - prettier@3.6.2 + - trufflehog@3.89.2 + - yamllint@1.37.1 + - bandit@1.8.6 + - trivy@0.64.1 - taplo@0.9.3 - - ruff@0.11.2 + - ruff@0.12.2 - isort@6.0.1 - - markdownlint@0.44.0 - - oxipng@9.1.4 - - svgo@3.3.2 + - markdownlint@0.45.0 + - oxipng@9.1.5 + - svgo@4.0.0 - actionlint@1.7.7 - - flake8@7.1.2 + - 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.24.2 + - gitleaks@8.27.2 - clang-format@16.0.3 ignore: - linters: [ALL] @@ -38,7 +38,7 @@ runtimes: enabled: - python@3.10.8 - go@1.21.0 - - node@18.20.5 + - node@22.16.0 actions: disabled: - trunk-announce diff --git a/Dockerfile b/Dockerfile index 733a46325..e033b1bba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,20 @@ -# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue # trunk-ignore-all(trivy/DS002): We must run as root for this container -# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container # trunk-ignore-all(hadolint/DL3002): We must run as root for this container # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions FROM python:3.13-bookworm AS builder +ARG PIO_ENV=native ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC # Install Dependencies ENV PIP_ROOT_USER_ACTION=ignore RUN apt-get update && apt-get install --no-install-recommends -y \ - wget g++ zip git ca-certificates \ + curl wget g++ zip git ca-certificates pkg-config \ libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ - libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config \ + libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ + libx11-dev libinput-dev libxkbcommon-x11-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware @@ -24,13 +24,26 @@ WORKDIR /tmp/firmware COPY . /tmp/firmware # Build -RUN bash ./bin/build-native.sh && \ +RUN bash ./bin/build-native.sh "$PIO_ENV" && \ cp "/tmp/firmware/release/meshtasticd_linux_$(uname -m)" "/tmp/firmware/release/meshtasticd" +# Fetch web assets +RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/firmware/bin/web.version)/build.tar" -o /tmp/web.tar \ + && mkdir -p /tmp/web \ + && tar -xf /tmp/web.tar -C /tmp/web/ \ + && gzip -dr /tmp/web \ + && rm /tmp/web.tar ##### PRODUCTION BUILD ############# FROM debian:bookworm-slim +LABEL org.opencontainers.image.title="Meshtastic" \ + org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \ + org.opencontainers.image.url="https://meshtastic.org" \ + org.opencontainers.image.documentation="https://meshtastic.org/docs/" \ + org.opencontainers.image.authors="Meshtastic" \ + org.opencontainers.image.licenses="GPL-3.0-or-later" \ + org.opencontainers.image.source="https://github.com/meshtastic/firmware/" ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC @@ -38,14 +51,17 @@ ENV TZ=Etc/UTC USER root RUN apt-get update && apt-get --no-install-recommends -y install \ - libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev liborcania2.3 libulfius2.7 libssl3 \ + libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev \ + liborcania2.3 libulfius2.7 libssl3 \ + libx11-6 libinput10 libxkbcommon-x11-0 \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ && mkdir -p /etc/meshtasticd/ssl # Fetch compiled binary from the builder -COPY --from=builder /tmp/firmware/release/meshtasticd /usr/sbin/ +COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ +COPY --from=builder /tmp/web /usr/share/meshtasticd/ # Copy config templates COPY ./bin/config.d /etc/meshtasticd/available.d @@ -54,7 +70,9 @@ VOLUME /var/lib/meshtasticd # Expose Meshtastic TCP API port from the host EXPOSE 4403 +# Expose Meshtastic Web UI port from the host +EXPOSE 9443 -CMD [ "sh", "-cx", "meshtasticd -d /var/lib/meshtasticd" ] +CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ] -HEALTHCHECK NONE \ No newline at end of file +HEALTHCHECK NONE 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  + diff --git a/SECURITY.md b/SECURITY.md index fb4d9005a..5092595e1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Firmware Version | Supported | | ---------------- | ------------------ | -| 2.5.x | :white_check_mark: | -| <= 2.4.x | :x: | +| 2.6.x | :white_check_mark: | +| <= 2.5.x | :x: | ## Reporting a Vulnerability diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 17afc2964..670736241 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -1,15 +1,16 @@ # trunk-ignore-all(trivy/DS002): We must run as root for this container -# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container # trunk-ignore-all(hadolint/DL3002): We must run as root for this container # 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 + RUN apk --no-cache add \ bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \ libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \ + libx11-dev libinput-dev libxkbcommon-dev \ && rm -rf /var/cache/apk/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware @@ -21,23 +22,35 @@ COPY . /tmp/firmware # Add `argp` for musl ENV PLATFORMIO_BUILD_FLAGS="-Os -ffunction-sections -fdata-sections -Wl,--gc-sections -largp" -RUN bash ./bin/build-native.sh && \ +RUN bash ./bin/build-native.sh "$PIO_ENV" && \ cp "/tmp/firmware/release/meshtasticd_linux_$(uname -m)" "/tmp/firmware/release/meshtasticd" # ##### 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" \ + org.opencontainers.image.documentation="https://meshtastic.org/docs/" \ + org.opencontainers.image.authors="Meshtastic" \ + org.opencontainers.image.licenses="GPL-3.0-or-later" \ + org.opencontainers.image.source="https://github.com/meshtastic/firmware/" # nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root USER root RUN apk --no-cache add \ - libstdc++ libgpiod yaml-cpp libusb i2c-tools libuv \ + shadow libstdc++ libgpiod yaml-cpp libusb i2c-tools libuv \ + libx11 libinput libxkbcommon \ && rm -rf /var/cache/apk/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ && mkdir -p /etc/meshtasticd/ssl -COPY --from=builder /tmp/firmware/release/meshtasticd /usr/sbin/ + +# Fetch compiled binary from the builder +COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ +# Copy config templates +COPY ./bin/config.d /etc/meshtasticd/available.d WORKDIR /var/lib/meshtasticd VOLUME /var/lib/meshtasticd diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index df3778002..6b9ebcb24 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -2,7 +2,9 @@ [esp32_base] extends = arduino_base custom_esp32_kind = esp32 -platform = platformio/espressif32@6.10.0 +platform = + # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 + platformio/espressif32@6.11.0 build_src_filter = ${arduino_base.build_src_filter} - - - - - @@ -44,13 +46,20 @@ lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${environmental_base.lib_deps} + ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} - https://github.com/meshtastic/esp32_https_server/archive/23665b3adc080a311dcbb586ed5941b5f94d6ea2.zip + # 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/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 - lewisxhe/XPowersLib@^0.2.7 + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + 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 - rweather/Crypto@^0.4.0 + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 lib_ignore = segger_rtt diff --git a/arch/esp32/esp32c6.ini b/arch/esp32/esp32c6.ini index dba3bac08..1afb9b547 100644 --- a/arch/esp32/esp32c6.ini +++ b/arch/esp32/esp32c6.ini @@ -1,6 +1,8 @@ [esp32c6_base] extends = esp32_base -platform = https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip +platform = + # Do not renovate until we have switched to pioarduino tagged builds + https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip build_flags = ${arduino_base.build_flags} -Wall @@ -23,10 +25,14 @@ lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${environmental_base.lib_deps} + ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} - lewisxhe/XPowersLib@^0.2.7 + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + 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 - rweather/Crypto@^0.4.0 + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 build_src_filter = ${esp32_base.build_src_filter} - diff --git a/arch/esp32/esp32s2.ini b/arch/esp32/esp32s2.ini index 40fdc461a..0f97408b8 100644 --- a/arch/esp32/esp32s2.ini +++ b/arch/esp32/esp32s2.ini @@ -16,4 +16,4 @@ build_flags = lib_ignore = ${esp32_base.lib_ignore} NimBLE-Arduino - libpax \ No newline at end of file + libpax diff --git a/arch/esp32/esp32s3.ini b/arch/esp32/esp32s3.ini index 1cd0e2033..8d8b6899e 100644 --- a/arch/esp32/esp32s3.ini +++ b/arch/esp32/esp32s3.ini @@ -3,4 +3,3 @@ extends = esp32_base custom_esp32_kind = esp32s3 monitor_speed = 115200 - diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini index ca12be6b1..4a77ec4b2 100644 --- a/arch/nrf52/nrf52.ini +++ b/arch/nrf52/nrf52.ini @@ -1,10 +1,14 @@ [nrf52_base] ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files -platform = platformio/nordicnrf52@^10.8.0 +platform = + # renovate: datasource=custom.pio depName=platformio/nordicnrf52 packageName=platformio/platform/nordicnrf52 + platformio/nordicnrf52@^10.8.0 extends = arduino_base platform_packages = ; our custom Git version until they merge our PR + # TODO renovate platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#e13f5820002a4fb2a5e6754b42ace185277e5adf + ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 build_type = debug @@ -24,8 +28,9 @@ build_src_filter = lib_deps= ${arduino_base.lib_deps} ${radiolib_base.lib_deps} - rweather/Crypto@^0.4.0 + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 lib_ignore = BluetoothOTA - lvgl \ No newline at end of file + lvgl diff --git a/arch/nrf52/nrf52840.ini b/arch/nrf52/nrf52840.ini index 0dab5d9ba..5e846b3b7 100644 --- a/arch/nrf52/nrf52840.ini +++ b/arch/nrf52/nrf52840.ini @@ -6,7 +6,9 @@ build_flags = ${nrf52_base.build_flags} lib_deps = ${nrf52_base.lib_deps} ${environmental_base.lib_deps} - https://github.com/Kongduino/Adafruit_nRFCrypto/archive/e31a8825ea3300b163a0a3c1ddd5de34e10e1371.zip + ${environmental_extra.lib_deps} + # renovate: datasource=git-refs depName=Kongduino-Adafruit_nRFCrypto packageName=https://github.com/Kongduino/Adafruit_nRFCrypto gitBranch=master + https://github.com/Kongduino/Adafruit_nRFCrypto/archive/5f838d2709461a2c981f642917aa50254a25c14c.zip ; Common NRF52 debugging settings follow. See the Meshtastic developer docs for how to connect SWD debugging probes to your board. diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index 81f861d5a..9ca33b80a 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -1,6 +1,8 @@ ; The Portduino based 'native' environment. Currently supported on Linux targets with real LoRa hardware (or simulated). [portduino_base] -platform = https://github.com/meshtastic/platform-native/archive/c5bd469ab9b5a6966321e09557b27d906961da63.zip +platform = + # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop + https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip framework = arduino build_src_filter = @@ -15,18 +17,21 @@ build_src_filter = + - - - - - - - - +<../variants/portduino> lib_deps = ${env.lib_deps} ${networking_base.lib_deps} ${radiolib_base.lib_deps} - rweather/Crypto@^0.4.0 + ${environmental_base.lib_deps} + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@^1.2.0 - https://github.com/pine64/libch341-spi-userspace/archive/a9b17e3452f7fb747000d9b4ad4409155b39f6ef.zip + # 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} @@ -43,4 +48,10 @@ build_flags = -lyaml-cpp -li2c -luv + -std=gnu17 -std=c++17 + +lib_ignore = + Adafruit NeoPixel + Adafruit ST7735 and ST7789 Library + SD diff --git a/arch/rp2xx0/rp2040.ini b/arch/rp2xx0/rp2040.ini index 33fcfb211..4f9421872 100644 --- a/arch/rp2xx0/rp2040.ini +++ b/arch/rp2xx0/rp2040.ini @@ -1,8 +1,13 @@ ; Common settings for rp2040 Processor based targets [rp2040_base] -platform = https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 ; For arduino-pico >= 4.4.3 +platform = + # TODO renovate + https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 + ; For arduino-pico >= 4.4.3 extends = arduino_base -platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 +platform_packages = + # TODO renovate + framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -23,5 +28,7 @@ lib_ignore = lib_deps = ${arduino_base.lib_deps} ${environmental_base.lib_deps} + ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} - rweather/Crypto \ No newline at end of file + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 diff --git a/arch/rp2xx0/rp2350.ini b/arch/rp2xx0/rp2350.ini index 841035c80..e8611a113 100644 --- a/arch/rp2xx0/rp2350.ini +++ b/arch/rp2xx0/rp2350.ini @@ -1,8 +1,13 @@ -; Common settings for rp2040 Processor based targets +; Common settings for rp2350 Processor based targets [rp2350_base] -platform = https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 ; For arduino-pico >= 4.4.3 +platform = + # TODO renovate + https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 + ; For arduino-pico >= 4.4.3 extends = arduino_base -platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 +platform_packages = + # TODO renovate + framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -20,5 +25,7 @@ lib_ignore = lib_deps = ${arduino_base.lib_deps} ${environmental_base.lib_deps} + ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} - rweather/Crypto \ No newline at end of file + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index c1b58bb82..be1ed662f 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -1,7 +1,11 @@ [stm32_base] extends = arduino_base -platform = ststm32 -platform_packages = platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip +platform = + # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 + platformio/ststm32@19.2.0 +platform_packages = + # TODO renovate + platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip extra_scripts = ${env.extra_scripts} post:extra_scripts/extra_stm32.py @@ -12,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 @@ -35,8 +42,9 @@ 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 = - mathertel/OneButton@2.6.1 - Wire + OneButton diff --git a/bin/99-meshtasticd-udev.rules b/bin/99-meshtasticd-udev.rules new file mode 100644 index 000000000..1151efafd --- /dev/null +++ b/bin/99-meshtasticd-udev.rules @@ -0,0 +1,7 @@ +# Set spidev ownership to 'spi' group +SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660" +# Allow access to USB CH341 devices +SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666" +# Set gpio ownership to 'gpio' group +SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660" +SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" 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 c6b1434dd..fff86e87e 100755 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -15,6 +15,7 @@ platformioFailed() { VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) +PIO_ENV=${1:-native} OUTDIR=release/ @@ -24,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 native || platformioFailed -pio run --environment native || platformioFailed -cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)" +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/bump_metainfo/bump_metainfo.py b/bin/bump_metainfo/bump_metainfo.py new file mode 100755 index 000000000..290cbae79 --- /dev/null +++ b/bin/bump_metainfo/bump_metainfo.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +import argparse +import xml.etree.ElementTree as ET +from defusedxml.ElementTree import parse +from datetime import datetime, timezone + + +# Indent by 2 spaces to align with xml formatting. +def indent(elem, level=0): + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + for child in elem: + indent(child, level + 1) + if not child.tail or not child.tail.strip(): + child.tail = i + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +def main(): + parser = argparse.ArgumentParser( + description="Prepend new release entry to metainfo.xml file.") + parser.add_argument("--file", help="Path to the metainfo.xml file", + default="org.meshtastic.meshtasticd.metainfo.xml") + parser.add_argument("version", help="Version string (e.g. 2.6.4)") + parser.add_argument("--date", help="Release date (YYYY-MM-DD), defaults to today", + default=datetime.now(timezone.utc).date().isoformat()) + + args = parser.parse_args() + + tree = parse(args.file) + root = tree.getroot() + + releases = root.find('releases') + if releases is None: + raise RuntimeError(" element not found in XML.") + + existing_versions = { + release.get('version'): release + for release in releases.findall('release') + } + existing_release = existing_versions.get(args.version) + + if existing_release is not None: + if not existing_release.get('date'): + print(f"Version {args.version} found without date. Adding date...") + existing_release.set('date', args.date) + else: + print( + f"Version {args.version} is already present with date, skipping insertion.") + else: + new_release = ET.Element('release', { + 'version': args.version, + 'date': args.date + }) + url = ET.SubElement(new_release, 'url', {'type': 'details'}) + url.text = f"https://github.com/meshtastic/firmware/releases?q=tag%3Av{args.version}" + + releases.insert(0, new_release) + + indent(releases, level=1) + releases.tail = "\n" + + print(f"Inserted new release: {args.version}") + + tree.write(args.file, encoding='UTF-8', xml_declaration=True) + + +if __name__ == "__main__": + main() diff --git a/bin/bump_metainfo/requirements.txt b/bin/bump_metainfo/requirements.txt new file mode 100644 index 000000000..09dd20d24 --- /dev/null +++ b/bin/bump_metainfo/requirements.txt @@ -0,0 +1 @@ +defusedxml==0.7.1 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 722f80fae..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: @@ -188,14 +187,26 @@ Logging: # AsciiLogs: true # default if not specified is !isatty() on stdout Webserver: -# Port: 443 # Port for Webserver & Webservices +# Port: 9443 # Port for Webserver & Webservices # RootPath: /usr/share/meshtasticd/web # Root Dir of WebServer # SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present # SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present + +HostMetrics: +# ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled +# Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel. +# 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 ConfigDirectory: /etc/meshtasticd/config.d/ + AvailableDirectory: /etc/meshtasticd/available.d/ # MACAddress: AA:BB:CC:DD:EE:FF -# MACAddressSource: eth0 \ No newline at end of file +# MACAddressSource: eth0 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-900M30S.yaml b/bin/config.d/lora-MeshAdv-900M30S.yaml index 113901d5e..5c148bf68 100644 --- a/bin/config.d/lora-MeshAdv-900M30S.yaml +++ b/bin/config.d/lora-MeshAdv-900M30S.yaml @@ -1,3 +1,5 @@ +# MeshAdv-Pi E22-900M30S +# https://github.com/chrismyers2000/MeshAdv-Pi-Hat Lora: Module: sx1262 CS: 21 @@ -9,4 +11,4 @@ Lora: DIO3_TCXO_VOLTAGE: true # Only for E22-900M33S: # Limit the output power to 8 dBm - # SX126X_MAX_POWER: 8 \ No newline at end of file + # SX126X_MAX_POWER: 8 diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..b47b5c996 --- /dev/null +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -0,0 +1,11 @@ +# MeshAdv Mini E22-900M22S +# https://github.com/chrismyers2000/MeshAdv-Mini +Lora: + Module: sx1262 # Ebyte E22-900M22S + CS: 8 + IRQ: 16 + Busy: 20 + Reset: 24 + 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-piggystick-lr1121.yaml b/bin/config.d/lora-piggystick-lr1121.yaml new file mode 100644 index 000000000..348db61b1 --- /dev/null +++ b/bin/config.d/lora-piggystick-lr1121.yaml @@ -0,0 +1,11 @@ +Lora: + Module: lr1121 + CS: 0 + IRQ: 6 + Reset: 2 + Busy: 4 + spidev: ch341 + DIO3_TCXO_VOLTAGE: 1.8 +# USB_Serialnum: 12345678 + USB_PID: 0x5512 + USB_VID: 0x1A86 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 594d973f5..12bfd4f6e 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -5,26 +5,26 @@ 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" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" +SET "BPS_RESET=0" @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone" SET "C3=esp32c3" @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. -SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core t-watch-s3 tracksenger" -SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite" +SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" +SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3" 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) +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) @@ -34,14 +34,16 @@ 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 -ECHO %SCRIPT_NAME% [Version 2.6.1] +ECHO %SCRIPT_NAME% [Version 2.6.2] ECHO Meshtastic GOTO eof @@ -57,11 +59,13 @@ 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 :endopts +IF %BPS_RESET% EQU 1 GOTO skip-filename + CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..." IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -f filename input." @@ -95,6 +99,9 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!" ) +:skip-filename +SET "ESPTOOL_BAUD=1200" + CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=!PYTHON! -m esptool" @@ -133,13 +140,16 @@ IF "__!ESPTOOL_PORT!__" == "____" ( ) CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." +IF %BPS_RESET% EQU 1 ( + @REM Attempt to change mode via 1200bps Reset. + CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + GOTO eof +) + @REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. @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!" @@ -193,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. @@ -254,6 +259,7 @@ EXIT /B %ERRORLEVEL% IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 +IF %BPS_RESET% EQU 1 GOTO :eof IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" EXIT /B %ERRORLEVEL% diff --git a/bin/device-install.sh b/bin/device-install.sh index bacf48f69..4674113b6 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -1,7 +1,7 @@ #!/bin/bash PYTHON=${PYTHON:-$(which python3 python | head -n 1)} -WEB_APP=false +BPS_RESET=false TFT_BUILD=false MCU="" @@ -22,7 +22,6 @@ BIGDB_8MB=( "icarus" "seeed-xiao-s3" "tbeam-s3-core" - "t-watch-s3" "tracksenger" ) BIGDB_16MB=( @@ -33,174 +32,185 @@ BIGDB_16MB=( "ESP32-S3-Pico" "m5stack-cores3" "station-g2" - "t-eth-elite" + "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" + "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 -[ -z "$FILENAME" -a -n "$1" ] && { - FILENAME=$1 - shift +if [[ $BPS_RESET == true ]]; then + $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + exit 0 +fi + +[ -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 [ -n "${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 [ -n "${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 [ -n "${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.bat b/bin/device-update.bat index d9a4bd19a..6d55294a7 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -8,12 +8,13 @@ SET "PYTHON=" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" +SET "CHANGE_MODE=0" GOTO getopts :help ECHO Flash image file to device, but leave existing system intact. ECHO. -ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] +ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--change-mode] ECHO. ECHO Options: ECHO -f filename The update .bin file to flash. Custom to your device type and region. (required) @@ -23,12 +24,15 @@ 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 --change-mode Attempt to place the device in correct mode. (1200bps Reset) +ECHO Some hardware requires this twice. ECHO. +ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11 GOTO eof :version -ECHO %SCRIPT_NAME% [Version 2.6.1] +ECHO %SCRIPT_NAME% [Version 2.6.2] ECHO Meshtastic GOTO eof @@ -44,10 +48,13 @@ 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"=="--change-mode" SET "CHANGE_MODE=1" SHIFT GOTO getopts :endopts +IF %CHANGE_MODE% EQU 1 GOTO skip-filename + CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..." IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -f filename input." @@ -77,6 +84,9 @@ IF "!FILENAME:update=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!" ) +:skip-filename +SET "ESPTOOL_BAUD=1200" + CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=!PYTHON! -m esptool" @@ -115,6 +125,12 @@ IF "__!ESPTOOL_PORT!__" == "____" ( ) CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." +IF %CHANGE_MODE% EQU 1 ( + @REM Attempt to change mode via 1200bps Reset. + CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + GOTO eof +) + @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..." CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof @@ -135,6 +151,7 @@ EXIT /B %ERRORLEVEL% IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 +IF %CHANGE_MODE% EQU 1 GOTO :eof IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" EXIT /B %ERRORLEVEL% diff --git a/bin/device-update.sh b/bin/device-update.sh index ae7b52ea2..ce0b5e434 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -1,6 +1,7 @@ #!/bin/sh PYTHON=${PYTHON:-$(which python3 python|head -n 1)} +CHANGE_MODE=false # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then @@ -17,17 +18,29 @@ fi # Usage info show_help() { cat << EOF -Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] +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). -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -f FILENAME The *update.bin file to flash. Custom to your device type. - + --change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) + 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 @@ -36,13 +49,13 @@ while getopts ":hp:P:f:" opt; do exit 0 ;; p) ESPTOOL_CMD="$ESPTOOL_CMD --port ${OPTARG}" - ;; + ;; P) PYTHON=${OPTARG} ;; f) FILENAME=${OPTARG} ;; *) - echo "Invalid flag." + echo "Invalid flag." show_help >&2 exit 1 ;; @@ -50,17 +63,22 @@ while getopts ":hp:P:f:" opt; do done shift "$((OPTIND-1))" -[ -z "$FILENAME" -a -n "$1" ] && { - FILENAME=$1 +if [ "$CHANGE_MODE" = true ]; then + $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + exit 0 +fi + +[ -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/meshtasticd.service b/bin/meshtasticd.service index 1e8ee98b8..63430bae8 100644 --- a/bin/meshtasticd.service +++ b/bin/meshtasticd.service @@ -5,10 +5,11 @@ StartLimitInterval=200 StartLimitBurst=5 [Service] -User=root -Group=root +AmbientCapabilities=CAP_NET_BIND_SERVICE +User=meshtasticd +Group=meshtasticd Type=simple -ExecStart=/usr/sbin/meshtasticd +ExecStart=/usr/bin/meshtasticd Restart=always RestartSec=3 diff --git a/bin/native-install.sh b/bin/native-install.sh index a8fcc29a6..18cd9205b 100755 --- a/bin/native-install.sh +++ b/bin/native-install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -cp "release/meshtasticd_linux_$(uname -m)" /usr/sbin/meshtasticd +cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd mkdir -p /etc/meshtasticd if [[ -f "/etc/meshtasticd/config.yaml" ]]; then cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml diff --git a/bin/org.meshtastic.meshtasticd.desktop b/bin/org.meshtastic.meshtasticd.desktop new file mode 100644 index 000000000..215c7ee05 --- /dev/null +++ b/bin/org.meshtastic.meshtasticd.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Meshtastic +Comment=Meshtastic App +Exec=meshtasticd +Icon=org.meshtastic.meshtasticd +Terminal=true +Type=Application +Categories=Network;Chat;HamRadio; \ No newline at end of file diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml new file mode 100644 index 000000000..291fe7a7c --- /dev/null +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -0,0 +1,133 @@ + + + org.meshtastic.meshtasticd + + Meshtastic + Decentralized mesh communication + + CC-BY-4.0 + GPL-3.0-or-later + + + Meshtastic + + + + + Meshtastic is an open source project for creating off-grid, affordable, and resilient communication with LoRa mesh networks. + + + + org.meshtastic.meshtasticd.desktop + + + Network + Chat + HamRadio + + + mesh + LoRa + + + + keyboard + pointing + touch + + + 360 + + + + #97be89 + #206538 + + + + intense + intense + + + https://github.com/meshtastic/firmware/issues + https://meshtastic.org/ + https://opencollective.com/meshtastic + https://meshtastic.org/docs/software/linux/usage/ + https://github.com/meshtastic/firmware/ + + + + https://meshtastic.org/img/software/meshtastic-ui/mui_home_dashboard_dark.webp + Home Dashboard + + + https://meshtastic.org/img/software/meshtastic-ui/mui_initial_boot.webp + Setup + + + https://meshtastic.org/img/software/meshtastic-ui/mui_node_list_dark.webp + Nodes List + + + https://meshtastic.org/img/software/meshtastic-ui/mui_chat_list_dark.webp + Chats List + + + https://meshtastic.org/img/software/meshtastic-ui/mui_chat_message_dark.webp + Messages + + + https://meshtastic.org/img/software/meshtastic-ui/mui_map_dark.webp + Map + + + https://meshtastic.org/img/software/meshtastic-ui/mui_settings_dark.webp + Settings + + + + + + 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 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.10 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.9 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.8 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.7 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.6 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.5 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.4 + + + \ No newline at end of file diff --git a/bin/org.meshtastic.meshtasticd.svg b/bin/org.meshtastic.meshtasticd.svg new file mode 100644 index 000000000..e6863f6a6 --- /dev/null +++ b/bin/org.meshtastic.meshtasticd.svg @@ -0,0 +1,16 @@ + + + +Created with Fabric.js 4.6.0 + + + + + + + + + + + + \ No newline at end of file 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/bin/rpkg.macros b/bin/rpkg.macros index 2bbb203de..aa036fc33 100644 --- a/bin/rpkg.macros +++ b/bin/rpkg.macros @@ -2,6 +2,10 @@ function meshtastic_version { meshtastic_version=$(python3 bin/buildinfo.py short) echo -n "$meshtastic_version" } +function web_version { + web_version=$(cat bin/web.version) + echo -n "$web_version" +} function git_commits_num { total_commits=$(git rev-list --all --count) echo -n "$total_commits" diff --git a/bin/web.version b/bin/web.version new file mode 100644 index 000000000..e46a05b19 --- /dev/null +++ b/bin/web.version @@ -0,0 +1 @@ +2.6.4 \ No newline at end of file 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/crowpanel.json b/boards/crowpanel.json new file mode 100644 index 000000000..75b097ef7 --- /dev/null +++ b/boards/crowpanel.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "ESP32-S3-WROOM-1-N16R8" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "ESP32-S3-WROOM-1-N16R8 (16 MB Flash, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 524288, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "monitor": { + "speed": 115200 + }, + "url": "https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf", + "vendor": "Espressif" +} 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/heltec_mesh_pocket.json b/boards/heltec_mesh_pocket.json new file mode 100644 index 000000000..e078c860c --- /dev/null +++ b/boards/heltec_mesh_pocket.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_pocket", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/project/meshpocket/", + "vendor": "Heltec" +} diff --git a/boards/heltec_vision_master_e213.json b/boards/heltec_vision_master_e213.json index bf5fe15ad..152515cf3 100644 --- a/boards/heltec_vision_master_e213.json +++ b/boards/heltec_vision_master_e213.json @@ -2,7 +2,8 @@ "build": { "arduino": { "ldscript": "esp32s3_out.ld", - "partitions": "default_8MB.csv" + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" }, "core": "esp32", "extra_flags": [ @@ -15,6 +16,7 @@ "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", + "psram_type": "opi", "hwids": [ ["0x303A", "0x1001"], ["0x303A", "0x0002"] diff --git a/boards/heltec_vision_master_e290.json b/boards/heltec_vision_master_e290.json index 70f7d5f02..b7cbac878 100644 --- a/boards/heltec_vision_master_e290.json +++ b/boards/heltec_vision_master_e290.json @@ -2,7 +2,8 @@ "build": { "arduino": { "ldscript": "esp32s3_out.ld", - "partitions": "default_8MB.csv" + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" }, "core": "esp32", "extra_flags": [ @@ -15,6 +16,7 @@ "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", + "psram_type": "opi", "hwids": [ ["0x303A", "0x1001"], ["0x303A", "0x0002"] diff --git a/boards/heltec_vision_master_t190.json b/boards/heltec_vision_master_t190.json index 341e70218..440f76ad0 100644 --- a/boards/heltec_vision_master_t190.json +++ b/boards/heltec_vision_master_t190.json @@ -2,7 +2,8 @@ "build": { "arduino": { "ldscript": "esp32s3_out.ld", - "partitions": "default_8MB.csv" + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" }, "core": "esp32", "extra_flags": [ @@ -15,6 +16,7 @@ "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", + "psram_type": "opi", "hwids": [ ["0x303A", "0x1001"], ["0x303A", "0x0002"] diff --git a/boards/seeed-sensecap-indicator.json b/boards/seeed-sensecap-indicator.json index 0a02fc882..03bff35b5 100644 --- a/boards/seeed-sensecap-indicator.json +++ b/boards/seeed-sensecap-indicator.json @@ -18,6 +18,7 @@ "f_boot": "120000000L", "boot": "qio", "flash_mode": "qio", + "psram_type": "opi", "hwids": [["0x1A86", "0x7523"]], "mcu": "esp32s3", "variant": "esp32s3" diff --git a/boards/seeed-xiao-s3.json b/boards/seeed-xiao-s3.json index 0b7b432a0..6981085dd 100644 --- a/boards/seeed-xiao-s3.json +++ b/boards/seeed-xiao-s3.json @@ -15,6 +15,7 @@ "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", + "psram_type": "opi", "hwids": [["0x2886", "0x0059"]], "mcu": "esp32s3", "variant": "seeed-xiao-s3" diff --git a/boards/seeed_solar_node.json b/boards/seeed_solar_node.json new file mode 100644 index 000000000..e77fbd077 --- /dev/null +++ b/boards/seeed_solar_node.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [["0x2886", "0x0059"]], + "usb_product": "XIAO-BOOT", + "mcu": "nrf52840", + "variant": "seeed_solar_node", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "seeed_solar_node", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Seeed-XIAO-BLE-Sense-nRF52840-p-5253.html", + "vendor": "Seeed Studio" +} diff --git a/boards/seeed_wio_tracker_L1.json b/boards/seeed_wio_tracker_L1.json new file mode 100644 index 000000000..e2bb93573 --- /dev/null +++ b/boards/seeed_wio_tracker_L1.json @@ -0,0 +1,57 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x2886", "0x1668"], + ["0x2886", "0x1667"] + ], + "usb_product": "TRACKER L1", + "mcu": "nrf52840", + "variant": "seeed_wio_tracker_L1", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "seeed_wio_tracker_L1", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Wio-Tracker-L1-p-6477.html", + "vendor": "Seeed Studio" +} 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/t-watch-s3.json b/boards/t-watch-s3.json index 5d4afd322..bae4f47b0 100644 --- a/boards/t-watch-s3.json +++ b/boards/t-watch-s3.json @@ -16,6 +16,7 @@ "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", + "psram_type": "opi", "hwids": [ ["0x303A", "0x1001"], ["0x303A", "0x0002"] @@ -23,16 +24,16 @@ "mcu": "esp32s3", "variant": "t-watch-s3" }, - "connectivity": ["wifi", "bluetooth"], + "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino"], "name": "LilyGo T-Watch 2020 V3", "upload": { - "flash_size": "8MB", + "flash_size": "16MB", "maximum_ram_size": 327680, - "maximum_size": 8388608, + "maximum_size": 16777216, "require_upload_port": true, "use_1200bps_touch": true, "wait_for_upload_port": true, 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 3ec57b805..b5009028a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,34 @@ -meshtasticd (2.5.22.0) UNRELEASED; urgency=medium +meshtasticd (2.7.3.0) UNRELEASED; urgency=medium + [ Austin Lane ] * Initial packaging * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump - -- Austin Lane Wed, 05 Feb 2025 01:10:33 +0000 + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ Ubuntu ] + * GitHub Actions Automatic version bump + + -- Ubuntu Thu, 10 Jul 2025 16:29:27 +0000 diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh index a8b2252ae..81e681e0c 100755 --- a/debian/ci_pack_sdeb.sh +++ b/debian/ci_pack_sdeb.sh @@ -5,13 +5,14 @@ export PLATFORMIO_PACKAGES_DIR=pio/packages export PLATFORMIO_CORE_DIR=pio/core # Download libraries to `pio` -platformio pkg install -e native -platformio pkg install -e native -t platformio/tool-scons@4.40502.0 +platformio pkg install -e native-tft +platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0 # Compress `pio` directory to prevent dh_clean from sanitizing it tar -cf pio.tar pio/ rm -rf pio -# Download the latest meshtastic/web release build.tar to `web.tar` -curl -L https://github.com/meshtastic/web/releases/latest/download/build.tar -o web.tar +# Download the meshtastic/web release build.tar to `web.tar` +web_ver=$(cat bin/web.version) +curl -L "https://github.com/meshtastic/web/releases/download/v$web_ver/build.tar" -o web.tar package=$(dpkg-parsechangelog --show-field Source) diff --git a/debian/control b/debian/control index 693cd6aa5..761383a5c 100644 --- a/debian/control +++ b/debian/control @@ -21,14 +21,19 @@ Build-Depends: debhelper-compat (= 13), openssl, libssl-dev, libulfius-dev, - liborcania-dev + liborcania-dev, + libx11-dev, + libinput-dev, + libxkbcommon-x11-dev Standards-Version: 4.6.2 Homepage: https://github.com/meshtastic/firmware Rules-Requires-Root: no Package: meshtasticd Architecture: any -Depends: ${misc:Depends}, ${shlibs:Depends} +Depends: adduser, + ${misc:Depends}, + ${shlibs:Depends} Description: Meshtastic daemon for communicating with Meshtastic devices Meshtastic is an off-grid text communication platform that uses inexpensive - LoRa radios. \ No newline at end of file + LoRa radios. diff --git a/debian/meshtasticd.dirs b/debian/meshtasticd.dirs index 45a1ca3db..a667768b2 100644 --- a/debian/meshtasticd.dirs +++ b/debian/meshtasticd.dirs @@ -1,5 +1,6 @@ +var/lib/meshtasticd etc/meshtasticd etc/meshtasticd/config.d etc/meshtasticd/available.d usr/share/meshtasticd/web -etc/meshtasticd/ssl \ No newline at end of file +etc/meshtasticd/ssl diff --git a/debian/meshtasticd.install b/debian/meshtasticd.install index da1b0685d..3c68b42b1 100644 --- a/debian/meshtasticd.install +++ b/debian/meshtasticd.install @@ -1,8 +1,8 @@ -.pio/build/native/meshtasticd usr/sbin +.pio/build/native-tft/meshtasticd usr/bin -bin/config.yaml etc/meshtasticd -bin/config.d/* etc/meshtasticd/available.d +bin/config.yaml etc/meshtasticd +bin/config.d/* etc/meshtasticd/available.d -bin/meshtasticd.service lib/systemd/system +bin/meshtasticd.service lib/systemd/system -web/* usr/share/meshtasticd/web \ No newline at end of file +web/* usr/share/meshtasticd/web diff --git a/debian/meshtasticd.postinst b/debian/meshtasticd.postinst new file mode 100755 index 000000000..fe0dbc332 --- /dev/null +++ b/debian/meshtasticd.postinst @@ -0,0 +1,80 @@ +#!/bin/sh +# postinst script for meshtasticd +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure|reconfigure) + # create spi, gpio groups (for udev rules) + # these groups already exist on Raspberry Pi OS + getent group spi >/dev/null 2>/dev/null || addgroup --system spi + getent group gpio >/dev/null 2>/dev/null || addgroup --system gpio + # create a meshtasticd group and user + getent passwd meshtasticd >/dev/null 2>/dev/null || adduser --system --home /var/lib/meshtasticd --no-create-home meshtasticd + getent group meshtasticd >/dev/null 2>/dev/null || addgroup --system meshtasticd + adduser meshtasticd meshtasticd >/dev/null 2>/dev/null + adduser meshtasticd spi >/dev/null 2>/dev/null + adduser meshtasticd gpio >/dev/null 2>/dev/null + # add meshtasticd user to appropriate groups (if they exist) + getent group plugdev >/dev/null 2>/dev/null && adduser meshtasticd plugdev >/dev/null 2>/dev/null + getent group dialout >/dev/null 2>/dev/null && adduser meshtasticd dialout >/dev/null 2>/dev/null + getent group i2c >/dev/null 2>/dev/null && adduser meshtasticd i2c >/dev/null 2>/dev/null + getent group video >/dev/null 2>/dev/null && adduser meshtasticd video >/dev/null 2>/dev/null + getent group audio >/dev/null 2>/dev/null && adduser meshtasticd audio >/dev/null 2>/dev/null + getent group input >/dev/null 2>/dev/null && adduser meshtasticd input >/dev/null 2>/dev/null + + + # migrate /root/.portduino to /var/lib/meshtasticd/.portduino + # should only run once, upon upgrade from < 2.6.9 + if [ -n "$2" ] && dpkg --compare-versions "$2" lt 2.6.9; then + if [ -d /root/.portduino ] && [ ! -e /var/lib/meshtasticd/.portduino ]; then + cp -r /root/.portduino /var/lib/meshtasticd/.portduino + echo "Migrated meshtasticd VFS from /root/.portduino to /var/lib/meshtasticd/.portduino" + echo "meshtasticd now runs as the 'meshtasticd' user, not 'root'." + echo "See https://github.com/meshtastic/firmware/pull/6718 for details" + fi + fi + + if [ -d /var/lib/meshtasticd ]; then + chown -R meshtasticd:meshtasticd /var/lib/meshtasticd + fi + + if [ -d /etc/meshtasticd ]; then + chown -R meshtasticd:meshtasticd /etc/meshtasticd + fi + + if [ -d /usr/share/meshtasticd ]; then + chown -R meshtasticd:meshtasticd /usr/share/meshtasticd + fi + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/meshtasticd.postrm b/debian/meshtasticd.postrm new file mode 100755 index 000000000..bb2c32a5b --- /dev/null +++ b/debian/meshtasticd.postrm @@ -0,0 +1,41 @@ +#!/bin/sh +# postrm script for meshtasticd +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + # Only remove /var/lib/meshtasticd on purge + if [ "${1}" = "purge" ] ; then + rm -rf /var/lib/meshtasticd + fi + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/meshtasticd.udev b/debian/meshtasticd.udev new file mode 100644 index 000000000..1151efafd --- /dev/null +++ b/debian/meshtasticd.udev @@ -0,0 +1,7 @@ +# Set spidev ownership to 'spi' group +SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660" +# Allow access to USB CH341 devices +SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666" +# Set gpio ownership to 'gpio' group +SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660" +SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" diff --git a/debian/rules b/debian/rules index 0612ba352..0b5d1ac57 100755 --- a/debian/rules +++ b/debian/rules @@ -26,7 +26,7 @@ override_dh_auto_build: mkdir -p web && tar -xf web.tar -C web gunzip web/ -r # Build with platformio - $(PIO_ENV) platformio run -e native + $(PIO_ENV) platformio run -e native-tft # Move the binary and default config to the correct name - mv .pio/build/native/program .pio/build/native/meshtasticd + mv .pio/build/native-tft/program .pio/build/native-tft/meshtasticd cp bin/config-dist.yaml bin/config.yaml diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index a09261056..eb4ab5ae7 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -10,6 +10,8 @@ # - https://docs.pagure.org/rpkg-util/v3/index.html # - https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/ +%global meshtasticd_user meshtasticd + Name: meshtasticd # Version Ex: 2.5.19 Version: {{{ meshtastic_version }}} @@ -21,7 +23,7 @@ Summary: Meshtastic daemon for communicating with Meshtastic devices License: GPL-3.0 URL: https://github.com/meshtastic/firmware Source0: {{{ git_dir_pack }}} -Source1: https://github.com/meshtastic/web/releases/latest/download/build.tar +Source1: https://github.com/meshtastic/web/releases/download/v{{{ web_version }}}/build.tar BuildRequires: systemd-rpm-macros BuildRequires: python3-devel @@ -42,6 +44,12 @@ BuildRequires: pkgconfig(openssl) BuildRequires: pkgconfig(liborcania) BuildRequires: pkgconfig(libyder) BuildRequires: pkgconfig(libulfius) +# TFT components: +BuildRequires: pkgconfig(x11) +BuildRequires: pkgconfig(libinput) +BuildRequires: pkgconfig(xkbcommon-x11) + +Requires: systemd-udev %description Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid @@ -55,19 +63,29 @@ tar -xf %{SOURCE1} -C web gzip -dr web %build -# Use the โnativeโ environment from platformio to build a Linux binary -platformio run -e native +# Use the โnative-tftโ environment from platformio to build a Linux binary +platformio run -e native-tft %install -mkdir -p %{buildroot}%{_sbindir} -install -m 0755 .pio/build/native/program %{buildroot}%{_sbindir}/meshtasticd +# Install meshtasticd binary +mkdir -p %{buildroot}%{_bindir} +install -m 0755 .pio/build/native-tft/program %{buildroot}%{_bindir}/meshtasticd +# Install portduino VFS dir +install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd + +# Install udev rules +mkdir -p %{buildroot}%{_udevrulesdir} +install -m 0644 bin/99-meshtasticd-udev.rules %{buildroot}%{_udevrulesdir}/99-meshtasticd-udev.rules + +# Install config dirs mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd install -m 0644 bin/config-dist.yaml %{buildroot}%{_sysconfdir}/meshtasticd/config.yaml mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/config.d mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/available.d cp -r bin/config.d/* %{buildroot}%{_sysconfdir}/meshtasticd/available.d +# Install systemd service install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.service # Install the web files under /usr/share/meshtasticd/web @@ -76,10 +94,54 @@ cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web # Install default SSL storage directory (for web) mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl +%pre +# create spi group (for udev rules) +getent group spi > /dev/null || groupadd -r spi +# create a meshtasticd group and user +getent group %{meshtasticd_user} > /dev/null || groupadd -r %{meshtasticd_user} +getent passwd %{meshtasticd_user} > /dev/null || \ + useradd -r -d %{_localstatedir}/lib/meshtasticd -g %{meshtasticd_user} -G spi \ + -s /sbin/nologin -c "Meshtastic Daemon" %{meshtasticd_user} +# add meshtasticd user to appropriate groups (if they exist) +getent group gpio > /dev/null && usermod -a -G gpio %{meshtasticd_user} > /dev/null +getent group plugdev > /dev/null && usermod -a -G plugdev %{meshtasticd_user} > /dev/null +getent group dialout > /dev/null && usermod -a -G dialout %{meshtasticd_user} > /dev/null +getent group i2c > /dev/null && usermod -a -G i2c %{meshtasticd_user} > /dev/null +getent group video > /dev/null && usermod -a -G video %{meshtasticd_user} > /dev/null +getent group audio > /dev/null && usermod -a -G audio %{meshtasticd_user} > /dev/null +getent group input > /dev/null && usermod -a -G input %{meshtasticd_user} > /dev/null +exit 0 + +%triggerin -- meshtasticd < 2.6.9 +# migrate .portduino (if it exists and hasnโt already been copied) +if [ -d /root/.portduino ] && [ ! -e /var/lib/meshtasticd/.portduino ]; then + mkdir -p /var/lib/meshtasticd + cp -r /root/.portduino /var/lib/meshtasticd/.portduino + chown -R %{meshtasticd_user}:%{meshtasticd_user} \ + %{_localstatedir}/lib/meshtasticd || : + # Fix SELinux labels if present (no-op on non-SELinux systems) + restorecon -R /var/lib/meshtasticd/.portduino 2>/dev/null || : + echo "Migrated meshtasticd VFS from /root/.portduino to /var/lib/meshtasticd/.portduino" + echo "meshtasticd now runs as the 'meshtasticd' user, not 'root'." + echo "See https://github.com/meshtastic/firmware/pull/6718 for details" +fi + +%post +%systemd_post meshtasticd.service + +%preun +%systemd_preun meshtasticd.service + +%postun +%systemd_postun_with_restart meshtasticd.service + %files +%defattr(-,%{meshtasticd_user},%{meshtasticd_user}) %license LICENSE %doc README.md -%{_sbindir}/meshtasticd +%{_bindir}/meshtasticd +%dir %{_localstatedir}/lib/meshtasticd +%{_udevrulesdir}/99-meshtasticd-udev.rules %dir %{_sysconfdir}/meshtasticd %dir %{_sysconfdir}/meshtasticd/config.d %dir %{_sysconfdir}/meshtasticd/available.d @@ -92,4 +154,4 @@ mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl %dir %{_sysconfdir}/meshtasticd/ssl %changelog -%autochangelog \ No newline at end of file +%autochangelog diff --git a/platformio.ini b/platformio.ini index c64f07b82..0c0d4e3c6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -49,19 +49,28 @@ 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 - #-DBUILD_EPOCH=$UNIX_TIME - #-D OLED_PL=1 + -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware + -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 + -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage + #-DBUILD_EPOCH=$UNIX_TIME + #-D OLED_PL=1 monitor_speed = 115200 monitor_filters = direct lib_deps = - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/e16cee124fe26490cb14880c679321ad8ac89c95.zip + # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip + # renovate: datasource=custom.pio depName=OneButton packageName=mathertel/library/OneButton mathertel/OneButton@2.6.1 + # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip + # renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip + # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master https://github.com/meshtastic/ArduinoThread/archive/7c3ee9e1951551b949763b1f5280f8db1fa4068d.zip + # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb nanopb/Nanopb@0.4.91 + # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 erriez/ErriezCRC32@1.0.1 https://github.com/meshtastic/SE05X#031f8feccae62689ebbd06914b44bd88547535af @@ -79,6 +88,7 @@ check_flags = framework = arduino lib_deps = ${env.lib_deps} + # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL end2endzone/NonBlockingRTTTL@1.3.0 build_flags = ${env.build_flags} -Os build_src_filter = ${env.build_src_filter} - - @@ -86,57 +96,109 @@ build_src_filter = ${env.build_src_filter} - -.+)$" + ], + "datasourceTemplate": "github-releases", + "depNameTemplate": "meshtastic/web", + "versioningTemplate": "semver-coerced" + }, + { + "customType": "regex", + "description": "Match normal PIO dependencies", + "managerFilePatterns": [ + "/.*\\.ini$/" + ], + "matchStrings": [ + "# renovate: datasource=(?.*?)(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\s+?.+?@(?.+?)\\s" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver-coerced{{/if}}" + }, + { + "customType": "regex", + "description": "Match PIO zipped dependencies with github tag ref", + "managerFilePatterns": [ + "/.*\\.ini$/" + ], + "matchStrings": [ + "# renovate: datasource=github-tags(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\s+?https://.+?archive/(?.+?).zip\\s" + ], + "datasourceTemplate": "github-tags", + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver-coerced{{/if}}" + }, + { + "customType": "regex", + "description": "Match PIO zipped dependencies with git commit ref", + "managerFilePatterns": [ + "/.*\\.ini$/" + ], + "matchStrings": [ + "# renovate: datasource=git-refs(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\sgitBranch=(?.+?)\\s+?https://.+?archive/(?.+?).zip\\s" + ], + "datasourceTemplate": "git-refs", + "currentValueTemplate": "{{{gitBranch}}}", + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}git{{/if}}" + } + ], + "packageRules": [ + { + "matchDepNames": [ + "meshtastic/device-ui" + ], + "reviewers": [ + "mverch67" + ], + "changelogUrl": "https://github.com/meshtastic/device-ui/compare/{{currentDigest}}...{{newDigest}}" + } + ] +} diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index c487f9d53..e4ef3b443 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -6,6 +6,11 @@ NCP5623 rgb; #endif +#ifdef HAS_LP5562 +#include +LP5562 rgbw; +#endif + #ifdef HAS_NEOPIXEL #include Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); @@ -26,7 +31,7 @@ class AmbientLightingThread : public concurrency::OSThread notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. // Enables Ambient Lighting by default if conditions are meet. -#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) +#ifdef HAS_RGB_LED #ifdef ENABLE_AMBIENTLIGHTING moduleConfig.ambient_lighting.led_state = true; #endif @@ -39,7 +44,7 @@ class AmbientLightingThread : public concurrency::OSThread // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; -#ifdef HAS_NCP5623 +#if defined(HAS_NCP5623) || defined(HAS_LP5562) _type = type; if (_type == ScanI2C::DeviceType::NONE) { LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus"); @@ -47,7 +52,7 @@ class AmbientLightingThread : public concurrency::OSThread return; } #endif -#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) +#ifdef HAS_RGB_LED if (!moduleConfig.ambient_lighting.led_state) { LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); disable(); @@ -58,66 +63,78 @@ class AmbientLightingThread : public concurrency::OSThread if (_type == ScanI2C::NCP5623) { rgb.begin(); #endif +#ifdef HAS_LP5562 + 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 -#ifdef HAS_NCP5623 } -#endif - } - protected: - int32_t runOnce() override - { -#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) -#ifdef HAS_NCP5623 - if (_type == ScanI2C::NCP5623 && moduleConfig.ambient_lighting.led_state) { + 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) { #endif - setLighting(); - return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification -#ifdef HAS_NCP5623 + 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"); #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); @@ -125,48 +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); #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); @@ -175,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 2363f804c..000000000 --- a/src/ButtonThread.cpp +++ /dev/null @@ -1,454 +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 -} - -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) - // #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - // buzzer_updata(); - // if (buttonPressed) { - // buttonPressed = false; // ๆธ ้คๆ ๅฟ - // LOG_INFO("PIN_BUTTON2 pressed!"); // ไธฒๅฃๆๅฐไฟกๆฏ - // // off_currentTime = millis(); - // while (digitalRead(PIN_BUTTON2) == HIGH) { - // if (cont < 40) { - // // unsigned long currentTime = millis(); // ่ทๅๅฝๅๆถ้ด - // // if (currentTime - off_currentTime >= 1000) { - // cont++; - // // off_currentTime = currentTime; - // // } - // delay(100); - // } else { - - // currentState = OFF; - // isBuzzing = false; - // cont = 0; - // BEEP_STATE = false; - // analogWrite(M2_buzzer, 0); - // pinMode(M2_buzzer, INPUT); - // screen->setOn(false); - // cont = 0; - // LOG_INFO("GGGGGGGGGGGGGGGGGGGGGGGGG"); - // pinMode(1, OUTPUT); - // digitalWrite(1, LOW); - // pinMode(6, OUTPUT); - // digitalWrite(6, LOW); - // } - // } - // } - - // #endif - 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(); - return 50; - } -#ifdef BUTTON_PIN -#if !defined(USERPREFS_BUTTON_PIN) - if (((config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN) != -#endif -#if defined(USERPREFS_BUTTON_PIN) - if (((config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN) != -#endif - 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(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 - break; - } - - case BUTTON_EVENT_PRESSED_SCREEN: { - // 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!"); - 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 - } - break; - } - - case BUTTON_EVENT_MULTI_PRESSED: { - LOG_BUTTON("Mulitipress! %hux", multipressClickCount); - switch (multipressClickCount) { -#if HAS_GPS - // 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_M2) - case 3: - LOG_INFO("3 clicks: toggle buzzer"); - buzzer_flag = !buzzer_flag; - if (buzzer_flag) { - playBeep(); - } - break; -#endif - -#if defined(USE_EINK) && defined(PIN_EINK_EN) // i.e. T-Echo - // 4 clicks: toggle backlight - case 4: - digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); - break; -#endif -#if defined(RAK_4631) - // 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!"); - if (screen) { - // 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 - wakeOnIrq(BUTTON_PIN_ALT, FALLING); -#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 a8f1f77c3..000000000 --- a/src/ButtonThread.h +++ /dev/null @@ -1,90 +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(); - - // 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); - - // IRQ callbacks - static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; } - static void userButtonPressedScreen() - { - if (millis() > c_holdOffTime) { - 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/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 0718ffcbd..44bc0897b 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -27,9 +27,6 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: return useShortName ? "LongM" : "LongMod"; break; - case meshtastic_Config_LoRaConfig_ModemPreset_VERY_LONG_SLOW: - return useShortName ? "VeryL" : "VLongSlow"; - break; default: return useShortName ? "Custom" : "Invalid"; break; diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 88f0764b5..f215be80f 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -12,13 +12,14 @@ #include "SPILock.h" #include "configuration.h" -#ifdef HAS_SDCARD +// Software SPI is used by MUI so disable SD card here until it's also implemented +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) #include #include #ifdef SDCARD_USE_SPI1 -SPIClass SPI1(HSPI); -#define SDHandler SPI1 +SPIClass SPI_HSPI(HSPI); +#define SDHandler SPI_HSPI #else #define SDHandler SPI #endif @@ -306,7 +307,7 @@ void fsInit() */ void setupSDCard() { -#ifdef HAS_SDCARD +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) concurrency::LockGuard g(spiLock); SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { diff --git a/src/Power.cpp b/src/Power.cpp index 0dec0fc21..9c67977bd 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -76,23 +76,47 @@ static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE; #endif #endif -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if __has_include() INA219Sensor ina219Sensor; -INA226Sensor ina226Sensor; -INA260Sensor ina260Sensor; -INA3221Sensor ina3221Sensor; +#else +NullSensor ina219Sensor; #endif -#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#if __has_include() +INA226Sensor ina226Sensor; +#else +NullSensor ina226Sensor; +#endif + +#if __has_include() +INA260Sensor ina260Sensor; +#else +NullSensor ina260Sensor; +#endif + +#if __has_include() +INA3221Sensor ina3221Sensor; +#else +NullSensor ina3221Sensor; +#endif + +#endif + +#if !MESHTASTIC_EXCLUDE_I2C #include "modules/Telemetry/Sensor/MAX17048Sensor.h" #include extern std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1]; #if HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY) +#if __has_include() MAX17048Sensor max17048Sensor; +#else +NullSensor max17048Sensor; +#endif #endif #endif -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT && !defined(ARCH_PORTDUINO) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT RAK9154Sensor rak9154Sensor; #endif @@ -203,7 +227,7 @@ class AnalogBatteryLevel : public HasBatteryLevel */ virtual int getBatteryPercent() override { -#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) +#if defined(HAS_RAKPROT) && !defined(HAS_PMU) if (hasRAK()) { return rak9154Sensor.getBusBatteryPercent(); } @@ -248,15 +272,13 @@ class AnalogBatteryLevel : public HasBatteryLevel virtual uint16_t getBattVoltage() override { -#if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) && \ - !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (hasRAK()) { return getRAKVoltage(); } #endif -#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(HAS_PMU) && \ - !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (hasINA()) { return getINAVoltage(); } @@ -380,6 +402,20 @@ class AnalogBatteryLevel : public HasBatteryLevel // if we have a integrated device with a battery, we can assume that the battery is always connected #ifdef BATTERY_IMMUTABLE virtual bool isBatteryConnect() override { return true; } +#elif defined(ADC_V) + virtual bool isBatteryConnect() override + { + int lastReading = digitalRead(ADC_V); + // ๅคๆญๅผๆฏๅฆๅๅ + for (int i = 2; i < 500; i++) { + int reading = digitalRead(ADC_V); + if (reading != lastReading) { + return false; // ๆๅๅ๏ผUSBไพ็ต, ๆฒกๆฅ็ตๆฑ + } + } + + return true; + } #else virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif @@ -412,8 +448,7 @@ class AnalogBatteryLevel : public HasBatteryLevel /// we can't be smart enough to say 'full'? virtual bool isCharging() override { -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && \ - !defined(HAS_PMU) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) if (hasRAK()) { return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; } @@ -421,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_PORTDUINO) && !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 @@ -436,6 +470,8 @@ class AnalogBatteryLevel : public HasBatteryLevel return isBatteryConnect() && isVbusIn(); #endif #endif + // by default, we check the battery voltage only + return isVbusIn(); } private: @@ -466,7 +502,7 @@ class AnalogBatteryLevel : public HasBatteryLevel } #endif -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) && !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) { @@ -533,9 +569,6 @@ Power::Power() : OSThread("Power") { statusHandler = {}; low_voltage_counter = 0; -#if defined(ELECROW_ThinkNode_M1) || defined(POWER_CFG) - low_voltage_counter_led3 = 0; -#endif #ifdef DEBUG_HEAP lastheap = memGet.getFreeHeap(); #endif @@ -627,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; @@ -646,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 @@ -716,9 +751,6 @@ void Power::readPowerStatus() const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent); LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); -#if defined(ELECROW_ThinkNode_M1) || defined(POWER_CFG) - power_num = powerStatus2.getBatteryVoltageMv(); -#endif newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP if (lastheap != memGet.getFreeHeap()) { @@ -766,9 +798,6 @@ void Power::readPowerStatus() if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { low_voltage_counter++; -#if defined(ELECROW_ThinkNode_M1) - low_voltage_counter_led3 = low_voltage_counter; -#endif LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); if (low_voltage_counter > 10) { #ifdef ARCH_NRF52 @@ -781,13 +810,7 @@ void Power::readPowerStatus() } } else { low_voltage_counter = 0; -#if defined(ELECROW_ThinkNode_M1) - low_voltage_counter_led3 = low_voltage_counter; -#endif } -#ifdef POWER_CFG - low_voltage_counter_led3 = low_voltage_counter; -#endif } } @@ -831,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 @@ -1136,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 4c4d203c2..3b3f8080d 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -19,14 +19,14 @@ #include "sleep.h" #include "target_specific.h" -#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#if HAS_WIFI && !defined(ARCH_PORTDUINO) || defined(MESHTASTIC_EXCLUDE_WIFI) #include "mesh/wifi/WiFiAPClient.h" #endif #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"); @@ -269,9 +260,6 @@ Fsm powerFSM(&stateBOOT); void PowerFSM_setup() { bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); - bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; bool hasPower = isPowered(); LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); @@ -295,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 @@ -331,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"); @@ -383,6 +371,12 @@ void PowerFSM_setup() // See: https://github.com/meshtastic/firmware/issues/1071 // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated // through the modules + +#if HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) + bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; + if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { powerFSM.add_timed_transition(&stateNB, &stateLS, Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, @@ -400,7 +394,9 @@ void PowerFSM_setup() Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, "Screen-on timeout"); } -#else +#endif // HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) + +#else // (not) ARCH_ESP32 // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, @@ -409,4 +405,4 @@ void PowerFSM_setup() powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state } -#endif \ No newline at end of file +#endif 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 fd4a5b196..cddc7ba7a 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -80,10 +80,46 @@ along with this program. If not, see . // Override user saved region, for producing region-locked builds // #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_SG_923 -// Total system gain in dBm to subtract from Tx power to remain within regulatory ERP limit for non-licensed operators -// This value should be set in variant.h and is PA gain + antenna gain (if system ships with an antenna) -#ifndef REGULATORY_GAIN_LORA -#define REGULATORY_GAIN_LORA 0 +// Total system gain in dBm to subtract from Tx power to remain within regulatory and Tx PA limits +// 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 // ----------------------------------------------------------------------------- @@ -99,8 +135,12 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- - +#if defined(SEEED_WIO_TRACKER_L1) +#define SSD1306_ADDRESS 0x3D +#define USE_SH1106 +#else #define SSD1306_ADDRESS 0x3C +#endif #define ST7567_ADDRESS 0x3F // The SH1106 controller is almost, but not quite, the same as SSD1306 @@ -149,9 +189,12 @@ 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 +#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418 +#define PCT2075_ADDR 0x37 // ----------------------------------------------------------------------------- // ACCELEROMETER @@ -165,11 +208,13 @@ 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 // ----------------------------------------------------------------------------- #define NCP5623_ADDR 0x38 +#define LP5562_ADDR 0x30 // ----------------------------------------------------------------------------- // Security @@ -186,6 +231,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 // ----------------------------------------------------------------------------- @@ -295,6 +349,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 4caa0f730..170bef3a6 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -31,14 +31,26 @@ ScanI2C::FoundDevice ScanI2C::firstRTC() const ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { - ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB}; - return firstOfOrNONE(5, types); + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; + return firstOfOrNONE(6, types); } 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 +{ + ScanI2C::DeviceType types[] = {NCP5623, LP5562}; + return firstOfOrNONE(2, types); } ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const @@ -74,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 5b6bbe629..dd290db98 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -18,7 +18,7 @@ class ScanI2C TDECKKB, BBQ10KB, RAK14004, - PMU_AXP192_AXP2101, + PMU_AXP192_AXP2101, // has the same adress as the TCA8418KB BME_680, BME_280, BMP_280, @@ -49,6 +49,7 @@ class ScanI2C VEML7700, RCWL9620, NCP5623, + LP5562, TSL2591, OPT3001, MLX90632, @@ -60,6 +61,7 @@ class ScanI2C FT6336U, STK8BAXX, ICM20948, + SCD4X, MAX30102, TPS65233, MPR121KB, @@ -69,6 +71,10 @@ class ScanI2C DFROBOT_RAIN, DPS310, LTR390UV, + RAK12035, + TCA8418KB, + PCT2075, + BMM150, } DeviceType; // typedef uint8_t DeviceAddress; @@ -121,6 +127,10 @@ class ScanI2C FoundDevice firstAccelerometer() const; + FoundDevice firstAQI() const; + + FoundDevice firstRGBLED() const; + virtual FoundDevice find(DeviceType) const; virtual bool exists(DeviceType) const; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 4d4649501..861088470 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -10,11 +10,6 @@ #include "meshUtils.h" // vformat #endif -// AXP192 and AXP2101 have the same device address, we just need to identify it in Power.cpp -#ifndef XPOWERS_AXP192_AXP2101_ADDRESS -#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 -#endif - bool in_array(uint8_t *array, int size, uint8_t lookfor) { int i; @@ -218,9 +213,20 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #ifdef HAS_NCP5623 SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); #endif -#ifdef HAS_PMU - SCAN_SIMPLE_CASE(XPOWERS_AXP192_AXP2101_ADDRESS, PMU_AXP192_AXP2101, "AXP192/AXP2101", (uint8_t)addr.address) +#ifdef HAS_LP5562 + SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); #endif + case XPOWERS_AXP192_AXP2101_ADDRESS: + // Do we have the axp2101/192 or the TCA8418 + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1); + if (registerValue == 0x0) { + logFoundDevice("TCA8418", (uint8_t)addr.address); + type = TCA8418KB; + } else { + logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address); + type = PMU_AXP192_AXP2101; + } + break; case BME_ADDR: case BME_ADDR_ALTERNATE: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID @@ -352,7 +358,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) { @@ -417,9 +423,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); @@ -428,6 +446,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(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 @@ -540,4 +561,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 41a2ff980..345c738d6 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -12,6 +12,7 @@ #include "RTC.h" #include "Throttle.h" #include "buzz.h" +#include "concurrency/Periodic.h" #include "meshUtils.h" #include "main.h" // pmu_found @@ -89,6 +90,45 @@ static const char *getGPSPowerStateString(GPSPowerState state) } } +#ifdef PIN_GPS_SWITCH +// If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping +// idefinitely + +int lastState = LOW; +bool firstrun = true; + +static int32_t gpsSwitch() +{ + if (gps) { + int currentState = digitalRead(PIN_GPS_SWITCH); + + // if the switch is set to zero, disable the GPS Thread + if (firstrun) + if (currentState == LOW) + lastState = HIGH; + + if (currentState != lastState) { + if (currentState == LOW) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + if (!firstrun) + playGPSDisableBeep(); + gps->disable(); + } else { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + if (!firstrun) + playGPSEnableBeep(); + gps->enable(); + } + lastState = currentState; + } + firstrun = false; + } + return 1000; +} + +static concurrency::Periodic *gpsPeriodic; +#endif + static void UBXChecksum(uint8_t *message, size_t length) { uint8_t CK_A = 0, CK_B = 0; @@ -530,6 +570,19 @@ bool GPS::setup() // Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s) _serial_gps->write("$PMTK886,1*29\r\n"); delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_PA1010D) { + // PA1010D is used in the Pimoroni GPS board. + + // Enable all constellations. + _serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // Only ask for RMC and GGA (GNRMC and GNGGA) + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS / WAAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); } else if (gnssModel == GNSS_MODEL_MTK_PA1616S) { // PA1616S is used in some GPS breakout boards from Adafruit // PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here. @@ -770,13 +823,6 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) powerState = newState; LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); -#ifdef HELTEC_MESH_NODE_T114 - if ((oldState == GPS_OFF || oldState == GPS_HARDSLEEP) && (newState != GPS_OFF && newState != GPS_HARDSLEEP)) { - _serial_gps->begin(serialSpeeds[speedSelect]); - } else if ((newState == GPS_OFF || newState == GPS_HARDSLEEP) && (oldState != GPS_OFF && oldState != GPS_HARDSLEEP)) { - _serial_gps->end(); - } -#endif switch (newState) { case GPS_ACTIVE: case GPS_IDLE: @@ -1204,9 +1250,11 @@ GnssModel_t GPS::probe(int serialSpeed) // Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms _serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); delay(20); - std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, - {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, - {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}}; + std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, + {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, + {"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B}, + {"L80", "_3339_", GNSS_MODEL_MTK_L76B}}; + PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; @@ -1389,6 +1437,12 @@ GPS *GPS::createGps() pinMode(PIN_GPS_PPS, INPUT); #endif +#ifdef PIN_GPS_SWITCH + // toggle GPS via external GPIO switch + pinMode(PIN_GPS_SWITCH, INPUT); + gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); +#endif + // Currently disabled per issue #525 (TinyGPS++ crash bug) // when fixed upstream, can be un-disabled to enable 3D FixType and PDOP #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS @@ -1482,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/GPS.h b/src/gps/GPS.h index 240cf66d2..9be57017f 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -27,6 +27,7 @@ typedef enum { GNSS_MODEL_UC6580, GNSS_MODEL_UNKNOWN, GNSS_MODEL_MTK_L76B, + GNSS_MODEL_MTK_PA1010D, GNSS_MODEL_MTK_PA1616S, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352, 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/gps/ubx.h b/src/gps/ubx.h index d674bed51..0fe2f01fb 100644 --- a/src/gps/ubx.h +++ b/src/gps/ubx.h @@ -224,7 +224,7 @@ static const uint8_t _message_GSA[] = { 0x00, // Rate for DDC 0x00, // Rate for UART1 0x00, // Rate for UART2 - 0x00, // Rate for USB usefull for native linux + 0x00, // Rate for USB useful for native linux 0x00, // Rate for SPI 0x00 // Reserved }; @@ -258,7 +258,7 @@ static const uint8_t _message_RMC[] = { 0x00, // Rate for DDC 0x01, // Rate for UART1 0x00, // Rate for UART2 - 0x01, // Rate for USB usefull for native linux + 0x01, // Rate for USB useful for native linux 0x00, // Rate for SPI 0x00 // Reserved }; @@ -269,7 +269,7 @@ static const uint8_t _message_GGA[] = { 0x00, // Rate for DDC 0x01, // Rate for UART1 0x00, // Rate for UART2 - 0x01, // Rate for USB, usefull for native linux + 0x01, // Rate for USB, useful for native linux 0x00, // Rate for SPI 0x00 // Reserved }; diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 96c6b44c1..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. @@ -129,9 +133,10 @@ bool EInkDisplay::connect() // backlight power, HIGH is backlight on, LOW is off pinMode(PIN_EINK_EN, OUTPUT); #ifdef ELECROW_ThinkNode_M1 - digitalWrite(PIN_EINK_EN, LOW); -#else + // ThinkNode M1 has a hardware dimmable backlight. Start enabled digitalWrite(PIN_EINK_EN, HIGH); +#else + digitalWrite(PIN_EINK_EN, LOW); #endif #endif @@ -173,14 +178,12 @@ 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); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - // VExt already enabled in setup() // RTC GPIO hold disabled in setup() @@ -217,6 +220,38 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } +#elif defined(HELTEC_MESH_POCKET) + { + spi1 = &SPI1; + spi1->begin(); + // VExt already enabled in setup() + // RTC GPIO hold disabled in setup() + + // Create GxEPD2 objects + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + + // Init GxEPD2 + 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 9c1c8d18e..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) || \ @@ -73,6 +84,10 @@ class EInkDisplay : public OLEDDisplay SPIClass *hspi = NULL; #endif +#if defined(HELTEC_MESH_POCKET) + SPIClass *spi1 = NULL; +#endif + private: // FIXME quick hack to limit drawing to a very slow rate uint32_t lastDrawMsec = 0; 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/NomadStarLED.h b/src/graphics/NomadStarLED.h new file mode 100644 index 000000000..0633a577e --- /dev/null +++ b/src/graphics/NomadStarLED.h @@ -0,0 +1,5 @@ +#ifdef HAS_LP5562 +#include +extern LP5562 rgbw; + +#endif \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e27495f54..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,37 +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 "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 @@ -81,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}; @@ -96,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 @@ -113,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) { @@ -387,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(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. * @@ -1279,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; @@ -1288,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); @@ -1530,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); @@ -1540,11 +313,12 @@ 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); #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) @@ -1556,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, @@ -1588,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 @@ -1610,6 +386,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) #ifdef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_ALDO2); #endif +#ifdef HELTEC_TRACKER_V1_X + uint8_t tft_vext_enabled = digitalRead(VEXT_ENABLE); +#endif #if !ARCH_PORTDUINO dispdev->displayOn(); #endif @@ -1620,6 +399,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) #endif dispdev->displayOn(); +#ifdef HELTEC_TRACKER_V1_X + // If the TFT VEXT power is not enabled, initialize the UI. + if (!tft_vext_enabled) { + ui->init(); + } +#endif #ifdef USE_ST7789 pinMode(VTFT_CTRL, OUTPUT); digitalWrite(VTFT_CTRL, LOW); @@ -1640,7 +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"); #ifdef ELECROW_ThinkNode_M1 if (digitalRead(PIN_EINK_EN) == HIGH) { digitalWrite(PIN_EINK_EN, LOW); @@ -1675,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); @@ -1689,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) +#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(); @@ -1758,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 = @@ -1789,17 +590,20 @@ 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 if (textMessageModule) textMessageObserver.observe(textMessageModule); if (inputBroker) inputObserver.observe(inputBroker); - // Modules can notify screen about refresh + // === Notify modules that support UI events === MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -1836,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 } @@ -1852,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 @@ -1867,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(); @@ -1879,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 (;;) { @@ -1909,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 @@ -1922,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"); @@ -1948,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; @@ -1963,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 @@ -1982,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) @@ -2045,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); } @@ -2081,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: @@ -2207,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; } @@ -2261,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() @@ -2281,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); } @@ -2348,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) { @@ -2428,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) || 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(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) || 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()); @@ -2758,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; @@ -2795,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 @@ -2817,31 +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) { - // Note: only selected admin messages notify this observer - // If you wish to handle a new type of message, you should modify AdminModule.cpp first - - 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 @@ -2851,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 \ No newline at end of file +#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/ScreenFonts.h b/src/graphics/ScreenFonts.h index 079a3e282..3373a47a7 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -65,7 +65,7 @@ #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(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 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 c5187cffc..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) @@ -120,6 +123,303 @@ static void rak14014_tpIntHandle(void) _rak14014_touch_int = true; } +#elif defined(ST72xx_DE) +#include +#include +#include +#include +TCA9534 ioex; + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Bus_RGB _bus_instance; + lgfx::Panel_RGB _panel_instance; + lgfx::Touch_GT911 _touch_instance; + + public: + const uint16_t screenWidth = TFT_WIDTH; + const uint16_t screenHeight = TFT_HEIGHT; + + bool init_impl(bool use_reset, bool use_clear) override + { + ioex.attach(Wire); + ioex.setDeviceAddress(0x18); + ioex.config(1, TCA9534::Config::OUT); + ioex.config(2, TCA9534::Config::OUT); + ioex.config(3, TCA9534::Config::OUT); + ioex.config(4, TCA9534::Config::OUT); + + ioex.output(1, TCA9534::Level::H); + ioex.output(3, TCA9534::Level::L); + ioex.output(4, TCA9534::Level::H); + + pinMode(1, OUTPUT); + digitalWrite(1, LOW); + ioex.output(2, TCA9534::Level::L); + delay(20); + ioex.output(2, TCA9534::Level::H); + delay(100); + pinMode(1, INPUT); + + return LGFX_Device::init_impl(use_reset, use_clear); + } + + LGFX(void) + { + { + auto cfg = _panel_instance.config(); + + cfg.memory_width = screenWidth; + cfg.memory_height = screenHeight; + cfg.panel_width = screenWidth; + cfg.panel_height = screenHeight; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + _panel_instance.config(cfg); + } + + { + auto cfg = _panel_instance.config_detail(); + cfg.use_psram = 0; + _panel_instance.config_detail(cfg); + } + + { + auto cfg = _bus_instance.config(); + cfg.panel = &_panel_instance; + cfg.pin_d0 = ST72xx_B0; // B0 + cfg.pin_d1 = ST72xx_B1; // B1 + cfg.pin_d2 = ST72xx_B2; // B2 + cfg.pin_d3 = ST72xx_B3; // B3 + cfg.pin_d4 = ST72xx_B4; // B4 + cfg.pin_d5 = ST72xx_G0; // G0 + cfg.pin_d6 = ST72xx_G1; // G1 + cfg.pin_d7 = ST72xx_G2; // G2 + cfg.pin_d8 = ST72xx_G3; // G3 + cfg.pin_d9 = ST72xx_G4; // G4 + cfg.pin_d10 = ST72xx_G5; // G5 + cfg.pin_d11 = ST72xx_R0; // R0 + cfg.pin_d12 = ST72xx_R1; // R1 + cfg.pin_d13 = ST72xx_R2; // R2 + cfg.pin_d14 = ST72xx_R3; // R3 + cfg.pin_d15 = ST72xx_R4; // R4 + + cfg.pin_henable = ST72xx_DE; + cfg.pin_vsync = ST72xx_VSYNC; + cfg.pin_hsync = ST72xx_HSYNC; + cfg.pin_pclk = ST72xx_PCLK; + cfg.freq_write = 13000000; + +#ifdef ST7265_HSYNC_POLARITY + cfg.hsync_polarity = ST7265_HSYNC_POLARITY; + cfg.hsync_front_porch = ST7265_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = ST7265_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = ST7265_HSYNC_BACK_PORCH; // 8; + + cfg.vsync_polarity = ST7265_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = ST7265_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = ST7265_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = ST7265_VSYNC_BACK_PORCH; // 8; + + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; +#endif + +#ifdef ST7262_HSYNC_POLARITY + cfg.hsync_polarity = ST7262_HSYNC_POLARITY; + cfg.hsync_front_porch = ST7262_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = ST7262_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = ST7262_HSYNC_BACK_PORCH; // 8; + + cfg.vsync_polarity = ST7262_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = ST7262_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = ST7262_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = ST7262_VSYNC_BACK_PORCH; // 8; + + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; +#endif + +#ifdef SC7277_HSYNC_POLARITY + cfg.hsync_polarity = SC7277_HSYNC_POLARITY; + cfg.hsync_front_porch = SC7277_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = SC7277_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = SC7277_HSYNC_BACK_PORCH; // 8; + + cfg.vsync_polarity = SC7277_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = SC7277_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = SC7277_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = SC7277_VSYNC_BACK_PORCH; // 8; + + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; +#endif + + _bus_instance.config(cfg); + } + _panel_instance.setBus(&_bus_instance); + + { + auto cfg = _touch_instance.config(); + cfg.x_min = 0; + cfg.x_max = TFT_WIDTH; + cfg.y_min = 0; + cfg.y_max = TFT_HEIGHT; + cfg.pin_int = -1; + cfg.pin_rst = -1; + cfg.bus_shared = true; + cfg.offset_rotation = 0; + + cfg.i2c_port = 0; + cfg.i2c_addr = 0x5D; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; + cfg.freq = 400000; + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); + } +}; + +static LGFX *tft = nullptr; + +#elif defined(ILI9488_CS) +#include // Graphics and font library for ILI9488 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ILI9488 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_GT911 _touch_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // configure SPI + cfg.spi_host = ILI9488_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ILI9488_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ILI9488_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ILI9488_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ILI9488_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ILI9488_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) + + // 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) +#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 +#endif + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } + +#ifdef ILI9488_BL + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ILI9488_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + +#if HAS_TOUCHSCREEN + // Configure settings for touch screen control. + { + auto cfg = _touch_instance.config(); + + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = TFT_HEIGHT - 1; + cfg.y_min = 0; + cfg.y_max = TFT_WIDTH - 1; + cfg.pin_int = SCREEN_TOUCH_INT; +#ifdef SCREEN_TOUCH_RST + cfg.pin_rst = SCREEN_TOUCH_RST; +#endif + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + // cfg.freq = 2500000; + + // I2C + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; +#ifdef SCREEN_TOUCH_USE_I2C1 + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; +#else + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; +#endif + // cfg.freq = 400000; + + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } +#endif + + setPanel(&_panel_instance); + } +}; + +static LGFX *tft = nullptr; + #elif defined(ST7789_CS) #include // Graphics and font library for ST7735 driver chip @@ -129,7 +429,7 @@ class LGFX : public lgfx::LGFX_Device lgfx::Bus_SPI _bus_instance; lgfx::Light_PWM _light_instance; #if HAS_TOUCHSCREEN -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(ELECROW) lgfx::Touch_FT5x06 _touch_instance; #else lgfx::Touch_GT911 _touch_instance; @@ -170,17 +470,32 @@ 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. - +#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_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped +#endif + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) @@ -217,6 +532,9 @@ class LGFX : public lgfx::LGFX_Device cfg.y_min = 0; cfg.y_max = TFT_WIDTH - 1; cfg.pin_int = SCREEN_TOUCH_INT; +#ifdef SCREEN_TOUCH_RST + cfg.pin_rst = SCREEN_TOUCH_RST; +#endif cfg.bus_shared = true; cfg.offset_rotation = TFT_OFFSET_ROTATION; // cfg.freq = 2500000; @@ -347,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 @@ -391,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) @@ -640,7 +963,7 @@ static LGFX *tft = nullptr; #endif #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(RAK14014) || defined(HX8357_CS) || (ARCH_PORTDUINO && HAS_SCREEN != 0) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -672,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) @@ -863,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 b757dcf30..beef3a1b2 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -6,22 +6,28 @@ 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) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; @@ -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/DEPG0154BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp deleted file mode 100644 index b8715ed1d..000000000 --- a/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "./DEPG0154BNS800.h" \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h deleted file mode 100644 index 62d42ef57..000000000 --- a/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - -E-Ink display driver - - DEPG0154BNS800 - - Manufacturer: DKE - - Size: 1.54 inch - - Resolution: 152px x 152px - - Flex connector marking: FPC7525 - -*/ - -#pragma once - -#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS -#include "configuration.h" - -#include "./SSD16XX.h" - -namespace NicheGraphics::Drivers -{ -class DEPG0154BNS800 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 152; - static constexpr uint32_t height = 152; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL); - - public: - DEPG0154BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte -}; - -} // namespace NicheGraphics::Drivers -#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp new file mode 100644 index 000000000..2c8df96ed --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp @@ -0,0 +1,132 @@ +#include "./DEPG0213BNS800.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Describes the operation performed when a "fast refresh" is performed +// Source: Modified from GxEPD2 (GxEPD2_213_BN) +static const uint8_t LUT_FAST[] = { + // 1 2 3 + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels) + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels) + 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels) + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM + + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 1. Any pixels changing W2B or B2W. Two medium taps. + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. All pixels. One short tap. + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. Cooldown + 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, 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, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, // +}; + +// How strongly the pixels are pulled and pushed +void DEPG0213BNS800::configVoltages() +{ + switch (updateType) { + case FAST: + // Reference: display datasheet, GxEPD1 + sendCommand(0x03); // Gate voltage + sendData(0x17); // VGH: 20V + + // Reference: display datasheet, GxEPD1 + sendCommand(0x04); // Source voltage + sendData(0x41); // VSH1: 15V + sendData(0x00); // VSH2: NA + sendData(0x32); // VSL: -15V + + // GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard + sendCommand(0x2C); // VCOM voltage + sendData(0x08); // VCOM: -0.2V + break; + + case FULL: + default: + // From OTP memory + break; + } +} + +// Load settings about how the pixels are moved from old state to new state during a refresh +// - manually specified, +// - or with stored values from displays OTP memory +void DEPG0213BNS800::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VSS + + sendCommand(0x32); // Write LUT register from MCU: + sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) + break; + + case FULL: + default: + // From OTP memory + break; + } +} + +// 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 DEPG0213BNS800::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xCF); // Differential, use manually loaded waveform + break; + + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + 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 DEPG0213BNS800::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms, then poll every 50ms + case FULL: + default: + return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms + } +} + +// For this display, we do not need to re-write the new image. +// We're overriding SSD16XX::finalizeUpdate to make this small optimization. +// The display does also work just fine with the generic SSD16XX method, though. +void DEPG0213BNS800::finalizeUpdate() +{ + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place + // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. + if (updateType != FULL) { + // writeNewImage(); // Not required for this display + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } + + // Enter deep-sleep to save a few ยตA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h new file mode 100644 index 000000000..3ce16e473 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - DEPG0213BNS800 + - Manufacturer: DKE + - Size: 2.13 inch + - Resolution: 122px x 250px + - 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. +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class DEPG0213BNS800 : 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: + DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte + + protected: + void configVoltages() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; + void finalizeUpdate() override; // Only overriden for a slight optimization +}; + +} // namespace NicheGraphics::Drivers +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp index 5f3a05670..15134d5ad 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp @@ -116,5 +116,10 @@ void DEPG0290BNS800::finalizeUpdate() sendCommand(0x7F); // Terminate image write without update wait(); } + + // Enter deep-sleep to save a few ยตA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file 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 new file mode 100644 index 000000000..b3a585eb7 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp @@ -0,0 +1,58 @@ +#include "./GDEY0213B74.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void GDEY0213B74::configScanning() +{ + // "Driver output control" + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from +// the controller IC's OTP memory, when the update procedure begins. +void GDEY0213B74::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 GDEY0213B74::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 GDEY0213B74::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h new file mode 100644 index 000000000..1c36f295d --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - GDEY0213B74 + - Manufacturer: Goodisplay + - Size: 2.13 inch + - Resolution: 250px x 122px + - Flex connector marking (not a unique identifier): + - FPC-A002 + - FPC-A005 20.06.15 TRX + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class GDEY0213B74 : 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: + GDEY0213B74() : SSD16XX(width, height, supported) {} + + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/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.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp new file mode 100644 index 000000000..e9a663f80 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp @@ -0,0 +1,68 @@ +#include "./LCMEN2R13ECC1.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void LCMEN2R13ECC1::configScanning() +{ + // "Driver output control" + sendCommand(0x01); + 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 +// - 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 LCMEN2R13ECC1::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x85); + break; + + case FULL: + default: + // From OTP memory + break; + } +} + +void LCMEN2R13ECC1::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 LCMEN2R13ECC1::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 800); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2500); // 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/LCMEN2R13ECC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h new file mode 100644 index 000000000..9fa6eaac9 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h @@ -0,0 +1,40 @@ +/* + +E-Ink display driver + - SSD1680 + - Manufacturer: WISEVAST + - Size: 2.13 inch + - Resolution: 122px x 255px + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class LCMEN2R13ECC1 : 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: + LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte + + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp index c843c4694..fb37544b2 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp @@ -1,9 +1,11 @@ -#include "./LCMEN2R13EFC1.h" - #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +#include "./LCMEN2R13EFC1.h" + #include +#include "SPILock.h" + using namespace NicheGraphics::Drivers; // Look up table: fast refresh, common electrode @@ -42,11 +44,10 @@ static const uint8_t LUT_FAST_BW[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; -// Look up table: fash refresh, pixels which change from white to black +// Look up table: fast refresh, pixels which change from white to black static const uint8_t LUT_FAST_WB[] = { - 0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, // - 0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, // 0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01, // + 0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // @@ -55,7 +56,7 @@ static const uint8_t LUT_FAST_WB[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; -// Look up table: fash refresh, pixels which remain black +// Look up table: fast refresh, pixels which remain black static const uint8_t LUT_FAST_BB[] = { 0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01, // 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, // @@ -151,6 +152,9 @@ void LCMEN213EFC1::reset() void LCMEN213EFC1::sendCommand(const uint8_t command) { + // Take firmware's SPI lock + spiLock->lock(); + spi->beginTransaction(spiSettings); digitalWrite(pin_dc, LOW); // DC pin low indicates command digitalWrite(pin_cs, LOW); @@ -158,6 +162,8 @@ void LCMEN213EFC1::sendCommand(const uint8_t command) digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); + + spiLock->unlock(); } void LCMEN213EFC1::sendData(uint8_t data) @@ -167,6 +173,9 @@ void LCMEN213EFC1::sendData(uint8_t data) void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) { + // Take firmware's SPI lock + spiLock->lock(); + spi->beginTransaction(spiSettings); digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command digitalWrite(pin_cs, LOW); @@ -184,6 +193,8 @@ void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); + + spiLock->unlock(); } void LCMEN213EFC1::configFull() 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/SSD16XX.cpp b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp index 5a5397dbd..d0d030be8 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp @@ -1,6 +1,9 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + #include "./SSD16XX.h" -#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +#include "SPILock.h" + using namespace NicheGraphics::Drivers; SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX) @@ -82,6 +85,9 @@ void SSD16XX::sendCommand(const uint8_t command) if (failed) return; + // Take firmware's SPI lock + spiLock->lock(); + spi->beginTransaction(spiSettings); digitalWrite(pin_dc, LOW); // DC pin low indicates command digitalWrite(pin_cs, LOW); @@ -89,6 +95,8 @@ void SSD16XX::sendCommand(const uint8_t command) digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); + + spiLock->unlock(); } void SSD16XX::sendData(uint8_t data) @@ -103,6 +111,9 @@ void SSD16XX::sendData(const uint8_t *data, uint32_t size) if (failed) return; + // Take firmware's SPI lock + spiLock->lock(); + spi->beginTransaction(spiSettings); digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command digitalWrite(pin_cs, LOW); @@ -119,6 +130,8 @@ void SSD16XX::sendData(const uint8_t *data, uint32_t size) digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); + + spiLock->unlock(); } void SSD16XX::configFullscreen() @@ -242,5 +255,18 @@ void SSD16XX::finalizeUpdate() sendCommand(0x7F); // Terminate image write without update wait(); } + + // Enter deep-sleep to save a few ยตA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); +} + +// Enter a lower-power state +// May only save a few ยตA.. +void SSD16XX::deepSleep() +{ + sendCommand(0x10); // Enter deep sleep + sendData(0x01); // Mode 1: preserve image RAM } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.h b/src/graphics/niche/Drivers/EInk/SSD16XX.h index 799a378c0..3f92818ce 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.h +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.h @@ -44,6 +44,7 @@ class SSD16XX : public EInk virtual void detachFromUpdate(); virtual bool isUpdateDone() override; virtual void finalizeUpdate() override; + virtual void deepSleep(); protected: uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? 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/FreeSans6pt7b.h b/src/graphics/niche/Fonts/FreeSans6pt7b.h index c5bcc32c4..0b3e74b8f 100644 --- a/src/graphics/niche/Fonts/FreeSans6pt7b.h +++ b/src/graphics/niche/Fonts/FreeSans6pt7b.h @@ -1,129 +1,198 @@ #pragma once - const uint8_t FreeSans6pt7bBitmaps[] PROGMEM = { - 0xAA, 0xA8, 0xC0, 0xF6, 0xA0, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, - 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, 0x70, 0x91, 0x23, 0x86, 0x12, 0xA2, 0x4E, 0xF4, 0xE0, - 0x5A, 0xAA, 0x94, 0x89, 0x12, 0x49, 0x29, 0x00, 0x27, 0x50, 0x21, 0x3E, 0x42, 0x00, 0xE0, 0xC0, 0x80, 0x24, 0xA4, 0xA4, 0x80, - 0x74, 0xE3, 0x18, 0xC6, 0x33, 0x70, 0x27, 0x92, 0x49, 0x20, 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, 0x79, 0x30, 0x43, 0x18, - 0x10, 0x71, 0x78, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0x7D, 0x04, 0x1E, 0x44, 0x10, 0x51, 0x78, 0x74, 0x61, 0xE8, 0xC6, - 0x31, 0x70, 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, - 0x78, 0x82, 0x87, 0x01, 0xF1, 0x83, 0x04, 0xF8, 0x3E, 0x07, 0x06, 0x36, 0x40, 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, 0x0F, 0x86, - 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, - 0x42, 0xC3, 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, 0xF9, 0x0A, 0x1C, - 0x18, 0x30, 0x61, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x61, - 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x87, - 0x29, 0x70, 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, - 0xA5, 0x99, 0x99, 0x99, 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, - 0x1E, 0x00, 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1F, 0x00, 0x00, - 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, - 0x08, 0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, 0xC2, 0x42, 0x42, 0x64, 0x24, 0x24, 0x38, 0x18, 0x18, 0xC4, 0x28, - 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, 0x42, 0x66, 0x24, 0x18, 0x18, 0x18, 0x24, 0x46, 0x42, 0xC3, - 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24, - 0x89, 0x20, 0xE9, 0x24, 0x92, 0x49, 0x70, 0x46, 0xA9, 0x10, 0xFE, 0x40, 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, 0x84, 0x3D, 0x18, - 0xC6, 0x31, 0xF0, 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, 0x39, 0x38, 0x7F, 0x81, 0x13, - 0x80, 0x6B, 0xA4, 0x92, 0x40, 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, 0xBF, 0x80, - 0x45, 0x55, 0x57, 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, 0xFF, 0x80, 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, 0xF4, 0x63, 0x18, - 0xC6, 0x20, 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, - 0xF2, 0x49, 0x20, 0x79, 0x24, 0x1C, 0x0B, 0x27, 0x80, 0x5D, 0x24, 0x93, 0x8C, 0x63, 0x18, 0xCF, 0xA0, 0x85, 0x24, 0x92, 0x30, - 0xC3, 0x00, 0x89, 0x2C, 0x96, 0x4A, 0xA5, 0x61, 0x30, 0x98, 0x49, 0x23, 0x08, 0x31, 0x2C, 0x80, 0x89, 0x24, 0x94, 0x50, 0xC2, - 0x08, 0x21, 0x00, 0x78, 0x44, 0x46, 0x23, 0xE0, 0x6A, 0xAA, 0xA9, 0xFF, 0xE0, 0x95, 0x55, 0x56, 0x66, 0x60}; + /* ' ' 0x20 */ + /* '!' 0x21 */ 0xFE, 0x80, + /* '"' 0x22 */ 0xB6, 0x80, + /* '#' 0x23 */ 0x24, 0x49, 0xF9, 0x42, 0x9F, 0x92, 0x28, + /* '$' 0x24 */ 0x23, 0xAB, 0x5A, 0x38, 0xB5, 0xAB, 0x88, + /* '%' 0x25 */ 0x71, 0x22, 0x88, 0xA2, 0x30, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0x80, + /* '&' 0x26 */ 0x31, 0x24, 0x8C, 0x72, 0x58, 0xA3, 0x74, + /* ''' 0x27 */ 0xE0, + /* '(' 0x28 */ 0x5A, 0xAA, 0x94, + /* ')' 0x29 */ 0x89, 0x12, 0x49, 0x49, 0x00, + /* '*' 0x2A */ 0x5E, 0x80, + /* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, + /* ',' 0x2C */ 0xE0, + /* '-' 0x2D */ 0xE0, + /* '.' 0x2E */ 0x80, + /* '/' 0x2F */ 0x25, 0x24, 0xA4, 0x80, + /* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, + /* '1' 0x31 */ 0x5D, 0x55, 0x40, + /* '2' 0x32 */ 0x74, 0x42, 0x11, 0x11, 0x10, 0xF8, + /* '3' 0x33 */ 0x74, 0x42, 0x13, 0x04, 0x31, 0x70, + /* '4' 0x34 */ 0x11, 0x8C, 0xA9, 0x4B, 0xE2, 0x10, + /* '5' 0x35 */ 0x7D, 0x04, 0x1E, 0x4C, 0x10, 0x63, 0x78, + /* '6' 0x36 */ 0x72, 0x61, 0xE8, 0xC6, 0x39, 0x70, + /* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, + /* '8' 0x38 */ 0x7A, 0x18, 0x61, 0x7A, 0x18, 0x61, 0x78, + /* '9' 0x39 */ 0x7B, 0x28, 0x61, 0xCD, 0xD0, 0x62, 0x70, + /* ':' 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, 0x06, 0x19, 0x3B, 0xC4, 0x99, 0x13, 0x22, 0x64, 0x96, 0x6E, 0x40, 0x04, 0x00, 0x7C, 0x00, + /* 'A' 0x41 */ 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, + /* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xF8, + /* 'C' 0x43 */ 0x3E, 0x41, 0x80, 0x80, 0x80, 0x80, 0x81, 0x43, 0x3E, + /* 'D' 0x44 */ 0xF9, 0x0A, 0x0C, 0x18, 0x30, 0x60, 0xC2, 0xF8, + /* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0xFC, + /* 'F' 0x46 */ 0xFC, 0x21, 0x0F, 0xC2, 0x10, 0x80, + /* 'G' 0x47 */ 0x3E, 0x41, 0x80, 0x80, 0x87, 0x81, 0xC1, 0x43, 0x3D, + /* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, + /* 'I' 0x49 */ 0xFF, 0x80, + /* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x86, 0x31, 0x70, + /* 'K' 0x4B */ 0x86, 0x29, 0x28, 0xD2, 0x48, 0xA1, 0x84, + /* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, + /* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, + /* 'N' 0x4E */ 0xC3, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, + /* 'O' 0x4F */ 0x3E, 0x31, 0xB0, 0x70, 0x18, 0x0C, 0x07, 0x06, 0xC6, 0x3E, 0x00, + /* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, + /* 'Q' 0x51 */ 0x3E, 0x31, 0xB0, 0x70, 0x18, 0x0C, 0x07, 0x06, 0xC6, 0x3F, 0x00, 0x40, + /* 'R' 0x52 */ 0xF9, 0x0A, 0x14, 0x2F, 0x90, 0xA1, 0x42, 0x86, + /* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x71, 0x78, + /* 'T' 0x54 */ 0xFC, 0x41, 0x04, 0x10, 0x41, 0x04, 0x10, + /* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE3, 0x7C, + /* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, + /* 'W' 0x57 */ 0x84, 0x38, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0xD2, 0x8C, 0x61, 0x8C, 0x31, 0x80, + /* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, + /* 'Y' 0x59 */ 0x82, 0x89, 0x11, 0x43, 0x82, 0x04, 0x08, 0x10, + /* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, + /* '[' 0x5B */ 0xEA, 0xAA, 0xAB, + /* '\' 0x5C */ 0x92, 0x24, 0x91, 0x20, + /* ']' 0x5D */ 0xD5, 0x55, 0x57, + /* '^' 0x5E */ 0x46, 0xA9, 0x10, + /* '_' 0x5F */ 0xFE, + /* '`' 0x60 */ 0x80, + /* 'a' 0x61 */ 0x79, 0x08, 0x11, 0xEC, 0x51, 0x9D, 0x80, + /* 'b' 0x62 */ 0x84, 0x3D, 0xB8, 0xC6, 0x3B, 0xF0, + /* 'c' 0x63 */ 0x7B, 0x18, 0x20, 0x83, 0x17, 0x80, + /* 'd' 0x64 */ 0x04, 0x17, 0xF3, 0x86, 0x18, 0x73, 0x74, + /* 'e' 0x65 */ 0x7B, 0x38, 0x7F, 0x83, 0x17, 0x80, + /* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, + /* 'g' 0x67 */ 0x77, 0x38, 0x61, 0x87, 0x37, 0x41, 0x8D, 0xE0, + /* 'h' 0x68 */ 0x84, 0x2D, 0x98, 0xC6, 0x31, 0x88, + /* 'i' 0x69 */ 0xBF, 0x80, + /* 'j' 0x6A */ 0x45, 0x55, 0x57, + /* 'k' 0x6B */ 0x84, 0x25, 0x6E, 0x72, 0x52, 0x88, + /* 'l' 0x6C */ 0xFF, 0x80, + /* 'm' 0x6D */ 0xFF, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, + /* 'n' 0x6E */ 0xB6, 0x63, 0x18, 0xC6, 0x20, + /* 'o' 0x6F */ 0x7B, 0x38, 0x61, 0x87, 0x37, 0x80, + /* 'p' 0x70 */ 0xF6, 0xE3, 0x18, 0xEF, 0xD0, 0x80, + /* 'q' 0x71 */ 0x77, 0x38, 0x61, 0x87, 0x37, 0x41, 0x04, + /* 'r' 0x72 */ 0xBA, 0x49, 0x20, + /* 's' 0x73 */ 0x69, 0x8E, 0x19, 0x60, + /* 't' 0x74 */ 0x5D, 0x24, 0x93, + /* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCD, 0xA0, + /* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, + /* 'w' 0x77 */ 0x89, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, + /* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, + /* 'y' 0x79 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x08, 0x21, 0x80, + /* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, + /* '{' 0x7B */ 0x69, 0x25, 0xB2, 0x49, 0x30, + /* '|' 0x7C */ 0xFF, 0xE0, + /* '}' 0x7D */ 0xC9, 0x24, 0xDA, 0x49, 0x60, + /* '~' 0x7E */ 0x66, 0x70, +}; -const GFXglyph FreeSans6pt7bGlyphs[] PROGMEM = {{0, 0, 0, 3, 0, 1}, // 0x20 ' ' - {0, 2, 9, 4, 1, -8}, // 0x21 '!' - {3, 4, 3, 4, 0, -8}, // 0x22 '"' - {5, 7, 8, 7, 0, -7}, // 0x23 '#' - {12, 6, 11, 7, 0, -9}, // 0x24 '$' - {21, 10, 9, 11, 0, -8}, // 0x25 '%' - {33, 7, 9, 8, 1, -8}, // 0x26 '&' - {41, 1, 3, 2, 1, -8}, // 0x27 ''' - {42, 2, 11, 4, 1, -8}, // 0x28 '(' - {45, 3, 11, 4, 0, -8}, // 0x29 ')' - {50, 4, 3, 5, 0, -8}, // 0x2A '*' - {52, 5, 5, 7, 1, -4}, // 0x2B '+' - {56, 1, 3, 3, 1, 0}, // 0x2C ',' - {57, 2, 1, 4, 1, -3}, // 0x2D '-' - {58, 1, 1, 3, 1, 0}, // 0x2E '.' - {59, 3, 9, 3, 0, -8}, // 0x2F '/' - {63, 5, 9, 7, 1, -8}, // 0x30 '0' - {69, 3, 9, 7, 1, -8}, // 0x31 '1' - {73, 6, 9, 7, 0, -8}, // 0x32 '2' - {80, 6, 9, 7, 0, -8}, // 0x33 '3' - {87, 6, 9, 7, 0, -8}, // 0x34 '4' - {94, 6, 9, 7, 0, -8}, // 0x35 '5' - {101, 5, 9, 7, 1, -8}, // 0x36 '6' - {107, 5, 9, 7, 1, -8}, // 0x37 '7' - {113, 6, 9, 7, 0, -8}, // 0x38 '8' - {120, 6, 9, 7, 0, -8}, // 0x39 '9' - {127, 1, 7, 3, 1, -6}, // 0x3A ':' - {128, 1, 8, 3, 1, -5}, // 0x3B ';' - {129, 5, 6, 7, 1, -5}, // 0x3C '<' - {133, 5, 3, 7, 1, -3}, // 0x3D '=' - {135, 5, 6, 7, 1, -5}, // 0x3E '>' - {139, 5, 9, 7, 1, -8}, // 0x3F '?' - {145, 11, 11, 12, 0, -8}, // 0x40 '@' - {161, 8, 9, 8, 0, -8}, // 0x41 'A' - {170, 6, 9, 8, 1, -8}, // 0x42 'B' - {177, 8, 9, 9, 0, -8}, // 0x43 'C' - {186, 7, 9, 8, 1, -8}, // 0x44 'D' - {194, 6, 9, 8, 1, -8}, // 0x45 'E' - {201, 6, 9, 7, 1, -8}, // 0x46 'F' - {208, 8, 9, 9, 0, -8}, // 0x47 'G' - {217, 7, 9, 9, 1, -8}, // 0x48 'H' - {225, 1, 9, 3, 1, -8}, // 0x49 'I' - {227, 5, 9, 6, 0, -8}, // 0x4A 'J' - {233, 7, 9, 8, 1, -8}, // 0x4B 'K' - {241, 5, 9, 7, 1, -8}, // 0x4C 'L' - {247, 8, 9, 10, 1, -8}, // 0x4D 'M' - {256, 7, 9, 9, 1, -8}, // 0x4E 'N' - {264, 9, 9, 9, 0, -8}, // 0x4F 'O' - {275, 6, 9, 8, 1, -8}, // 0x50 'P' - {282, 9, 10, 9, 0, -8}, // 0x51 'Q' - {294, 7, 9, 9, 1, -8}, // 0x52 'R' - {302, 6, 9, 8, 1, -8}, // 0x53 'S' - {309, 7, 9, 8, 0, -8}, // 0x54 'T' - {317, 7, 9, 9, 1, -8}, // 0x55 'U' - {325, 8, 9, 8, 0, -8}, // 0x56 'V' - {334, 11, 9, 11, 0, -8}, // 0x57 'W' - {347, 8, 9, 8, 0, -8}, // 0x58 'X' - {356, 8, 9, 8, 0, -8}, // 0x59 'Y' - {365, 7, 9, 7, 0, -8}, // 0x5A 'Z' - {373, 2, 12, 3, 1, -8}, // 0x5B '[' - {376, 3, 9, 3, 0, -8}, // 0x5C '\' - {380, 3, 12, 3, 0, -8}, // 0x5D ']' - {385, 4, 5, 6, 1, -8}, // 0x5E '^' - {388, 7, 1, 7, 0, 2}, // 0x5F '_' - {389, 3, 1, 3, 0, -8}, // 0x60 '`' - {390, 6, 7, 7, 0, -6}, // 0x61 'a' - {396, 5, 9, 7, 1, -8}, // 0x62 'b' - {402, 6, 7, 6, 0, -6}, // 0x63 'c' - {408, 6, 9, 7, 0, -8}, // 0x64 'd' - {415, 6, 7, 6, 0, -6}, // 0x65 'e' - {421, 3, 9, 3, 0, -8}, // 0x66 'f' - {425, 6, 10, 7, 0, -6}, // 0x67 'g' - {433, 5, 9, 6, 1, -8}, // 0x68 'h' - {439, 1, 9, 3, 1, -8}, // 0x69 'i' - {441, 2, 12, 3, 0, -8}, // 0x6A 'j' - {444, 5, 9, 6, 1, -8}, // 0x6B 'k' - {450, 1, 9, 3, 1, -8}, // 0x6C 'l' - {452, 8, 7, 10, 1, -6}, // 0x6D 'm' - {459, 5, 7, 6, 1, -6}, // 0x6E 'n' - {464, 6, 7, 6, 0, -6}, // 0x6F 'o' - {470, 5, 9, 7, 1, -6}, // 0x70 'p' - {476, 6, 9, 7, 0, -6}, // 0x71 'q' - {483, 3, 7, 4, 1, -6}, // 0x72 'r' - {486, 6, 7, 6, 0, -6}, // 0x73 's' - {492, 3, 8, 3, 0, -7}, // 0x74 't' - {495, 5, 7, 6, 1, -6}, // 0x75 'u' - {500, 6, 7, 6, 0, -6}, // 0x76 'v' - {506, 9, 7, 9, 0, -6}, // 0x77 'w' - {514, 6, 7, 6, 0, -6}, // 0x78 'x' - {520, 6, 10, 6, 0, -6}, // 0x79 'y' - {528, 5, 7, 6, 0, -6}, // 0x7A 'z' - {533, 2, 12, 4, 1, -8}, // 0x7B '{' - {536, 1, 11, 3, 1, -8}, // 0x7C '|' - {538, 2, 12, 4, 1, -8}, // 0x7D '}' - {541, 6, 2, 6, 0, -4}}; // 0x7E '~' +const GFXglyph FreeSans6pt7bGlyphs[] 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, 5, 11, 7, 1, -9}, + /* '%' 0x25 */ {18, 10, 9, 11, 0, -8}, + /* '&' 0x26 */ {30, 6, 9, 8, 1, -8}, + /* ''' 0x27 */ {37, 1, 3, 2, 1, -8}, + /* '(' 0x28 */ {38, 2, 11, 4, 1, -8}, + /* ')' 0x29 */ {41, 3, 11, 4, 0, -8}, + /* '*' 0x2A */ {46, 3, 3, 5, 1, -8}, + /* '+' 0x2B */ {48, 5, 5, 7, 1, -4}, + /* ',' 0x2C */ {52, 1, 3, 3, 1, 0}, + /* '-' 0x2D */ {53, 3, 1, 4, 1, -3}, + /* '.' 0x2E */ {54, 1, 1, 3, 1, 0}, + /* '/' 0x2F */ {55, 3, 9, 3, 0, -8}, + /* '0' 0x30 */ {59, 5, 9, 7, 1, -8}, + /* '1' 0x31 */ {65, 2, 9, 7, 2, -8}, + /* '2' 0x32 */ {68, 5, 9, 7, 1, -8}, + /* '3' 0x33 */ {74, 5, 9, 7, 1, -8}, + /* '4' 0x34 */ {80, 5, 9, 7, 1, -8}, + /* '5' 0x35 */ {86, 6, 9, 7, 0, -8}, + /* '6' 0x36 */ {93, 5, 9, 7, 1, -8}, + /* '7' 0x37 */ {99, 5, 9, 7, 1, -8}, + /* '8' 0x38 */ {105, 6, 9, 7, 0, -8}, + /* '9' 0x39 */ {112, 6, 9, 7, 0, -8}, + /* ':' 0x3A */ {119, 1, 7, 3, 1, -6}, + /* ';' 0x3B */ {120, 1, 8, 3, 1, -5}, + /* '<' 0x3C */ {121, 5, 5, 7, 1, -4}, + /* '=' 0x3D */ {125, 5, 3, 7, 1, -3}, + /* '>' 0x3E */ {127, 5, 5, 7, 1, -4}, + /* '?' 0x3F */ {131, 5, 9, 7, 1, -8}, + /* '@' 0x40 */ {137, 11, 11, 12, 0, -8}, + /* 'A' 0x41 */ {153, 8, 9, 8, 0, -8}, + /* 'B' 0x42 */ {162, 6, 9, 8, 1, -8}, + /* 'C' 0x43 */ {169, 8, 9, 9, 0, -8}, + /* 'D' 0x44 */ {178, 7, 9, 8, 1, -8}, + /* 'E' 0x45 */ {186, 6, 9, 8, 1, -8}, + /* 'F' 0x46 */ {193, 5, 9, 7, 1, -8}, + /* 'G' 0x47 */ {199, 8, 9, 9, 0, -8}, + /* 'H' 0x48 */ {208, 7, 9, 9, 1, -8}, + /* 'I' 0x49 */ {216, 1, 9, 3, 1, -8}, + /* 'J' 0x4A */ {218, 5, 9, 6, 0, -8}, + /* 'K' 0x4B */ {224, 6, 9, 8, 1, -8}, + /* 'L' 0x4C */ {231, 5, 9, 7, 1, -8}, + /* 'M' 0x4D */ {237, 8, 9, 10, 1, -8}, + /* 'N' 0x4E */ {246, 7, 9, 9, 1, -8}, + /* 'O' 0x4F */ {254, 9, 9, 9, 0, -8}, + /* 'P' 0x50 */ {265, 6, 9, 8, 1, -8}, + /* 'Q' 0x51 */ {272, 9, 10, 9, 0, -8}, + /* 'R' 0x52 */ {284, 7, 9, 9, 1, -8}, + /* 'S' 0x53 */ {292, 6, 9, 8, 1, -8}, + /* 'T' 0x54 */ {299, 6, 9, 8, 0, -8}, + /* 'U' 0x55 */ {306, 7, 9, 9, 1, -8}, + /* 'V' 0x56 */ {314, 7, 9, 8, 0, -8}, + /* 'W' 0x57 */ {322, 11, 9, 11, 0, -8}, + /* 'X' 0x58 */ {335, 6, 9, 8, 1, -8}, + /* 'Y' 0x59 */ {342, 7, 9, 8, 1, -8}, + /* 'Z' 0x5A */ {350, 7, 9, 7, 0, -8}, + /* '[' 0x5B */ {358, 2, 12, 3, 1, -8}, + /* '\' 0x5C */ {361, 3, 9, 3, 0, -8}, + /* ']' 0x5D */ {365, 2, 12, 3, 0, -8}, + /* '^' 0x5E */ {368, 4, 5, 6, 1, -8}, + /* '_' 0x5F */ {371, 7, 1, 7, 0, 2}, + /* '`' 0x60 */ {372, 1, 1, 3, 1, -8}, + /* 'a' 0x61 */ {373, 7, 7, 7, 0, -6}, + /* 'b' 0x62 */ {380, 5, 9, 7, 1, -8}, + /* 'c' 0x63 */ {386, 6, 7, 6, 0, -6}, + /* 'd' 0x64 */ {392, 6, 9, 7, 0, -8}, + /* 'e' 0x65 */ {399, 6, 7, 6, 0, -6}, + /* 'f' 0x66 */ {405, 3, 9, 3, 0, -8}, + /* 'g' 0x67 */ {409, 6, 10, 7, 0, -6}, + /* 'h' 0x68 */ {417, 5, 9, 6, 1, -8}, + /* 'i' 0x69 */ {423, 1, 9, 3, 1, -8}, + /* 'j' 0x6A */ {425, 2, 12, 3, 0, -8}, + /* 'k' 0x6B */ {428, 5, 9, 6, 1, -8}, + /* 'l' 0x6C */ {434, 1, 9, 3, 1, -8}, + /* 'm' 0x6D */ {436, 8, 7, 10, 1, -6}, + /* 'n' 0x6E */ {443, 5, 7, 6, 1, -6}, + /* 'o' 0x6F */ {448, 6, 7, 6, 0, -6}, + /* 'p' 0x70 */ {454, 5, 9, 7, 1, -6}, + /* 'q' 0x71 */ {460, 6, 9, 7, 0, -6}, + /* 'r' 0x72 */ {467, 3, 7, 4, 1, -6}, + /* 's' 0x73 */ {470, 4, 7, 6, 1, -6}, + /* 't' 0x74 */ {474, 3, 8, 3, 0, -7}, + /* 'u' 0x75 */ {477, 5, 7, 6, 1, -6}, + /* 'v' 0x76 */ {482, 6, 7, 6, 0, -6}, + /* 'w' 0x77 */ {488, 8, 7, 9, 0, -6}, + /* 'x' 0x78 */ {495, 5, 7, 6, 0, -6}, + /* 'y' 0x79 */ {500, 6, 10, 6, 0, -6}, + /* 'z' 0x7A */ {508, 5, 7, 6, 0, -6}, + /* '{' 0x7B */ {513, 3, 12, 4, 0, -8}, + /* '|' 0x7C */ {518, 1, 11, 3, 1, -8}, + /* '}' 0x7D */ {520, 3, 12, 4, 1, -8}, + /* '~' 0x7E */ {525, 6, 2, 6, 0, -4}, +}; const GFXfont FreeSans6pt7b PROGMEM = {(uint8_t *)FreeSans6pt7bBitmaps, (GFXglyph *)FreeSans6pt7bGlyphs, 0x20, 0x7E, 14}; - -// Approx. 1215 bytes diff --git a/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h b/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h deleted file mode 100644 index d222cd1c3..000000000 --- a/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h +++ /dev/null @@ -1,302 +0,0 @@ -/* - -Uses Windows-1251 encoding to map translingual Cyrillic characters to range between (uint8_t)127 and (uint8_t)255 -https://en.wikipedia.org/wiki/Windows-1251 - -Cyrillic characters present to the firmware as UTF8. -A NicheGraphics implementation needs to identify these, and substitute the appropriate Windows-1251 char value. - -*/ - -#pragma once - -const uint8_t FreeSans6pt8bCyrillicBitmaps[] PROGMEM = { - 0xFF, 0xA0, 0xC0, 0xFF, 0xA0, 0xC0, 0xB6, 0x80, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x31, 0x75, 0x54, 0x78, 0x79, 0x75, - 0x7C, 0x41, 0x00, 0x01, 0x1C, 0x49, 0x22, 0x50, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0xC0, 0x21, 0x44, 0x94, 0x62, 0x59, 0xE2, - 0xF4, 0xE0, 0x6A, 0xAA, 0x90, 0x48, 0x92, 0x49, 0x4A, 0x00, 0x5D, 0x40, 0x21, 0x09, 0xF2, 0x10, 0xE0, 0xC0, 0x80, 0x25, 0x25, - 0x24, 0x26, 0xA3, 0x18, 0xC6, 0x31, 0xF0, 0x27, 0x92, 0x49, 0x20, 0x11, 0xB4, 0x41, 0x0C, 0xC6, 0x10, 0xFC, 0x26, 0xA2, 0x13, - 0x04, 0x31, 0xF0, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0xFF, 0xE1, 0x4D, 0x84, 0x31, 0xF0, 0x26, 0xE3, 0x0F, 0x46, 0x31, - 0xF0, 0xFF, 0xC4, 0x22, 0x11, 0x08, 0x40, 0x11, 0xA4, 0x51, 0x39, 0x1C, 0x51, 0x78, 0x11, 0xA4, 0x71, 0x45, 0xF0, 0x51, 0x78, - 0xC0, 0x30, 0xC0, 0x36, 0x1F, 0x20, 0xE0, 0x80, 0xF8, 0x3E, 0xC1, 0xC2, 0xE8, 0x00, 0x74, 0x62, 0x11, 0x10, 0x80, 0x20, 0x0F, - 0x06, 0x18, 0x81, 0xA7, 0xD4, 0x93, 0x22, 0x64, 0x4A, 0x7E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x1C, 0x24, 0x24, 0x7E, - 0x42, 0x42, 0xC3, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xF9, 0x1A, 0x1C, - 0x18, 0x30, 0x60, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x3C, 0x46, - 0x82, 0x80, 0x8F, 0x81, 0x83, 0xC3, 0x7D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x86, - 0x31, 0x78, 0x87, 0x1A, 0x65, 0x8F, 0x1A, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, - 0xA5, 0x99, 0x99, 0x99, 0x83, 0x87, 0x8D, 0x19, 0x32, 0x62, 0xC3, 0x86, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC2, - 0x3E, 0x00, 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x3F, 0x00, 0xFD, - 0x0E, 0x0C, 0x1F, 0xD0, 0xA0, 0xC1, 0x82, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, - 0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0x7C, 0xC3, 0x42, 0x42, 0x26, 0x24, 0x24, 0x14, 0x18, 0x18, 0xC4, 0x28, 0xC5, - 0x39, 0xA5, 0x24, 0xA4, 0x52, 0x8C, 0x71, 0x8C, 0x30, 0x80, 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, 0xC3, 0x42, 0x26, 0x24, - 0x18, 0x18, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x10, 0x41, 0x06, 0x08, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24, 0x89, 0x20, 0xED, - 0xB6, 0xDB, 0x6D, 0xF0, 0x46, 0xAA, 0x90, 0xFC, 0x90, 0xFC, 0x4F, 0x98, 0xFC, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0xF0, 0x79, 0x18, - 0x20, 0x45, 0xE0, 0x04, 0x10, 0x5F, 0xC6, 0x18, 0x51, 0x7C, 0xFC, 0x7F, 0x08, 0xF8, 0x29, 0x74, 0x92, 0x40, 0x7D, 0x18, 0x61, - 0x45, 0xF0, 0x52, 0x30, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0x88, 0xDF, 0x80, 0x51, 0x55, 0x56, 0x84, 0x21, 0x2A, 0x72, 0x92, 0x98, - 0xFF, 0x80, 0xFF, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFC, 0x63, 0x18, 0xC4, 0x79, 0x18, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xFA, - 0x10, 0x80, 0x7D, 0x18, 0x61, 0x45, 0xF0, 0x41, 0x04, 0xF2, 0x49, 0x00, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0x4B, 0xA4, 0x93, 0x8C, - 0x63, 0x18, 0xFC, 0xCD, 0x24, 0x94, 0x30, 0xC0, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, 0x96, 0x66, 0x99, 0xCA, 0x52, 0x63, 0x18, - 0x84, 0x40, 0x78, 0xC4, 0x44, 0x7C, 0x6A, 0xAA, 0xA9, 0xFF, 0xF0, 0xC9, 0x24, 0x4A, 0x49, 0x40, 0xE8, 0xC0, 0xFE, 0x18, 0x61, - 0x86, 0x18, 0x61, 0xFC, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, 0x10, 0x8F, 0xE0, 0x82, - 0x08, 0x20, 0x82, 0x08, 0x00, 0x64, 0x0F, 0x88, 0x88, 0x80, 0x3D, 0x0C, 0x2E, 0xF9, 0x04, 0x0F, 0x7C, 0x08, 0x81, 0x10, 0x22, - 0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0, - 0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0x83, - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, 0x78, 0x24, 0x13, - 0xC9, 0x14, 0x8E, 0x7C, 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, 0x60, 0x9A, 0xCC, 0xA9, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, - 0x44, 0x8C, 0x63, 0x18, 0xFC, 0x80, 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, 0x70, 0x22, 0x95, 0xA8, 0xC4, - 0x23, 0x10, 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, 0x28, 0x0F, 0xE0, 0x82, 0x0F, - 0xE0, 0x82, 0x0F, 0xC0, 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, 0x51, 0x55, 0x56, 0xA1, 0x24, 0x92, 0x49, 0x00, 0xFF, - 0x80, 0xDF, 0x80, 0x27, 0xC9, 0x24, 0x8A, 0x28, 0xA2, 0x8B, 0xF8, 0x20, 0x80, 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, 0x88, - 0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x89, 0x80, 0x79, 0x1F, 0x30, 0x45, 0xE0, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, - 0x7C, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0xB4, 0x24, 0x92, 0x40, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, 0xFE, 0x08, - 0x20, 0xFE, 0x18, 0x61, 0xFC, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, 0x1F, 0x08, - 0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0x88, 0xA4, 0x9A, - 0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68, - 0xE1, 0xC2, 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, 0x3E, 0x44, - 0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, - 0xC1, 0x82, 0x3C, 0x46, 0x83, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, 0xFA, 0x18, - 0x61, 0xFE, 0x08, 0x20, 0x80, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, - 0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, 0x08, 0x1E, 0x32, 0xD1, 0x38, 0x8C, 0x4F, 0x2C, 0xFC, 0x08, 0x00, 0x87, 0x34, - 0x8C, 0x30, 0xC4, 0xB3, 0x84, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, 0x8E, 0x38, 0xE3, 0x8D, 0xF0, - 0xC3, 0x0C, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF, - 0x80, 0x40, 0x20, 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC, - 0x2E, 0x17, 0x0B, 0xF9, 0x80, 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, 0x87, - 0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, 0x79, 0x11, 0xD9, - 0xCD, 0xD0, 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, 0xF4, 0xBD, 0x29, 0xF8, 0xF8, 0x88, 0x88, 0x3C, 0x48, 0x91, 0x22, 0x5F, - 0xE0, 0x80, 0x79, 0x1F, 0xF0, 0x45, 0xE0, 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, 0x78, 0x23, 0x82, 0xCD, 0xE0, 0x9C, 0xEB, 0x5C, - 0xC4, 0x70, 0x27, 0x3A, 0xD7, 0x31, 0x9A, 0xCC, 0xA9, 0x7A, 0x52, 0x94, 0xE4, 0x8F, 0x3D, 0x6D, 0xA6, 0x90, 0x8C, 0x7F, 0x18, - 0xC4, 0x79, 0x1C, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xC4, 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, 0x79, 0x1C, 0x30, 0x45, 0xE0, - 0xF9, 0x08, 0x42, 0x10, 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04, - 0x01, 0x00, 0x40, 0x4B, 0x8C, 0x65, 0xE4, 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, 0x99, 0x97, 0x11, 0x96, 0x59, 0x65, 0x97, 0xF0, - 0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, 0x86, 0x1F, 0x63, 0x8F, 0xD0, 0x84, 0x3D, 0x18, - 0xF8, 0xF4, 0xDE, 0x19, 0xF8, 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, 0xFC, 0x7E, 0xD4, 0xC4, -}; - -const GFXglyph FreeSans6pt8bCyrillicGlyphs[] PROGMEM = { - {0, 0, 0, 3, 0, 0}, // 0x20 ' ' - {3, 2, 9, 3, 1, -8}, // 0x21 '!' - {6, 3, 3, 4, 1, -8}, // 0x22 '"' - {8, 7, 8, 7, 0, -7}, // 0x23 '#' - {15, 6, 11, 7, 0, -8}, // 0x24 '$' - {24, 10, 9, 11, 0, -8}, // 0x25 '%' - {36, 6, 9, 8, 1, -8}, // 0x26 '&' - {43, 1, 3, 2, 1, -8}, // 0x27 ''' - {44, 2, 10, 4, 1, -7}, // 0x28 '(' - {47, 3, 11, 4, 0, -7}, // 0x29 ')' - {52, 3, 4, 5, 1, -8}, // 0x2A '*' - {54, 5, 6, 7, 1, -5}, // 0x2B '+' - {58, 1, 3, 3, 1, 0}, // 0x2C ',' - {59, 2, 1, 4, 1, -3}, // 0x2D '-' - {60, 1, 1, 3, 1, 0}, // 0x2E '.' - {61, 3, 8, 3, 0, -7}, // 0x2F '/' - {64, 5, 9, 7, 1, -8}, // 0x30 '0' - {70, 3, 9, 7, 1, -8}, // 0x31 '1' - {74, 6, 9, 7, 0, -8}, // 0x32 '2' - {81, 5, 9, 7, 1, -8}, // 0x33 '3' - {87, 6, 9, 7, 0, -8}, // 0x34 '4' - {94, 5, 9, 7, 1, -8}, // 0x35 '5' - {100, 5, 9, 7, 1, -8}, // 0x36 '6' - {106, 5, 9, 7, 1, -8}, // 0x37 '7' - {112, 6, 9, 7, 0, -8}, // 0x38 '8' - {119, 6, 9, 7, 0, -8}, // 0x39 '9' - {126, 2, 6, 3, 1, -5}, // 0x3A ':' - {128, 2, 8, 3, 1, -5}, // 0x3B ';' - {130, 5, 5, 7, 1, -4}, // 0x3C '<' - {134, 5, 3, 7, 1, -3}, // 0x3D '=' - {136, 5, 5, 7, 1, -4}, // 0x3E '>' - {140, 5, 9, 7, 1, -8}, // 0x3F '?' - {146, 11, 11, 12, 0, -8}, // 0x40 '@' - {162, 8, 9, 8, 0, -8}, // 0x41 'A' - {171, 6, 9, 8, 1, -8}, // 0x42 'B' - {178, 7, 9, 9, 1, -8}, // 0x43 'C' - {186, 7, 9, 9, 1, -8}, // 0x44 'D' - {194, 6, 9, 8, 1, -8}, // 0x45 'E' - {201, 6, 9, 7, 1, -8}, // 0x46 'F' - {208, 8, 9, 9, 1, -8}, // 0x47 'G' - {217, 7, 9, 9, 1, -8}, // 0x48 'H' - {225, 1, 9, 3, 1, -8}, // 0x49 'I' - {227, 5, 9, 6, 0, -8}, // 0x4A 'J' - {233, 7, 9, 8, 1, -8}, // 0x4B 'K' - {241, 5, 9, 7, 1, -8}, // 0x4C 'L' - {247, 8, 9, 10, 1, -8}, // 0x4D 'M' - {256, 7, 9, 9, 1, -8}, // 0x4E 'N' - {264, 9, 9, 9, 0, -8}, // 0x4F 'O' - {275, 6, 9, 8, 1, -8}, // 0x50 'P' - {282, 9, 9, 9, 0, -8}, // 0x51 'Q' - {293, 7, 9, 9, 1, -8}, // 0x52 'R' - {301, 6, 9, 8, 1, -8}, // 0x53 'S' - {308, 7, 9, 7, 0, -8}, // 0x54 'T' - {316, 7, 9, 9, 1, -8}, // 0x55 'U' - {324, 8, 9, 8, 0, -8}, // 0x56 'V' - {333, 11, 9, 11, 0, -8}, // 0x57 'W' - {346, 6, 9, 8, 1, -8}, // 0x58 'X' - {353, 8, 9, 8, 0, -8}, // 0x59 'Y' - {362, 7, 9, 7, 0, -8}, // 0x5A 'Z' - {370, 2, 12, 3, 1, -8}, // 0x5B '[' - {373, 3, 9, 3, 0, -8}, // 0x5C '\' - {377, 3, 12, 3, 0, -8}, // 0x5D ']' - {382, 4, 5, 6, 1, -8}, // 0x5E '^' - {385, 6, 1, 7, 0, 2}, // 0x5F '_' - {386, 2, 2, 4, 1, -8}, // 0x60 '`' - {387, 5, 6, 7, 1, -5}, // 0x61 'a' - {391, 5, 9, 7, 1, -8}, // 0x62 'b' - {397, 6, 6, 6, 0, -5}, // 0x63 'c' - {402, 6, 9, 7, 0, -8}, // 0x64 'd' - {409, 5, 6, 7, 1, -5}, // 0x65 'e' - {413, 3, 9, 3, 0, -8}, // 0x66 'f' - {417, 6, 9, 7, 0, -5}, // 0x67 'g' - {424, 5, 9, 7, 1, -8}, // 0x68 'h' - {430, 1, 9, 3, 1, -8}, // 0x69 'i' - {432, 2, 12, 3, 0, -8}, // 0x6A 'j' - {435, 5, 9, 6, 1, -8}, // 0x6B 'k' - {441, 1, 9, 3, 1, -8}, // 0x6C 'l' - {443, 8, 6, 10, 1, -5}, // 0x6D 'm' - {449, 5, 6, 7, 1, -5}, // 0x6E 'n' - {453, 6, 6, 7, 0, -5}, // 0x6F 'o' - {458, 5, 9, 7, 1, -5}, // 0x70 'p' - {464, 6, 9, 7, 0, -5}, // 0x71 'q' - {471, 3, 6, 4, 1, -5}, // 0x72 'r' - {474, 6, 6, 6, 0, -5}, // 0x73 's' - {479, 3, 8, 3, 0, -7}, // 0x74 't' - {482, 5, 6, 7, 1, -5}, // 0x75 'u' - {486, 6, 6, 6, 0, -5}, // 0x76 'v' - {491, 8, 6, 9, 0, -5}, // 0x77 'w' - {497, 4, 6, 6, 1, -5}, // 0x78 'x' - {500, 5, 9, 6, 0, -5}, // 0x79 'y' - {506, 5, 6, 6, 0, -5}, // 0x7A 'z' - {510, 2, 12, 4, 1, -8}, // 0x7B '{' - {513, 1, 12, 3, 1, -8}, // 0x7C '|' - {515, 3, 12, 4, 0, -8}, // 0x7D '}' - {520, 5, 2, 7, 1, -4}, // 0x7E '~' - {522, 6, 9, 8, 1, -8}, // - {529, 9, 11, 9, 0, -8}, // - {542, 6, 11, 7, 1, -10}, // - {551, 0, 0, 8, 0, 0}, // - {551, 4, 9, 5, 1, -8}, // - {556, 0, 0, 8, 0, 0}, // - {556, 0, 0, 8, 0, 0}, // - {556, 0, 0, 8, 0, 0}, // - {556, 0, 0, 8, 0, 0}, // - {556, 6, 8, 8, 1, -7}, // - {562, 0, 0, 8, 0, 0}, // - {562, 11, 9, 13, 1, -8}, // - {575, 0, 0, 8, 0, 0}, // - {575, 11, 9, 12, 1, -8}, // - {588, 6, 11, 8, 1, -10}, // - {597, 9, 9, 9, 0, -8}, // - {608, 7, 11, 9, 1, -8}, // - {618, 6, 11, 7, 0, -8}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 0, 0, 8, 0, 0}, // - {627, 9, 6, 10, 0, -5}, // - {634, 0, 0, 8, 0, 0}, // - {634, 9, 6, 10, 1, -5}, // - {641, 4, 8, 6, 1, -7}, // - {645, 6, 9, 7, 0, -8}, // - {652, 5, 7, 7, 1, -5}, // - {657, 0, 0, 8, 0, 0}, // - {657, 7, 11, 7, 0, -10}, // - {667, 5, 11, 6, 0, -7}, // - {674, 5, 9, 6, 0, -8}, // - {680, 0, 0, 8, 0, 0}, // - {680, 6, 10, 7, 1, -9}, // - {688, 0, 0, 8, 0, 0}, // - {688, 0, 0, 8, 0, 0}, // - {688, 6, 11, 8, 1, -10}, // - {697, 7, 9, 9, 1, -8}, // - {705, 0, 0, 8, 0, 0}, // - {705, 0, 0, 8, 0, 0}, // - {705, 2, 12, 3, 0, -8}, // - {708, 0, 0, 8, 0, 0}, // - {708, 0, 0, 8, 0, 0}, // - {708, 3, 11, 3, 0, -10}, // - {713, 0, 0, 8, 0, 0}, // - {713, 0, 0, 8, 0, 0}, // - {713, 1, 9, 3, 1, -8}, // - {715, 1, 9, 3, 1, -8}, // - {717, 3, 8, 5, 1, -7}, // - {720, 6, 9, 7, 1, -5}, // - {727, 0, 0, 8, 0, 0}, // - {727, 0, 0, 8, 0, 0}, // - {727, 6, 9, 7, 0, -8}, // - {734, 9, 9, 11, 1, -8}, // - {745, 6, 6, 6, 0, -5}, // - {750, 0, 0, 8, 0, 0}, // - {750, 0, 0, 8, 0, 0}, // - {750, 6, 9, 8, 1, -8}, // - {757, 6, 6, 6, 0, -5}, // - {762, 3, 9, 3, 0, -8}, // - {766, 8, 9, 8, 0, -8}, // - {775, 6, 9, 8, 1, -8}, // - {782, 6, 9, 8, 1, -8}, // - {789, 6, 9, 7, 1, -8}, // - {796, 9, 11, 10, 0, -8}, // - {809, 6, 9, 8, 1, -8}, // - {816, 9, 9, 11, 1, -8}, // - {827, 6, 9, 8, 1, -8}, // - {834, 7, 9, 9, 1, -8}, // - {842, 7, 11, 9, 1, -10}, // - {852, 6, 9, 8, 1, -8}, // - {859, 7, 9, 8, 0, -8}, // - {867, 8, 9, 10, 1, -8}, // - {876, 7, 9, 9, 1, -8}, // - {884, 8, 9, 10, 1, -8}, // - {893, 7, 9, 9, 1, -8}, // - {901, 6, 9, 8, 1, -8}, // - {908, 7, 9, 9, 1, -8}, // - {916, 7, 9, 7, 0, -8}, // - {924, 7, 9, 7, 0, -8}, // - {932, 9, 9, 10, 1, -8}, // - {943, 6, 9, 8, 1, -8}, // - {950, 8, 11, 9, 1, -8}, // - {961, 6, 9, 8, 1, -8}, // - {968, 8, 9, 10, 1, -8}, // - {977, 9, 11, 10, 1, -8}, // - {990, 10, 9, 10, 0, -8}, // - {1002, 9, 9, 10, 1, -8}, // - {1013, 6, 9, 8, 1, -8}, // - {1020, 7, 9, 9, 1, -8}, // - {1028, 10, 9, 12, 1, -8}, // - {1040, 6, 9, 8, 1, -8}, // - {1047, 6, 6, 7, 0, -5}, // - {1052, 6, 9, 7, 0, -8}, // - {1059, 5, 6, 6, 1, -5}, // - {1063, 4, 6, 5, 1, -5}, // - {1066, 7, 7, 7, 0, -5}, // - {1073, 6, 6, 7, 0, -5}, // - {1078, 8, 6, 9, 1, -5}, // - {1084, 6, 6, 6, 0, -5}, // - {1089, 5, 6, 7, 1, -5}, // - {1093, 5, 8, 7, 1, -7}, // - {1098, 4, 6, 6, 1, -5}, // - {1101, 5, 6, 6, 0, -5}, // - {1105, 6, 6, 7, 1, -5}, // - {1110, 5, 6, 7, 1, -5}, // - {1114, 6, 6, 7, 0, -5}, // - {1119, 5, 6, 7, 1, -5}, // - {1123, 5, 9, 7, 1, -5}, // - {1129, 6, 6, 6, 0, -5}, // - {1134, 5, 6, 5, 0, -5}, // - {1138, 5, 9, 6, 0, -5}, // - {1144, 10, 11, 10, 0, -7}, // - {1158, 5, 6, 6, 0, -5}, // - {1162, 6, 7, 7, 1, -5}, // - {1168, 4, 6, 6, 1, -5}, // - {1171, 6, 6, 8, 1, -5}, // - {1176, 7, 7, 9, 1, -5}, // - {1183, 7, 6, 8, 0, -5}, // - {1189, 6, 6, 8, 1, -5}, // - {1194, 5, 6, 6, 1, -5}, // - {1198, 5, 6, 6, 1, -5}, // - {1202, 8, 6, 9, 1, -5}, // - {1208, 5, 6, 7, 1, -5} // -}; - -const GFXfont FreeSans6pt8bCyrillic PROGMEM = {(uint8_t *)FreeSans6pt8bCyrillicBitmaps, (GFXglyph *)FreeSans6pt8bCyrillicGlyphs, - 0x20, 0xFF, 16}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h new file mode 100644 index 000000000..4042bd835 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1250.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1250 +*/ +const uint8_t FreeSans6pt_Win1250Bitmaps[] PROGMEM = { +/* 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 = { +/* 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, +0x01, 0xFF, 14 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h new file mode 100644 index 000000000..8fe505fbd --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1251.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1251 +*/ +const uint8_t FreeSans6pt_Win1251Bitmaps[] PROGMEM = { +/* 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 = { +/* 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, +0x01, 0xFF, 14 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h new file mode 100644 index 000000000..b17be8756 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1252.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1252 +*/ +const uint8_t FreeSans6pt_Win1252Bitmaps[] PROGMEM = { +/* 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 = { +/* 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, +0x01, 0xFF, 10 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h new file mode 100644 index 000000000..dd66801a1 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1250.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1250 +*/ +const uint8_t FreeSans9pt_Win1250Bitmaps[] PROGMEM = { +/* 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 = { +/* 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, +0x01, 0xFF, 21 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h new file mode 100644 index 000000000..b1511d996 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1251.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1251 +*/ +const uint8_t FreeSans9pt_Win1251Bitmaps[] PROGMEM = { +/* 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 = { +/* 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, +0x01, 0xFF, 21 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h new file mode 100644 index 000000000..8bc656632 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1252.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1252 +*/ +const uint8_t FreeSans9pt_Win1252Bitmaps[] PROGMEM = { +/* 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 = { +/* 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, +0x01, 0xFF, 16 +}; diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index 6c6245ec3..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) @@ -263,22 +264,10 @@ uint16_t InkHUD::Applet::Y(float f) // Print text, specifying the position of any edge / corner of the textbox void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) { - printAt(x, y, std::string(text), ha, va); -} - -// Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) -{ - // Custom font - // - set with AppletFont::addSubstitution - // - find certain UTF8 chars - // - replace with glyph from custom font (or suitable ASCII addSubstitution?) - getFont().applySubstitutions(&text); - // We do still have to run getTextBounds to find the width int16_t textOffsetX, textOffsetY; uint16_t textWidth, textHeight; - getTextBounds(text.c_str(), 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); + getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); int16_t cursorX = 0; int16_t cursorY = 0; @@ -310,7 +299,13 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalA } setCursor(cursorX, cursorY); - print(text.c_str()); + print(text); +} + +// Print text, specifying the position of any edge / corner of the textbox +void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) +{ + printAt(x, y, text.c_str(), ha, va); } // Set which font should be used for subsequent drawing @@ -328,11 +323,51 @@ InkHUD::AppletFont InkHUD::Applet::getFont() return currentFont; } +// Parse any text which might have "special characters" +// Re-encodes UTF-8 characters to match our 8-bit encoded fonts +std::string InkHUD::Applet::parse(std::string text) +{ + return getFont().decodeUTF8(text); +} + +// Get the best version of a node's short name available to us +// Parses any non-ascii chars +// Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji) +std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) +{ + assert(node); + + // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) + if (node->has_user) { + std::string parsed = parse(node->user.short_name); + if (isPrintable(parsed)) + return parsed; + } + + // Otherwise, use the "last 4" of node id + // - if short name unknown, or + // - if short name is emoji (we can't render this) + std::string nodeID = hexifyNodeNum(node->num); + return nodeID.substr(nodeID.length() - 4); +} + +// Determine if all characters of a string are printable using the current font +bool InkHUD::Applet::isPrintable(std::string text) +{ + // 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 == '\x1A') + return false; + } + + // No unprintable characters found + return true; +} + // Gets rendered width of a string // Wrapper for getTextBounds uint16_t InkHUD::Applet::getTextWidth(const char *text) { - // We do still have to run getTextBounds to find the width int16_t textOffsetX, textOffsetY; uint16_t textWidth, textHeight; @@ -345,8 +380,6 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text) // Wrapper for getTextBounds uint16_t InkHUD::Applet::getTextWidth(std::string text) { - getFont().applySubstitutions(&text); - return getTextWidth(text.c_str()); } @@ -395,12 +428,6 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) // Avoids splitting words in half, instead moving the entire word to a new line wherever possible void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) { - // Custom font glyphs - // - set with AppletFont::addSubstitution - // - find certain UTF8 chars - // - replace with glyph from custom font (or suitable ASCII addSubstitution?) - getFont().applySubstitutions(&text); - // Place the AdafruitGFX cursor to suit our "top" coord setCursor(left, top + getFont().heightAboveCursor()); diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 8f4466647..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 @@ -133,12 +133,15 @@ class Applet : public GFX void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color = BLACK); // Draw the Meshtastic logo - std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc - SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value - std::string getTimeString(uint32_t epochSeconds); // Human readable - std::string getTimeString(); // Current time, human readable - uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu - std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric + std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc + SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value + std::string getTimeString(uint32_t epochSeconds); // Human readable + std::string getTimeString(); // Current time, human readable + uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu + std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric + std::string parse(std::string text); // Handle text which might contain special chars + std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars + bool isPrintable(std::string); // Check for characters which the font can't print // Convenient references diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 25597c9b9..db7097f3f 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -2,14 +2,17 @@ #include "./AppletFont.h" +#include + using namespace NicheGraphics; InkHUD::AppletFont::AppletFont() { - // Default constructor uses the in-built AdafruitGFX font + // Default constructor uses the in-built AdafruitGFX font (not recommended) } -InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafruitGFXFont) +InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding, int8_t paddingTop, int8_t paddingBottom) + : gfxFont(&adafruitGFXFont), encoding(encoding) { // AdafruitGFX fonts are drawn relative to a "cursor line"; // they print as if the glyphs are resting on the line of piece of ruled paper. @@ -22,6 +25,10 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafru // AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding, // which we'd rather not deal with. If we want padding, we'll add it manually. + this->ascenderHeight = 0; + this->descenderHeight = 0; + this->height = 0; + // Scan each glyph in the AdafruitGFX font for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) { uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph @@ -33,10 +40,16 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafru int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; if (glyphAscender > 0) this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); + + int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; + if (glyphDescender > 0) + this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); } - // Determine how far characters may hang "below the line" - descenderHeight = height - ascenderHeight; + // Apply any manual padding to grow or shrink the line size + // Helpful if a font has one or two exceptionally large characters, which would make the lines ridiculously tall + ascenderHeight += paddingTop; + descenderHeight += paddingBottom; // Find how far the cursor advances when we "print" a space character spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; @@ -83,139 +96,640 @@ uint8_t InkHUD::AppletFont::widthBetweenWords() return this->spaceCharWidth; } -// Add to the list of substituted glyphs -// This "find and replace" operation will be run before text is printed -// Used to swap out UTF8 special characters, either with a custom font, or with a suitable ASCII approximation -void InkHUD::AppletFont::addSubstitution(const char *from, const char *to) +// Convert a unicode char from set of UTF-8 bytes to UTF-32 +// Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value +uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) { - substitutions.push_back({.from = from, .to = to}); + uint32_t utf32 = 0; + + switch (utf8.length()) { + case 2: + // 5 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00011111) << 6; + utf32 |= (utf8.at(1) & 0b00111111); + break; + + case 3: + // 4 bits + 6 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00001111) << (6 + 6); + utf32 |= (utf8.at(1) & 0b00111111) << 6; + utf32 |= (utf8.at(2) & 0b00111111); + break; + + case 4: + // 3 bits + 6 bits + 6 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00000111) << (6 + 6 + 6); + utf32 |= (utf8.at(1) & 0b00111111) << (6 + 6); + utf32 |= (utf8.at(2) & 0b00111111) << 6; + utf32 |= (utf8.at(3) & 0b00111111); + break; + default: + assert(false); + } + + return utf32; } -// Run all registered substitutions on a string -// Used to swap out UTF8 special chars -void InkHUD::AppletFont::applySubstitutions(std::string *text) +// Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII +// Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars +std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) { - // For each substitution - for (Substitution s : substitutions) { + // Final processed output + std::string decoded; - // Find and replace - // - search for Substitution::from - // - replace with Substitution::to - size_t i = text->find(s.from); - while (i != std::string::npos) { - text->replace(i, strlen(s.from), s.to); - i = text->find(s.from, i); // Continue looking from last position + // Holds bytes for one UTF-8 char during parsing + std::string utf8Char; + uint8_t utf8CharSize = 0; + + for (char &c : encoded) { + + // If first byte + if (utf8Char.empty()) { + // If MSB is unset, byte is an ASCII char + // If MSB is set, byte is part of a UTF-8 char. Counting number of higher-order bits tells how many bytes in char + if ((c & 0x80)) { + char c1 = c; + while (c1 & 0x80) { + c1 <<= 1; + utf8CharSize++; + } + } + } + + // Append the byte to the UTF-8 char we're building + utf8Char += c; + + // More bytes left to collect. Iterate. + if (utf8Char.length() < utf8CharSize) + continue; + + // Now collected all bytes for this char + // Remap the value to match the encoding of our 8-bit AppletFont + decoded += applyEncoding(utf8Char); + + // Reset, ready to build next UTF-8 char from the encoded bytes + utf8Char.clear(); + utf8CharSize = 0; + } // For each char + + // All chars processed, return result + return decoded; +} + +// Re-encode a single UTF-8 character to extended ASCII +// Target encoding depends on the font +char InkHUD::AppletFont::applyEncoding(std::string utf8) +{ + // ##################################################### Syntactic Sugar ##################################################### +#define REMAP(in, out) \ + case in: \ + return out; + // ########################################################################################################################### + + // Latin - Central Europe + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1250.TXT + if (encoding == WINDOWS_1250) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); + + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x20AC, 0x80); // EURO SIGN + REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK + REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86); // DAGGER + REMAP(0x2021, 0x87); // DOUBLE DAGGER + REMAP(0x2030, 0x89); // PER MILLE SIGN + REMAP(0x0160, 0x8A); // LATIN CAPITAL LETTER S WITH CARON + REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x015A, 0x8C); // LATIN CAPITAL LETTER S WITH ACUTE + REMAP(0x0164, 0x8D); // LATIN CAPITAL LETTER T WITH CARON + REMAP(0x017D, 0x8E); // LATIN CAPITAL LETTER Z WITH CARON + REMAP(0x0179, 0x8F); // LATIN CAPITAL LETTER Z WITH ACUTE + + REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95); // BULLET + REMAP(0x2013, 0x96); // EN DASH + REMAP(0x2014, 0x97); // EM DASH + REMAP(0x2122, 0x99); // TRADE MARK SIGN + REMAP(0x0161, 0x9A); // LATIN SMALL LETTER S WITH CARON + REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x015B, 0x9C); // LATIN SMALL LETTER S WITH ACUTE + REMAP(0x0165, 0x9D); // LATIN SMALL LETTER T WITH CARON + REMAP(0x017E, 0x9E); // LATIN SMALL LETTER Z WITH CARON + REMAP(0x017A, 0x9F); // LATIN SMALL LETTER Z WITH ACUTE + + REMAP(0x00A0, 0xA0); // NO-BREAK SPACE + REMAP(0x02C7, 0xA1); // CARON + REMAP(0x02D8, 0xA2); // BREVE + REMAP(0x0141, 0xA3); // LATIN CAPITAL LETTER L WITH STROKE + REMAP(0x00A4, 0xA4); // CURRENCY SIGN + REMAP(0x0104, 0xA5); // LATIN CAPITAL LETTER A WITH OGONEK + REMAP(0x00A6, 0xA6); // BROKEN BAR + REMAP(0x00A7, 0xA7); // SECTION SIGN + REMAP(0x00A8, 0xA8); // DIAERESIS + REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN + REMAP(0x015E, 0xAA); // LATIN CAPITAL LETTER S WITH CEDILLA + REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC); // NOT SIGN + REMAP(0x00AD, 0xAD); // SOFT HYPHEN + REMAP(0x00AE, 0xAE); // REGISTERED SIGN + REMAP(0x017B, 0xAF); // LATIN CAPITAL LETTER Z WITH DOT ABOVE + + REMAP(0x00B0, 0xB0); // DEGREE SIGN + REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN + REMAP(0x02DB, 0xB2); // OGONEK + REMAP(0x0142, 0xB3); // LATIN SMALL LETTER L WITH STROKE + REMAP(0x00B4, 0xB4); // ACUTE ACCENT + REMAP(0x00B5, 0xB5); // MICRO SIGN + REMAP(0x00B6, 0xB6); // PILCROW SIGN + REMAP(0x00B7, 0xB7); // MIDDLE DOT + REMAP(0x00B8, 0xB8); // CEDILLA + REMAP(0x0105, 0xB9); // LATIN SMALL LETTER A WITH OGONEK + REMAP(0x015F, 0xBA); // LATIN SMALL LETTER S WITH CEDILLA + REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x013D, 0xBC); // LATIN CAPITAL LETTER L WITH CARON + REMAP(0x02DD, 0xBD); // DOUBLE ACUTE ACCENT + REMAP(0x013E, 0xBE); // LATIN SMALL LETTER L WITH CARON + REMAP(0x017C, 0xBF); // LATIN SMALL LETTER Z WITH DOT ABOVE + + REMAP(0x0154, 0xC0); // LATIN CAPITAL LETTER R WITH ACUTE + REMAP(0x00C1, 0xC1); // LATIN CAPITAL LETTER A WITH ACUTE + REMAP(0x00C2, 0xC2); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + REMAP(0x0102, 0xC3); // LATIN CAPITAL LETTER A WITH BREVE + REMAP(0x00C4, 0xC4); // LATIN CAPITAL LETTER A WITH DIAERESIS + REMAP(0x0139, 0xC5); // LATIN CAPITAL LETTER L WITH ACUTE + REMAP(0x0106, 0xC6); // LATIN CAPITAL LETTER C WITH ACUTE + REMAP(0x00C7, 0xC7); // LATIN CAPITAL LETTER C WITH CEDILLA + REMAP(0x010C, 0xC8); // LATIN CAPITAL LETTER C WITH CARON + REMAP(0x00C9, 0xC9); // LATIN CAPITAL LETTER E WITH ACUTE + REMAP(0x0118, 0xCA); // LATIN CAPITAL LETTER E WITH OGONEK + REMAP(0x00CB, 0xCB); // LATIN CAPITAL LETTER E WITH DIAERESIS + REMAP(0x011A, 0xCC); // LATIN CAPITAL LETTER E WITH CARON + REMAP(0x00CD, 0xCD); // LATIN CAPITAL LETTER I WITH ACUTE + REMAP(0x00CE, 0xCE); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + REMAP(0x010E, 0xCF); // LATIN CAPITAL LETTER D WITH CARON + + REMAP(0x0110, 0xD0); // LATIN CAPITAL LETTER D WITH STROKE + REMAP(0x0143, 0xD1); // LATIN CAPITAL LETTER N WITH ACUTE + REMAP(0x0147, 0xD2); // LATIN CAPITAL LETTER N WITH CARON + REMAP(0x00D3, 0xD3); // LATIN CAPITAL LETTER O WITH ACUTE + REMAP(0x00D4, 0xD4); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + REMAP(0x0150, 0xD5); // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + REMAP(0x00D6, 0xD6); // LATIN CAPITAL LETTER O WITH DIAERESIS + REMAP(0x00D7, 0xD7); // MULTIPLICATION SIGN + REMAP(0x0158, 0xD8); // LATIN CAPITAL LETTER R WITH CARON + REMAP(0x016E, 0xD9); // LATIN CAPITAL LETTER U WITH RING ABOVE + REMAP(0x00DA, 0xDA); // LATIN CAPITAL LETTER U WITH ACUTE + REMAP(0x0170, 0xDB); // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + REMAP(0x00DC, 0xDC); // LATIN CAPITAL LETTER U WITH DIAERESIS + REMAP(0x00DD, 0xDD); // LATIN CAPITAL LETTER Y WITH ACUTE + REMAP(0x0162, 0xDE); // LATIN CAPITAL LETTER T WITH CEDILLA + REMAP(0x00DF, 0xDF); // LATIN SMALL LETTER SHARP S + + REMAP(0x0155, 0xE0); // LATIN SMALL LETTER R WITH ACUTE + REMAP(0x00E1, 0xE1); // LATIN SMALL LETTER A WITH ACUTE + REMAP(0x00E2, 0xE2); // LATIN SMALL LETTER A WITH CIRCUMFLEX + REMAP(0x0103, 0xE3); // LATIN SMALL LETTER A WITH BREVE + REMAP(0x00E4, 0xE4); // LATIN SMALL LETTER A WITH DIAERESIS + REMAP(0x013A, 0xE5); // LATIN SMALL LETTER L WITH ACUTE + REMAP(0x0107, 0xE6); // LATIN SMALL LETTER C WITH ACUTE + REMAP(0x00E7, 0xE7); // LATIN SMALL LETTER C WITH CEDILLA + REMAP(0x010D, 0xE8); // LATIN SMALL LETTER C WITH CARON + REMAP(0x00E9, 0xE9); // LATIN SMALL LETTER E WITH ACUTE + REMAP(0x0119, 0xEA); // LATIN SMALL LETTER E WITH OGONEK + REMAP(0x00EB, 0xEB); // LATIN SMALL LETTER E WITH DIAERESIS + REMAP(0x011B, 0xEC); // LATIN SMALL LETTER E WITH CARON + REMAP(0x00ED, 0xED); // LATIN SMALL LETTER I WITH ACUTE + REMAP(0x00EE, 0xEE); // LATIN SMALL LETTER I WITH CIRCUMFLEX + REMAP(0x010F, 0xEF); // LATIN SMALL LETTER D WITH CARON + + REMAP(0x0111, 0xF0); // LATIN SMALL LETTER D WITH STROKE + REMAP(0x0144, 0xF1); // LATIN SMALL LETTER N WITH ACUTE + REMAP(0x0148, 0xF2); // LATIN SMALL LETTER N WITH CARON + REMAP(0x00F3, 0xF3); // LATIN SMALL LETTER O WITH ACUTE + REMAP(0x00F4, 0xF4); // LATIN SMALL LETTER O WITH CIRCUMFLEX + REMAP(0x0151, 0xF5); // LATIN SMALL LETTER O WITH DOUBLE ACUTE + REMAP(0x00F6, 0xF6); // LATIN SMALL LETTER O WITH DIAERESIS + REMAP(0x00F7, 0xF7); // DIVISION SIGN + REMAP(0x0159, 0xF8); // LATIN SMALL LETTER R WITH CARON + REMAP(0x016F, 0xF9); // LATIN SMALL LETTER U WITH RING ABOVE + REMAP(0x00FA, 0xFA); // LATIN SMALL LETTER U WITH ACUTE + REMAP(0x0171, 0xFB); // LATIN SMALL LETTER U WITH DOUBLE ACUTE + REMAP(0x00FC, 0xFC); // LATIN SMALL LETTER U WITH DIAERESIS + REMAP(0x00FD, 0xFD); // LATIN SMALL LETTER Y WITH ACUTE + REMAP(0x0163, 0xFE); // LATIN SMALL LETTER T WITH CEDILLA + REMAP(0x02D9, 0xFF); // DOT ABOVE } } -} -// Apply a set of substitutions which remap UTF8 for a Windows-1251 font -// Windows-1251 is an 8-bit character encoding, suitable for several languages which use the Cyrillic script -void InkHUD::AppletFont::addSubstitutionsWin1251() -{ - addSubstitution("ะ", "\x80"); - addSubstitution("ะ", "\x81"); - addSubstitution("ั", "\x83"); - addSubstitution("โฌ", "\x88"); - addSubstitution("ะ", "\x8A"); - addSubstitution("ะ", "\x8C"); - addSubstitution("ะ", "\x8D"); - addSubstitution("ะ", "\x8E"); - addSubstitution("ะ", "\x8F"); + // Latin - Cyrillic + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1251.TXT + else if (encoding == WINDOWS_1251) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); - addSubstitution("ั", "\x90"); - addSubstitution("ั", "\x9A"); - addSubstitution("ั", "\x9C"); - addSubstitution("ั", "\x9D"); - addSubstitution("ั", "\x9E"); - addSubstitution("ั", "\x9F"); + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x0402, 0x80); // CYRILLIC CAPITAL LETTER DJE + REMAP(0x0403, 0x81); // CYRILLIC CAPITAL LETTER GJE + REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK + REMAP(0x0453, 0x83); // CYRILLIC SMALL LETTER GJE + REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86); // DAGGER + REMAP(0x2021, 0x87); // DOUBLE DAGGER + REMAP(0x20AC, 0x88); // EURO SIGN + REMAP(0x2030, 0x89); // PER MILLE SIGN + REMAP(0x0409, 0x8A); // CYRILLIC CAPITAL LETTER LJE + REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x040A, 0x8C); // CYRILLIC CAPITAL LETTER NJE + REMAP(0x040C, 0x8D); // CYRILLIC CAPITAL LETTER KJE + REMAP(0x040B, 0x8E); // CYRILLIC CAPITAL LETTER TSHE + REMAP(0x040F, 0x8F); // CYRILLIC CAPITAL LETTER DZHE - addSubstitution("ะ", "\xA1"); - addSubstitution("ั", "\xA2"); - addSubstitution("ะ", "\xA3"); - addSubstitution("า", "\xA5"); - addSubstitution("ะ", "\xA8"); - addSubstitution("ะ", "\xAA"); - addSubstitution("ะ", "\xAF"); + REMAP(0x0452, 0x90); // CYRILLIC SMALL LETTER DJE + REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95); // BULLET + REMAP(0x2013, 0x96); // EN DASH + REMAP(0x2014, 0x97); // EM DASH + REMAP(0x2122, 0x99); // TRADE MARK SIGN + REMAP(0x0459, 0x9A); // CYRILLIC SMALL LETTER LJE + REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x045A, 0x9C); // CYRILLIC SMALL LETTER NJE + REMAP(0x045C, 0x9D); // CYRILLIC SMALL LETTER KJE + REMAP(0x045B, 0x9E); // CYRILLIC SMALL LETTER TSHE + REMAP(0x045F, 0x9F); // CYRILLIC SMALL LETTER DZHE - addSubstitution("ะ", "\xB2"); - addSubstitution("ั", "\xB3"); - addSubstitution("า", "\xB4"); - addSubstitution("ั", "\xB8"); - addSubstitution("โ", "\xB9"); - addSubstitution("ั", "\xBA"); - addSubstitution("ั", "\xBC"); - addSubstitution("ะ ", "\xBD"); - addSubstitution("ั", "\xBE"); - addSubstitution("ั", "\xBF"); + REMAP(0x00A0, 0xA0); // NO-BREAK SPACE + REMAP(0x040E, 0xA1); // CYRILLIC CAPITAL LETTER SHORT U + REMAP(0x045E, 0xA2); // CYRILLIC SMALL LETTER SHORT U + REMAP(0x0408, 0xA3); // CYRILLIC CAPITAL LETTER JE + REMAP(0x00A4, 0xA4); // CURRENCY SIGN + REMAP(0x0490, 0xA5); // CYRILLIC CAPITAL LETTER GHE WITH UPTURN + REMAP(0x00A6, 0xA6); // BROKEN BAR + REMAP(0x00A7, 0xA7); // SECTION SIGN + REMAP(0x0401, 0xA8); // CYRILLIC CAPITAL LETTER IO + REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN + REMAP(0x0404, 0xAA); // CYRILLIC CAPITAL LETTER UKRAINIAN IE + REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC); // NOT SIGN + REMAP(0x00AD, 0xAD); // SOFT HYPHEN + REMAP(0x00AE, 0xAE); // REGISTERED SIGN + REMAP(0x0407, 0xAF); // CYRILLIC CAPITAL LETTER YI - addSubstitution("ะ", "\xC0"); - addSubstitution("ะ", "\xC1"); - addSubstitution("ะ", "\xC2"); - addSubstitution("ะ", "\xC3"); - addSubstitution("ะ", "\xC4"); - addSubstitution("ะ", "\xC5"); - addSubstitution("ะ", "\xC6"); - addSubstitution("ะ", "\xC7"); - addSubstitution("ะ", "\xC8"); - addSubstitution("ะ", "\xC9"); - addSubstitution("ะ", "\xCA"); - addSubstitution("ะ", "\xCB"); - addSubstitution("ะ", "\xCC"); - addSubstitution("ะ", "\xCD"); - addSubstitution("ะ", "\xCE"); - addSubstitution("ะ", "\xCF"); + REMAP(0x00B0, 0xB0); // DEGREE SIGN + REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN + REMAP(0x0406, 0xB2); // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I + REMAP(0x0456, 0xB3); // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + REMAP(0x0491, 0xB4); // CYRILLIC SMALL LETTER GHE WITH UPTURN + REMAP(0x00B5, 0xB5); // MICRO SIGN + REMAP(0x00B6, 0xB6); // PILCROW SIGN + REMAP(0x00B7, 0xB7); // MIDDLE DOT + REMAP(0x0451, 0xB8); // CYRILLIC SMALL LETTER IO + REMAP(0x2116, 0xB9); // NUMERO SIGN + REMAP(0x0454, 0xBA); // CYRILLIC SMALL LETTER UKRAINIAN IE + REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x0458, 0xBC); // CYRILLIC SMALL LETTER JE + REMAP(0x0405, 0xBD); // CYRILLIC CAPITAL LETTER DZE + REMAP(0x0455, 0xBE); // CYRILLIC SMALL LETTER DZE + REMAP(0x0457, 0xBF); // CYRILLIC SMALL LETTER YI - addSubstitution("ะ ", "\xD0"); - addSubstitution("ะก", "\xD1"); - addSubstitution("ะข", "\xD2"); - addSubstitution("ะฃ", "\xD3"); - addSubstitution("ะค", "\xD4"); - addSubstitution("ะฅ", "\xD5"); - addSubstitution("ะฆ", "\xD6"); - addSubstitution("ะง", "\xD7"); - addSubstitution("ะจ", "\xD8"); - addSubstitution("ะฉ", "\xD9"); - addSubstitution("ะช", "\xDA"); - addSubstitution("ะซ", "\xDB"); - addSubstitution("ะฌ", "\xDC"); - addSubstitution("ะญ", "\xDD"); - addSubstitution("ะฎ", "\xDE"); - addSubstitution("ะฏ", "\xDF"); + REMAP(0x0410, 0xC0); // CYRILLIC CAPITAL LETTER A + REMAP(0x0411, 0xC1); // CYRILLIC CAPITAL LETTER BE + REMAP(0x0412, 0xC2); // CYRILLIC CAPITAL LETTER VE + REMAP(0x0413, 0xC3); // CYRILLIC CAPITAL LETTER GHE + REMAP(0x0414, 0xC4); // CYRILLIC CAPITAL LETTER DE + REMAP(0x0415, 0xC5); // CYRILLIC CAPITAL LETTER IE + REMAP(0x0416, 0xC6); // CYRILLIC CAPITAL LETTER ZHE + REMAP(0x0417, 0xC7); // CYRILLIC CAPITAL LETTER ZE + REMAP(0x0418, 0xC8); // CYRILLIC CAPITAL LETTER I + REMAP(0x0419, 0xC9); // CYRILLIC CAPITAL LETTER SHORT I + REMAP(0x041A, 0xCA); // CYRILLIC CAPITAL LETTER KA + REMAP(0x041B, 0xCB); // CYRILLIC CAPITAL LETTER EL + REMAP(0x041C, 0xCC); // CYRILLIC CAPITAL LETTER EM + REMAP(0x041D, 0xCD); // CYRILLIC CAPITAL LETTER EN + REMAP(0x041E, 0xCE); // CYRILLIC CAPITAL LETTER O + REMAP(0x041F, 0xCF); // CYRILLIC CAPITAL LETTER PE - addSubstitution("ะฐ", "\xE0"); - addSubstitution("ะฑ", "\xE1"); - addSubstitution("ะฒ", "\xE2"); - addSubstitution("ะณ", "\xE3"); - addSubstitution("ะด", "\xE4"); - addSubstitution("ะต", "\xE5"); - addSubstitution("ะถ", "\xE6"); - addSubstitution("ะท", "\xE7"); - addSubstitution("ะธ", "\xE8"); - addSubstitution("ะน", "\xE9"); - addSubstitution("ะบ", "\xEA"); - addSubstitution("ะป", "\xEB"); - addSubstitution("ะผ", "\xEC"); - addSubstitution("ะฝ", "\xED"); - addSubstitution("ะพ", "\xEE"); - addSubstitution("ะฟ", "\xEF"); + REMAP(0x0420, 0xD0); // CYRILLIC CAPITAL LETTER ER + REMAP(0x0421, 0xD1); // CYRILLIC CAPITAL LETTER ES + REMAP(0x0422, 0xD2); // CYRILLIC CAPITAL LETTER TE + REMAP(0x0423, 0xD3); // CYRILLIC CAPITAL LETTER U + REMAP(0x0424, 0xD4); // CYRILLIC CAPITAL LETTER EF + REMAP(0x0425, 0xD5); // CYRILLIC CAPITAL LETTER HA + REMAP(0x0426, 0xD6); // CYRILLIC CAPITAL LETTER TSE + REMAP(0x0427, 0xD7); // CYRILLIC CAPITAL LETTER CHE + REMAP(0x0428, 0xD8); // CYRILLIC CAPITAL LETTER SHA + REMAP(0x0429, 0xD9); // CYRILLIC CAPITAL LETTER SHCHA + REMAP(0x042A, 0xDA); // CYRILLIC CAPITAL LETTER HARD SIGN + REMAP(0x042B, 0xDB); // CYRILLIC CAPITAL LETTER YERU + REMAP(0x042C, 0xDC); // CYRILLIC CAPITAL LETTER SOFT SIGN + REMAP(0x042D, 0xDD); // CYRILLIC CAPITAL LETTER E + REMAP(0x042E, 0xDE); // CYRILLIC CAPITAL LETTER YU + REMAP(0x042F, 0xDF); // CYRILLIC CAPITAL LETTER YA - addSubstitution("ั", "\xF0"); - addSubstitution("ั", "\xF1"); - addSubstitution("ั", "\xF2"); - addSubstitution("ั", "\xF3"); - addSubstitution("ั", "\xF4"); - addSubstitution("ั ", "\xF5"); - addSubstitution("ั", "\xF6"); - addSubstitution("ั", "\xF7"); - addSubstitution("ั", "\xF8"); - addSubstitution("ั", "\xF9"); - addSubstitution("ั", "\xFA"); - addSubstitution("ั", "\xFB"); - addSubstitution("ั", "\xFC"); - addSubstitution("ั", "\xFD"); - addSubstitution("ั", "\xFE"); - addSubstitution("ั", "\xFF"); + REMAP(0x0430, 0xE0); // CYRILLIC SMALL LETTER A + REMAP(0x0431, 0xE1); // CYRILLIC SMALL LETTER BE + REMAP(0x0432, 0xE2); // CYRILLIC SMALL LETTER VE + REMAP(0x0433, 0xE3); // CYRILLIC SMALL LETTER GHE + REMAP(0x0434, 0xE4); // CYRILLIC SMALL LETTER DE + REMAP(0x0435, 0xE5); // CYRILLIC SMALL LETTER IE + REMAP(0x0436, 0xE6); // CYRILLIC SMALL LETTER ZHE + REMAP(0x0437, 0xE7); // CYRILLIC SMALL LETTER ZE + REMAP(0x0438, 0xE8); // CYRILLIC SMALL LETTER I + REMAP(0x0439, 0xE9); // CYRILLIC SMALL LETTER SHORT I + REMAP(0x043A, 0xEA); // CYRILLIC SMALL LETTER KA + REMAP(0x043B, 0xEB); // CYRILLIC SMALL LETTER EL + REMAP(0x043C, 0xEC); // CYRILLIC SMALL LETTER EM + REMAP(0x043D, 0xED); // CYRILLIC SMALL LETTER EN + REMAP(0x043E, 0xEE); // CYRILLIC SMALL LETTER O + REMAP(0x043F, 0xEF); // CYRILLIC SMALL LETTER PE + + REMAP(0x0440, 0xF0); // CYRILLIC SMALL LETTER ER + REMAP(0x0441, 0xF1); // CYRILLIC SMALL LETTER ES + REMAP(0x0442, 0xF2); // CYRILLIC SMALL LETTER TE + REMAP(0x0443, 0xF3); // CYRILLIC SMALL LETTER U + REMAP(0x0444, 0xF4); // CYRILLIC SMALL LETTER EF + REMAP(0x0445, 0xF5); // CYRILLIC SMALL LETTER HA + REMAP(0x0446, 0xF6); // CYRILLIC SMALL LETTER TSE + REMAP(0x0447, 0xF7); // CYRILLIC SMALL LETTER CHE + REMAP(0x0448, 0xF8); // CYRILLIC SMALL LETTER SHA + REMAP(0x0449, 0xF9); // CYRILLIC SMALL LETTER SHCHA + REMAP(0x044A, 0xFA); // CYRILLIC SMALL LETTER HARD SIGN + REMAP(0x044B, 0xFB); // CYRILLIC SMALL LETTER YERU + REMAP(0x044C, 0xFC); // CYRILLIC SMALL LETTER SOFT SIGN + REMAP(0x044D, 0xFD); // CYRILLIC SMALL LETTER E + REMAP(0x044E, 0xFE); // CYRILLIC SMALL LETTER YU + REMAP(0x044F, 0xFF); // CYRILLIC SMALL LETTER YA + } + } + + // Latin - Western Europe + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT + else if (encoding == WINDOWS_1252) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); + + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x20AC, 0x80) // EURO SIGN + REMAP(0x201A, 0x82) // SINGLE LOW-9 QUOTATION MARK + REMAP(0x0192, 0x83) // LATIN SMALL LETTER F WITH HOOK + REMAP(0x201E, 0x84) // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85) // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86) // DAGGER + REMAP(0x2021, 0x87) // DOUBLE DAGGER + REMAP(0x02C6, 0x88) // MODIFIER LETTER CIRCUMFLEX ACCENT + REMAP(0x2030, 0x89) // PER MILLE SIGN + REMAP(0x0160, 0x8A) // LATIN CAPITAL LETTER S WITH CARON + REMAP(0x2039, 0x8B) // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x0152, 0x8C) // LATIN CAPITAL LIGATURE OE + REMAP(0x017D, 0x8E) // LATIN CAPITAL LETTER Z WITH CARON + + REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95) // BULLET + REMAP(0x2013, 0x96) // EN DASH + REMAP(0x2014, 0x97) // EM DASH + REMAP(0x02DC, 0x98) // SMALL TILDE + REMAP(0x2122, 0x99) // TRADE MARK SIGN + REMAP(0x0161, 0x9A) // LATIN SMALL LETTER S WITH CARON + REMAP(0x203A, 0x9B) // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x0153, 0x9C) // LATIN SMALL LIGATURE OE + REMAP(0x017E, 0x9E) // LATIN SMALL LETTER Z WITH CARON + REMAP(0x0178, 0x9F) // LATIN CAPITAL LETTER Y WITH DIAERESIS + + REMAP(0x00A0, 0xA0) // NO-BREAK SPACE + REMAP(0x00A1, 0xA1) // INVERTED EXCLAMATION MARK + REMAP(0x00A2, 0xA2) // CENT SIGN + REMAP(0x00A3, 0xA3) // POUND SIGN + REMAP(0x00A4, 0xA4) // CURRENCY SIGN + REMAP(0x00A5, 0xA5) // YEN SIGN + REMAP(0x00A6, 0xA6) // BROKEN BAR + REMAP(0x00A7, 0xA7) // SECTION SIGN + REMAP(0x00A8, 0xA8) // DIAERESIS + REMAP(0x00A9, 0xA9) // COPYRIGHT SIGN + REMAP(0x00AA, 0xAA) // FEMININE ORDINAL INDICATOR + REMAP(0x00AB, 0xAB) // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC) // NOT SIGN + REMAP(0x00AD, 0xAD) // SOFT HYPHEN + REMAP(0x00AE, 0xAE) // REGISTERED SIGN + REMAP(0x00AF, 0xAF) // MACRON + + REMAP(0x00B0, 0xB0) // DEGREE SIGN + REMAP(0x00B1, 0xB1) // PLUS-MINUS SIGN + REMAP(0x00B2, 0xB2) // SUPERSCRIPT TWO + REMAP(0x00B3, 0xB3) // SUPERSCRIPT THREE + REMAP(0x00B4, 0xB4) // ACUTE ACCENT + REMAP(0x00B5, 0xB5) // MICRO SIGN + REMAP(0x00B6, 0xB6) // PILCROW SIGN + REMAP(0x00B7, 0xB7) // MIDDLE DOT + REMAP(0x00B8, 0xB8) // CEDILLA + REMAP(0x00B9, 0xB9) // SUPERSCRIPT ONE + REMAP(0x00BA, 0xBA) // MASCULINE ORDINAL INDICATOR + REMAP(0x00BB, 0xBB) // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00BC, 0xBC) // VULGAR FRACTION ONE QUARTER + REMAP(0x00BD, 0xBD) // VULGAR FRACTION ONE HALF + REMAP(0x00BE, 0xBE) // VULGAR FRACTION THREE QUARTERS + REMAP(0x00BF, 0xBF) // INVERTED QUESTION MARK + + REMAP(0x00C0, 0xC0) // LATIN CAPITAL LETTER A WITH GRAVE + REMAP(0x00C1, 0xC1) // LATIN CAPITAL LETTER A WITH ACUTE + REMAP(0x00C2, 0xC2) // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + REMAP(0x00C3, 0xC3) // LATIN CAPITAL LETTER A WITH TILDE + REMAP(0x00C4, 0xC4) // LATIN CAPITAL LETTER A WITH DIAERESIS + REMAP(0x00C5, 0xC5) // LATIN CAPITAL LETTER A WITH RING ABOVE + REMAP(0x00C6, 0xC6) // LATIN CAPITAL LETTER AE + REMAP(0x00C7, 0xC7) // LATIN CAPITAL LETTER C WITH CEDILLA + REMAP(0x00C8, 0xC8) // LATIN CAPITAL LETTER E WITH GRAVE + REMAP(0x00C9, 0xC9) // LATIN CAPITAL LETTER E WITH ACUTE + REMAP(0x00CA, 0xCA) // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + REMAP(0x00CB, 0xCB) // LATIN CAPITAL LETTER E WITH DIAERESIS + REMAP(0x00CC, 0xCC) // LATIN CAPITAL LETTER I WITH GRAVE + REMAP(0x00CD, 0xCD) // LATIN CAPITAL LETTER I WITH ACUTE + REMAP(0x00CE, 0xCE) // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + REMAP(0x00CF, 0xCF) // LATIN CAPITAL LETTER I WITH DIAERESIS + + REMAP(0x00D0, 0xD0) // LATIN CAPITAL LETTER ETH + REMAP(0x00D1, 0xD1) // LATIN CAPITAL LETTER N WITH TILDE + REMAP(0x00D2, 0xD2) // LATIN CAPITAL LETTER O WITH GRAVE + REMAP(0x00D3, 0xD3) // LATIN CAPITAL LETTER O WITH ACUTE + REMAP(0x00D4, 0xD4) // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + REMAP(0x00D5, 0xD5) // LATIN CAPITAL LETTER O WITH TILDE + REMAP(0x00D6, 0xD6) // LATIN CAPITAL LETTER O WITH DIAERESIS + REMAP(0x00D7, 0xD7) // MULTIPLICATION SIGN + REMAP(0x00D8, 0xD8) // LATIN CAPITAL LETTER O WITH STROKE + REMAP(0x00D9, 0xD9) // LATIN CAPITAL LETTER U WITH GRAVE + REMAP(0x00DA, 0xDA) // LATIN CAPITAL LETTER U WITH ACUTE + REMAP(0x00DB, 0xDB) // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + REMAP(0x00DC, 0xDC) // LATIN CAPITAL LETTER U WITH DIAERESIS + REMAP(0x00DD, 0xDD) // LATIN CAPITAL LETTER Y WITH ACUTE + REMAP(0x00DE, 0xDE) // LATIN CAPITAL LETTER THORN + REMAP(0x00DF, 0xDF) // LATIN SMALL LETTER SHARP S + + REMAP(0x00E0, 0xE0) // LATIN SMALL LETTER A WITH GRAVE + REMAP(0x00E1, 0xE1) // LATIN SMALL LETTER A WITH ACUTE + REMAP(0x00E2, 0xE2) // LATIN SMALL LETTER A WITH CIRCUMFLEX + REMAP(0x00E3, 0xE3) // LATIN SMALL LETTER A WITH TILDE + REMAP(0x00E4, 0xE4) // LATIN SMALL LETTER A WITH DIAERESIS + REMAP(0x00E5, 0xE5) // LATIN SMALL LETTER A WITH RING ABOVE + REMAP(0x00E6, 0xE6) // LATIN SMALL LETTER AE + REMAP(0x00E7, 0xE7) // LATIN SMALL LETTER C WITH CEDILLA + REMAP(0x00E8, 0xE8) // LATIN SMALL LETTER E WITH GRAVE + REMAP(0x00E9, 0xE9) // LATIN SMALL LETTER E WITH ACUTE + REMAP(0x00EA, 0xEA) // LATIN SMALL LETTER E WITH CIRCUMFLEX + REMAP(0x00EB, 0xEB) // LATIN SMALL LETTER E WITH DIAERESIS + REMAP(0x00EC, 0xEC) // LATIN SMALL LETTER I WITH GRAVE + REMAP(0x00ED, 0xED) // LATIN SMALL LETTER I WITH ACUTE + REMAP(0x00EE, 0xEE) // LATIN SMALL LETTER I WITH CIRCUMFLEX + REMAP(0x00EF, 0xEF) // LATIN SMALL LETTER I WITH DIAERESIS + + REMAP(0x00F0, 0xF0) // LATIN SMALL LETTER ETH + REMAP(0x00F1, 0xF1) // LATIN SMALL LETTER N WITH TILDE + REMAP(0x00F2, 0xF2) // LATIN SMALL LETTER O WITH GRAVE + REMAP(0x00F3, 0xF3) // LATIN SMALL LETTER O WITH ACUTE + REMAP(0x00F4, 0xF4) // LATIN SMALL LETTER O WITH CIRCUMFLEX + REMAP(0x00F5, 0xF5) // LATIN SMALL LETTER O WITH TILDE + REMAP(0x00F6, 0xF6) // LATIN SMALL LETTER O WITH DIAERESIS + REMAP(0x00F7, 0xF7) // DIVISION SIGN + REMAP(0x00F8, 0xF8) // LATIN SMALL LETTER O WITH STROKE + REMAP(0x00F9, 0xF9) // LATIN SMALL LETTER U WITH GRAVE + REMAP(0x00FA, 0xFA) // LATIN SMALL LETTER U WITH ACUTE + REMAP(0x00FB, 0xFB) // LATIN SMALL LETTER U WITH CIRCUMFLEX + REMAP(0x00FC, 0xFC) // LATIN SMALL LETTER U WITH DIAERESIS + REMAP(0x00FD, 0xFD) // LATIN SMALL LETTER Y WITH ACUTE + REMAP(0x00FE, 0xFE) // LATIN SMALL LETTER THORN + REMAP(0x00FF, 0xFF) // LATIN SMALL LETTER Y WITH DIAERESIS + } + } + + 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 +#undef REMAP } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 504bd12b3..e1fe37974 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -4,10 +4,7 @@ Wrapper class for an AdafruitGFX font Pre-calculates some font dimension info which InkHUD uses repeatedly - - Also contains an optional set of "substitutions". - These can be used to detect special UTF8 chars, and replace occurrences with a remapped char val to suit a custom font - These can also be used to swap UTF8 chars for a suitable ASCII substitution (e.g. German รถ -> oe, etc) + Re-encodes UTF-8 characters to suit extended ASCII AdafruitGFX fonts */ @@ -24,36 +21,67 @@ namespace NicheGraphics::InkHUD class AppletFont { public: + enum Encoding { + ASCII, + WINDOWS_1250, + WINDOWS_1251, + WINDOWS_1252, + }; + AppletFont(); - explicit AppletFont(const GFXfont &adafruitGFXFont); + AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); uint8_t lineHeight(); uint8_t heightAboveCursor(); uint8_t heightBelowCursor(); uint8_t widthBetweenWords(); // Width of the space character - void applySubstitutions(std::string *text); // Run all char-substitution operations, prior to printing - void addSubstitution(const char *from, const char *to); // Register a find-replace action, for remapping UTF8 chars - void addSubstitutionsWin1251(); // Cyrillic fonts: remap UTF8 values to their Win-1251 equivalent - // Todo: Polish font + std::string decodeUTF8(std::string encoded); const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font private: + uint32_t toUtf32(std::string utf8); + char applyEncoding(std::string utf8); + uint8_t height = 8; // Default value: in-built AdafruitGFX font uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font - // One pair of find-replace values, for substituting or remapping UTF8 chars - struct Substitution { - const char *from; - const char *to; - }; - - std::vector substitutions; // List of all character substitutions to run, prior to printing a string + Encoding encoding = ASCII; }; } // namespace NicheGraphics::InkHUD +// Macros for InkHUD's standard fonts +// -------------------------------------- + +// Use these once only, passing them to InkHUD::Applet::fontLarge and InkHUD::Applet:fontSmall +// 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) + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index ea7b74262..db0805f4e 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -286,6 +286,10 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) bool isOurNode = node->num == nodeDB->getNodeNum(); bool unknownHops = !node->has_hops_away && !isOurNode; + // Parse any non-ascii chars in the short name, + // and use last 4 instead if unknown / can't render + std::string shortName = parseShortName(node); + // We will draw a left or right hand variant, to place text towards screen center // Hopefully avoid text spilling off screen // Most values are the same, regardless of left-right handedness @@ -299,7 +303,7 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); // Common dimensions (left or right variant) - textW = getTextWidth(node->user.short_name); + textW = getTextWidth(shortName); if (textW == 0) paddingInnerW = 0; // If no text, no padding for text textH = fontSmall.lineHeight(); @@ -325,7 +329,7 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) drawRect(labelX, labelY, labelW, labelH, BLACK); // Short name - printAt(textX, textY, node->user.short_name, LEFT, MIDDLE); + printAt(textX, textY, shortName, LEFT, MIDDLE); // If the label is for our own node, // fade it by overdrawing partially with white diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 8ede40780..1b0bfa9d0 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -142,16 +142,18 @@ void InkHUD::NodeListApplet::onRender() meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); // -- Shortname -- - // use "?" if unknown - if (node && node->has_user) - shortName = node->user.short_name; + // Parse special chars in the short name + // Use "?" if unknown + if (node) + shortName = parseShortName(node); else shortName = "?"; // -- Longname -- - // use node id if unknown + // Parse special chars in long name + // Use node id if unknown if (node && node->has_user) - longName = node->user.long_name; // Found in nodeDB + longName = parse(node->user.long_name); // Found in nodeDB else { // Not found in nodeDB, show a hex nodeid instead longName = hexifyNodeNum(nodeNum); @@ -166,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 @@ -180,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/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp index b12ea4809..c52719e55 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp @@ -9,6 +9,12 @@ using namespace NicheGraphics; void InkHUD::BasicExampleApplet::onRender() { printAt(0, 0, "Hello, World!"); + + // If text might contain "special characters", is needs parsing first + // This applies to data such as text-messages and and node names + + // std::string greeting = parse("Grรผezi mitenand!"); + // printAt(0, 0, greeting); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h index f280afcda..22670a0f0 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -5,7 +5,7 @@ An example of an InkHUD applet. Tells us when a new text message arrives. -This applet makes use of the MeshModule API to detect new messages, +This applet makes use of the Module API to detect new messages, which is a general part of the Meshtastic firmware, and not part of InkHUD. In variants//nicheGraphics.h: diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index 520b3ef65..ecaa7cea3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -11,10 +11,20 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") OSThread::setIntervalFromNow(8 * 1000UL); OSThread::enabled = true; - textLeft = ""; - textRight = ""; - textTitle = xstr(APP_VERSION_SHORT); - fontTitle = fontSmall; + // 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) { + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + fontTitle = fontMedium; + textLeft = xstr(APP_VERSION_SHORT); + textRight = parseShortName(ourNode); + textTitle = "Meshtastic"; + } else { + fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = xstr(APP_VERSION_SHORT); + } bringToForeground(); // This is then drawn with a FULL refresh by Renderer::begin @@ -42,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()) { @@ -102,10 +146,11 @@ void InkHUD::LogoApplet::onShutdown() // Prepare for the powered-off screen now // We can change these values because the initial "shutting down" screen has already rendered at this point + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); textLeft = ""; textRight = ""; - textTitle = owner.short_name; - fontTitle = fontLarge; + textTitle = parseShortName(ourNode); + 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 4f8205647..c84ee09e0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -18,8 +18,9 @@ namespace NicheGraphics::InkHUD enum MenuAction { NO_ACTION, - SEND_NODEINFO, - SEND_POSITION, + SEND_PING, + STORE_CANNEDMESSAGE_SELECTION, + SEND_CANNEDMESSAGE, SHUTDOWN, NEXT_TILE, TOGGLE_BACKLIGHT, @@ -32,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 f59579230..a1f79a28f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -4,6 +4,8 @@ #include "RTC.h" +#include "MeshService.h" +#include "Router.h" #include "airtime.h" #include "main.h" #include "power.h" @@ -30,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() @@ -64,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 @@ -144,6 +156,24 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->nextTile(); break; + case SEND_PING: + service->refreshLocalMeshNode(); + service->trySendPosition(NODENUM_BROADCAST, true); + + // Force the next refresh to use FULL, to protect the display, as some users will probably spam this button + 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; @@ -175,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) @@ -235,6 +274,7 @@ void InkHUD::MenuApplet::execute(MenuItem item) void InkHUD::MenuApplet::showPage(MenuPage page) { items.clear(); + items.shrink_to_fit(); switch (page) { case ROOT: @@ -242,7 +282,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown - // items.push_back(MenuItem("Send", MenuPage::SEND)); // TODO + items.push_back(MenuItem("Send", MenuPage::SEND)); items.push_back(MenuItem("Options", MenuPage::OPTIONS)); // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); @@ -250,10 +290,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case SEND: - items.push_back(MenuItem("Send Message", MenuPage::EXIT)); - items.push_back(MenuItem("Send NodeInfo", MenuAction::SEND_NODEINFO)); - items.push_back(MenuItem("Send Position", MenuAction::SEND_POSITION)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + populateSendPage(); + break; + + case CANNEDMESSAGE_RECIPIENT: + populateRecipientPage(); break; case OPTIONS: @@ -284,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)); @@ -389,11 +434,14 @@ void InkHUD::MenuApplet::onRender() // Center-line for the text int16_t center = itemT + (itemH / 2); + // Box, if currently selected if (cursorShown && i == cursor) drawRect(itemL, itemT, itemW, itemH, BLACK); + + // Item's text printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); - // Testing only: circle instead of check box + // Checkbox, if relevant if (item.checkState) { const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left @@ -485,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 @@ -498,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. @@ -511,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 @@ -607,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/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index aa702c032..ae0836d19 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -33,11 +33,6 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket if (getFrom(p) == nodeDB->getNodeNum()) return 0; - // Abort if message was only an "emoji reaction" - // Possibly some implementation of this in future? - if (p->decoded.emoji) - return 0; - Notification n; n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time @@ -122,7 +117,7 @@ void InkHUD::NotificationApplet::onRender() int16_t textM = divX + padW + (getTextWidth(text) / 2); // Restrict area for printing - // - don't overlap border, or diveder + // - don't overlap border, or divider setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); // Drop shadow @@ -218,7 +213,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila // Sender id if (node && node->has_user) - text += node->user.short_name; + text += parseShortName(node); else text += hexifyNodeNum(message->sender); @@ -232,7 +227,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila // Sender id if (node && node->has_user) - text += node->user.short_name; + text += parseShortName(node); else text += hexifyNodeNum(message->sender); @@ -241,7 +236,8 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila } } - return text; + // Parse any non-ascii characters and return + return parse(text); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index 81de05b30..09931f109 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -12,20 +12,20 @@ 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 setFont(fontSmall); - std::string name = "Name: " + std::string(getDeviceName()); + std::string name = "Name: " + parse(getDeviceName()); if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " - name = std::string(getDeviceName()); + name = parse(getDeviceName()); if (getTextWidth(name) < width()) // Does it fit? printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); } 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 f7e2a8e9d..7c6232f3b 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -27,11 +27,6 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket * if (getFrom(p) == nodeDB->getNodeNum()) return 0; - // Abort if message was only an "emoji reaction" - // Possibly some implemetation of this in future? - if (p->decoded.emoji) - return 0; - requestAutoshow(); // Want to become foreground, if permitted requestUpdate(); // Want to update display, if applet is foreground @@ -72,13 +67,13 @@ void InkHUD::AllMessageApplet::onRender() } // Sender's id - // - shortname, if available, or + // - short name and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); if (sender && sender->has_user) { - header += sender->user.short_name; + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; - header += sender->user.long_name; + header += parse(sender->user.long_name); header += ")"; } else header += hexifyNodeNum(message->sender); @@ -100,19 +95,32 @@ void InkHUD::AllMessageApplet::onRender() // Print message text // =================== + // Parse any non-ascii chars in the message + std::string text = parse(message->text); + // 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(), message->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 - printWrapped(0, textTop, width(), message->text); + // Fallback (too large): print with fontSmall + setFont(fontSmall); + printWrapped(0, textTop, width(), text); } // Don't show notifications for text messages when our applet is displayed diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index 7a1d14f32..a3b9615a5 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -23,11 +23,6 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) if (!isActive()) return 0; - // Abort if only an "emoji reactions" - // Possibly some implemetation of this in future? - if (p->decoded.emoji) - return 0; - // If DM (not broadcast) if (!isBroadcast(p->to)) { // Want to update display, if applet is foreground @@ -68,13 +63,13 @@ void InkHUD::DMApplet::onRender() } // Sender's id - // - shortname, if available, or + // - shortname and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); if (sender && sender->has_user) { - header += sender->user.short_name; + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; - header += sender->user.long_name; + header += parse(sender->user.long_name); header += ")"; } else header += hexifyNodeNum(latestMessage->dm.sender); @@ -96,19 +91,32 @@ void InkHUD::DMApplet::onRender() // Print message text // =================== + // Parse any non-ascii chars in the message + std::string text = parse(latestMessage->dm.text); + // 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(), latestMessage->dm.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 - printWrapped(0, textTop, width(), latestMessage->dm.text); + // Fallback (too large): print with fontSmall + setFont(fontSmall); + printWrapped(0, textTop, width(), text); } // Don't show notifications for direct messages when our applet is displayed diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp index ceb9c01fe..5a659c606 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -16,7 +16,7 @@ void InkHUD::HeardApplet::onActivate() void InkHUD::HeardApplet::onDeactivate() { - // Avoid an unlikely situation where frquent activation / deactivation populated duplicate info from node DB + // Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB cards.clear(); } @@ -41,6 +41,7 @@ void InkHUD::HeardApplet::handleParsed(CardInfo c) cards.push_front(c); // Insert into base class' card collection cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen + cards.shrink_to_fit(); // Our rendered image needs to change if: if (previous.nodeNum != c.nodeNum // Different node @@ -54,7 +55,7 @@ void InkHUD::HeardApplet::handleParsed(CardInfo c) } // When applet is activated, pre-fill with stale data from NodeDB -// We're sorting using the last_heard value. Succeptible to weirdness if node's RTC changes. +// We're sorting using the last_heard value. Susceptible to weirdness if node's RTC changes. // No SNR is available in node db, so we can't calculate signal either // These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead void InkHUD::HeardApplet::populateFromNodeDB() @@ -72,7 +73,7 @@ void InkHUD::HeardApplet::populateFromNodeDB() return (top->last_heard > bottom->last_heard); }); - // Keep the most recent entries onlyt + // Keep the most recent entries only // Just enough to fill the screen if (ordered.size() > maxCards()) ordered.resize(maxCards()); diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp index 02aa4a721..1ccf7fc14 100644 --- a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp @@ -53,6 +53,7 @@ void InkHUD::RecentsListApplet::handleParsed(CardInfo c) cards.push_front(c); // Store this CardInfo cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen + cards.shrink_to_fit(); // Record the time of this observation // Used to count active nodes, and to know when to prune inactive nodes @@ -99,10 +100,12 @@ void InkHUD::RecentsListApplet::prune() if (!isActive(ages.at(i).seenAtMs)) { // Drop this item, and all others behind it ages.resize(i); + ages.shrink_to_fit(); cards.resize(i); + cards.shrink_to_fit(); // Request an update, if pruning did modify our data - // Required if pruning was scheduled. Redundent if pruning was prior to rendering. + // Required if pruning was scheduled. Redundant if pruning was prior to rendering. requestAutoshow(); requestUpdate(); diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index d7d2e79c8..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,29 +70,29 @@ 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); + 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 const int16_t dotsB = msgB; // Get dimensions for message text - uint16_t bodyH = getWrappedTextHeight(msgL, msgW, m.text); + uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText); int16_t bodyT = msgB - bodyH; // Print message // - if incoming if (!outgoing) - printWrapped(msgL, bodyT, msgW, m.text); + printWrapped(msgL, bodyT, msgW, bodyText); // Print message // - if outgoing else { - if (getTextWidth(m.text) < width()) // If short, - printAt(msgR, bodyT, m.text, RIGHT); // print right align - else // If long, - printWrapped(msgL, bodyT, msgW, m.text); // need printWrapped(), which doesn't support right align + if (getTextWidth(bodyText) < width()) // If short, + printAt(msgR, bodyT, bodyText, RIGHT); // print right align + else // If long, + printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align } // Move cursor up @@ -103,12 +104,16 @@ void InkHUD::ThreadedMessageApplet::onRender() // - shortname, if possible, or "me" // - time received, if possible std::string info; - if (sender && sender->has_user) - info += sender->user.short_name; - else if (outgoing) + if (outgoing) info += "Me"; - else - info += hexifyNodeNum(m.sender); + else { + // Check if sender is node db + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); + if (sender) + info += parseShortName(sender); // Handle any unprintable chars in short name + else + info += hexifyNodeNum(m.sender); // No node info at all. Print the node num + } std::string timeString = getTimeString(m.timestamp); if (timeString.length() > 0) { @@ -166,59 +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; - - // Abort if messages was an "emoji reaction" - // Possibly some implemetation of this in future? - if (p->decoded.emoji) - 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 ddd01b7e1..cdda1638d 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -3,11 +3,14 @@ #include "./Events.h" #include "RTC.h" +#include "buzz.h" +#include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" #include "./Applet.h" #include "./SystemApplet.h" +#include "graphics/niche/Utils/FlashData.h" using namespace NicheGraphics; @@ -25,6 +28,9 @@ void InkHUD::Events::begin() deepSleepObserver.observe(¬ifyDeepSleep); rebootObserver.observe(¬ifyReboot); textMessageObserver.observe(textMessageModule); +#if !MESHTASTIC_EXCLUDE_ADMIN + adminMessageObserver.observe((Observable *)adminModule); +#endif #ifdef ARCH_ESP32 lightSleepObserver.observe(¬ifyLightSleep); #endif @@ -32,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) { @@ -44,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) { @@ -97,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 } @@ -117,8 +138,13 @@ int InkHUD::Events::beforeReboot(void *unused) sa->onReboot(); } - inkhud->persistence->saveSettings(); - inkhud->persistence->saveLatestMessage(); + // Save settings to flash, or erase if factory reset in progress + if (!eraseOnReboot) { + inkhud->persistence->saveSettings(); + inkhud->persistence->saveLatestMessage(); + } else { + NicheGraphics::clearFlashData(); + } // Note: no forceUpdate call here // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen @@ -136,11 +162,6 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) if (getFrom(packet) == nodeDB->getNodeNum()) return 0; - // Short circuit: don't store "emoji reactions" - // Possibly some implementation of this in future? - if (packet->decoded.emoji) - return 0; - // Determine whether the message is broadcast or a DM // Store this info to prevent confusion after a reboot // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set @@ -171,6 +192,24 @@ 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(AdminModule_ObserverData *data) +{ + 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: + break; + } + + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) +} + #ifdef ARCH_ESP32 // Callback for lightSleepObserver // Make sure the display is not partway through an update when we begin light sleep @@ -182,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 6a6e9d7a2..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,6 +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(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); // Prepare for light sleep #endif @@ -52,10 +53,20 @@ class Events CallbackObserver textMessageObserver = CallbackObserver(this, &Events::onReceiveTextMessage); + // Get notified of incoming admin messages, and handle any which are relevant to InkHUD + 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; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp index ac6fe1b35..94e0aa661 100644 --- a/src/graphics/niche/InkHUD/MessageStore.cpp +++ b/src/graphics/niche/InkHUD/MessageStore.cpp @@ -22,6 +22,8 @@ InkHUD::MessageStore::MessageStore(std::string label) } // Write the contents of the MessageStore::messages object to flash +// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. +// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally void InkHUD::MessageStore::saveToFlash() { assert(!filename.empty()); @@ -29,7 +31,9 @@ void InkHUD::MessageStore::saveToFlash() #ifdef FSCom // Make the directory, if doesn't already exist // This is the same directory accessed by NicheGraphics::FlashData + spiLock->lock(); FSCom.mkdir("/NicheGraphics"); + spiLock->unlock(); // Open or create the file // No "full atomic": don't save then rename @@ -37,6 +41,9 @@ void InkHUD::MessageStore::saveToFlash() LOG_INFO("Saving messages in %s", filename.c_str()); + // Take firmware's SPI Lock while writing + spiLock->lock(); + // 1st byte: how many messages will be written to store f.write(messages.size()); @@ -51,6 +58,9 @@ void InkHUD::MessageStore::saveToFlash() LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); } + // Release firmware's SPI lock, because SafeFile::close needs it + spiLock->unlock(); + bool writeSucceeded = f.close(); if (!writeSucceeded) { @@ -63,6 +73,7 @@ void InkHUD::MessageStore::saveToFlash() // Attempt to load the previous contents of the MessageStore:message deque from flash. // Filename is controlled by the "label" parameter +// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. void InkHUD::MessageStore::loadFromFlash() { // Hopefully redundant. Initial intention is to only load / save once per boot. @@ -70,6 +81,9 @@ void InkHUD::MessageStore::loadFromFlash() #ifdef FSCom + // Take the firmware's SPI Lock, in case filesystem is on SD card + concurrency::LockGuard guard(spiLock); + // Check that the file *does* actually exist if (!FSCom.exists(filename.c_str())) { LOG_WARN("'%s' not found. Using default values", filename.c_str()); 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 07fe6c942..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. @@ -13,7 +13,7 @@ This document is intended as a reference for maintainers. A haphazard collection - [Non-interactive](#non-interactive) - [Customizable](#customizable) - [Event-Driven Rendering](#event-driven-rendering) - - [No `#ifdef` spaghetti](#no-ifdef-spaghetti) + - [Avoid the Preprocessor](#avoid-the-preprocessor) - [The Implementation](#the-implementation) - [The Rendering Process](#the-rendering-process) - [Concepts](#concepts) @@ -23,6 +23,10 @@ This document is intended as a reference for maintainers. A haphazard collection - [Adding a Variant](#adding-a-variant) - [platformio.ini](#platformioini) - [nicheGraphics.h](#nichegraphicsh) +- [Fonts](#fonts) + - [Parsing Unicode Text](#parsing-unicode-text) + - [Localization](#localization) + - [Creating / Modifying](#creating--modifying) - [Class Notes](#class-notes) - [`InkHUD::InkHUD`](#inkhudinkhud) - [`InkHUD::Persistence`](#inkhudpersistence) @@ -73,9 +77,11 @@ The user should be given the choice to decide which information they would like The display image does not update "automatically". Individual applets are responsible for deciding when they have new information to show, and then requesting a display update. -### No `#ifdef` spaghetti +### Avoid the Preprocessor -**Don't** use preprocessor macros for device-specific configuration. This should be achieved with config methods, in [`nicheGraphics.h`](#nichegraphicsh). +**Don't** use preprocessor macros to write code which targets individual devices. + +**Do** configure InkHUD to suit each device in [`nicheGraphics.h`](#nichegraphicsh). **Do** use preprocessor macros to guard all files @@ -103,7 +109,7 @@ The display image does not update "automatically". Individual applets are respon (animated diagram) - + An overview: @@ -281,11 +287,14 @@ ${esp32s3_base.lib_deps} ### nicheGraphics.h -โ Wrap this file in `#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS` +Should contain a `setupNicheGraphics` method, which creates and configures the various components for InkHUD. -`nicheGraphics.h` should be placed in the same folder as your variant's `platformio.ini`. If this is not possible, modify `build_src_filter`. +For well commented examples, see: -`nicheGraphics.h` should contain a `setupNicheGraphics` method, which creates and configures the various components for InkHUD. +- `/variants/heltec_vision_master_e290/nicheGraphics.h` (ESP32) +- `/variants/ELECROW-ThinkNode-M1/nicheGraphics.h` (NRF52) + +As a general overview: - Display - Start SPI @@ -301,10 +310,85 @@ ${esp32s3_base.lib_deps} - Setup `TwoButton` driver (user button, optional "auxiliary" button) - Connect to InkHUD handlers (use lambdas) -For well commented examples, see: +## Fonts -- `variants/heltec_vision_master_e290/nicheGraphics.h` (ESP32) -- `variants/t-echo/nicheGraphics.h` (NRF52) +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_12PT_WIN1252; +InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; +InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + +// Using a generic AdafruitGFX font instead: +// 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 and emoji. + +### Parsing Unicode Text + +Text received by the firmware is encoded as UTF-8. + +Applets must manually parse any text which may contain non-ASCII characters. Strings like text-messages and node names should be parsed. + +```cpp +std::string greeting = "Gรณรฐan daginn!"; +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: + +- Windows-1250 (Central European) +- Windows-1251 (Cyrillic) +- Windows-1252 (Western European) + +The default builds use Windows-1252 encoding. This can be changed in nicheGraphics.h. + +```cpp +InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1250; +InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1250; +InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1250; + +InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1251; +InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1251; +InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1251; +``` + +### Creating / Modifying + +For basic conversion and editing, online tools might be sufficient: + +- [https://rop.nl/truetype2gfx/](https://rop.nl/truetype2gfx/) - converting from ttf +- [https://tchapi.github.io/Adafruit-GFX-Font-Customiser/](https://tchapi.github.io/Adafruit-GFX-Font-Customiser/) - editing glyphs + +For heavy editing, this offline workflow is suggested: + +- [FontForge](https://fontforge.org/en-US/) + - re-ordering glyphs + - Encoding > Load Encoding + - Encoding > Reencode + - .ttf to .bdf conversion + - Element > Bitmap Strikes Available.. + - File > Generate Fonts +- [GFXFontEditor](https://github.com/ScottFerg56/GFXFontEditor) + - manual glyph correction + - .bdf to AdafruitGFX .h conversion + - File > Edit Font Properties + - right-click glyph list, flatten font + - File > Save As + - manually edit exported .h + - remove `#include ` + +If possible, custom Extended-ASCII fonts should use one of the encodings which InkHUD already supports. If this is not possible, a mapping for the new encoding will need to be added. + +See [Encoding](#encoding) for details on using an extended-ASCII font. ## Class Notes @@ -502,6 +586,10 @@ Applets themselves do also listen separately for various events, but for the pur Button input is sometimes handled by a system applet. `InkHUD::Events` determines whether the button should be handled by a specific system applet, or should instead trigger a default behavior +#### Factory Reset + +The Events class handles the admin messages(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory. We do this because some applets (e.g. ThreadedMessageApplet) save their own data to flash, so if we erased earlier, that data would get re-written during reboot. + --- ### `InkHUD::Applet` @@ -624,17 +712,63 @@ The default AdafruitGFX text handling places characters "upon a line", as if han The height of this box is `AppletFont::lineHeight`, which is the height of the tallest character in the font. This gives us a fixed-height for text, which is much tighter than with AdafruitGFX's default line spacing. -#### UTF-8 Substitutions +#### Encoding -To enable non-English text, the `AppletFont` class includes a mechanism to detect specific UTF-8 characters, and replace them with alternative glyphs from the AdafruitGFX font. This can be used to remap characters for a custom font, or to offer a suitable ASCII replacement. +An AppletFont may be constructed from a standard 7bit ASCII AdafruitGFX font, however InkHUD also supports 8bit extended-ASCII fonts. + +For this, the encoding must be specified when instantiating the AppletFont. ```cpp -// With a custom font -// ั is ASCII 0xBF, in Windows-1251 encoding -addSubstitution("ั", "\xBF"); - -// Substitution (with a default font) -addSubstitution("รถ", "oe"); +InkHUD::AppletFont(FreeSans9pt_Win1250, InkHUD::AppletFont::WINDOWS_1250); ``` -These substitutions should be performed in a variant's `setupNicheGraphics` method. For convenience, some common ASCII encodings have ready-to-go sets of substitutions you can apply, for example `AppletFont::addSubstitutionsWin1251` +Currently supported encodings are: + +- ASCII +- Windows-1250 (Central European) +- Windows-1251 (Cyrillic) +- Windows-1252 (Western European) + +To add support for additional encodings, add to the `AppletFont::Encodings` enum, and then define the mapping from unicode in `AppletFont::applyEncoding`. + +#### Custom Line Height + +Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritcs. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. + +```cpp +// -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/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index 10d89ef41..bd29f981d 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -2,6 +2,7 @@ #include "./TwoButton.h" +#include "NodeDB.h" // For the helper function TwoButton::getUserButtonPin #include "PowerFSM.h" #include "sleep.h" @@ -57,16 +58,48 @@ void TwoButton::stop() detachInterrupt(buttons[1].pin); } +// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings +// This helper method isn't used by the TweButton class itself, it could be moved elsewhere. +// Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. +uint8_t TwoButton::getUserButtonPin() +{ + uint8_t pin = 0xFF; // Unset + + // Use default pin for variant, if no better source +#ifdef BUTTON_PIN + pin = BUTTON_PIN; +#endif + + // From userPrefs.jsonc, if set +#ifdef USERPREFS_BUTTON_PIN + pin = USERPREFS_BUTTON_PIN; +#endif + + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; + + return pin; +} + // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; + } + } + assert(whichButton < 2); buttons[whichButton].pin = pin; - buttons[whichButton].activeLogic = LOW; - buttons[whichButton].mode = internalPullup ? INPUT_PULLUP : INPUT; // fix me + buttons[whichButton].activeLogic = LOW; // Unimplemented - pinMode(buttons[whichButton].pin, buttons[whichButton].mode); + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) @@ -148,7 +181,7 @@ void TwoButton::isrSecondary() void TwoButton::startThread() { if (!OSThread::enabled) { - OSThread::setInterval(50); + OSThread::setInterval(10); OSThread::enabled = true; } } @@ -265,7 +298,9 @@ int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) // Manually trigger the button-down ISR // - during light sleep, our ISR is disabled // - if light sleep ends by button press, pretend our own ISR caught it - if (cause == ESP_SLEEP_WAKEUP_GPIO) + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) isrPrimary(); return 0; // Indicates success diff --git a/src/graphics/niche/Inputs/TwoButton.h b/src/graphics/niche/Inputs/TwoButton.h index 1e1576256..ae66adf96 100644 --- a/src/graphics/niche/Inputs/TwoButton.h +++ b/src/graphics/niche/Inputs/TwoButton.h @@ -30,10 +30,12 @@ class TwoButton : protected concurrency::OSThread public: typedef std::function Callback; + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + static TwoButton *getInstance(); // Create or get the singleton instance void start(); // Start handling button input void stop(); // Stop handling button input (disconnect ISRs for sleep) - void setWiring(uint8_t whichButton, uint8_t pin, bool internalPulldown = false); + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setHandlerDown(uint8_t whichButton, Callback onDown); void setHandlerUp(uint8_t whichButton, Callback onUp); @@ -62,8 +64,7 @@ class TwoButton : protected concurrency::OSThread public: // Per-button config uint8_t pin = 0xFF; // 0xFF: unset - bool activeLogic = LOW; // Active LOW by default. Todo: remove, unused - uint8_t mode = INPUT; // Whether to use internal pull up / pull down resistors + bool activeLogic = LOW; // Active LOW by default. Currently unimplemented. uint32_t debounceLength = 50; // Minimum length for shortpress, in ms uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms volatile State state = State::REST; // Internal state 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 75% rename from src/graphics/niche/FlashData.h rename to src/graphics/niche/Utils/FlashData.h index 8a63c6108..233d0922e 100644 --- a/src/graphics/niche/FlashData.h +++ b/src/graphics/niche/Utils/FlashData.h @@ -13,6 +13,7 @@ Avoid bloating everyone's protobuf code for our one-off UI implementations #include "configuration.h" +#include "SPILock.h" #include "SafeFile.h" namespace NicheGraphics @@ -46,6 +47,9 @@ template class FlashData public: static bool load(T *data, const char *label) { + // Take firmware's SPI lock + concurrency::LockGuard guard(spiLock); + // Set false if we run into issues bool okay = true; @@ -103,14 +107,18 @@ template class FlashData return okay; } - // Save module's custom data (settings?) to flash. Does use protobufs + // Save module's custom data (settings?) to flash. Doesn't use protobufs + // Takes the firmware's SPI lock, in case the files are stored on SD card + // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally. static void save(T *data, const char *label) { // Get a filename based on the label std::string filename = getFilename(label); #ifdef FSCom + spiLock->lock(); FSCom.mkdir("/NicheGraphics"); + spiLock->unlock(); auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. @@ -119,10 +127,10 @@ template class FlashData // Calculate a hash of the data uint32_t hash = getHash(data); + spiLock->lock(); f.write((uint8_t *)data, sizeof(T)); // Write the actual data f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash - - // f.flush(); + spiLock->unlock(); bool writeSucceeded = f.close(); @@ -135,6 +143,32 @@ template class FlashData } }; +// Erase contents of the NicheGraphics data directory +inline void clearFlashData() +{ + + // Take firmware's SPI lock, in case the files are stored on SD card + concurrency::LockGuard guard(spiLock); + +#ifdef FSCom + File dir = FSCom.open("/NicheGraphics"); // Open the directory + File file = dir.openNextFile(); // Attempt to open the first file in the directory + + // While the directory still contains files + while (file) { + std::string path = "/NicheGraphics/"; + path += file.name(); + LOG_DEBUG("Erasing %s", path.c_str()); + file.close(); + FSCom.remove(path.c_str()); + + file = dir.openNextFile(); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented\n"); +#endif +} + } // namespace NicheGraphics #endif \ No newline at end of file diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index cacb02694..b2e92bdae 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -11,6 +11,7 @@ #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" +#include #endif DeviceScreen *deviceScreen = nullptr; @@ -26,12 +27,10 @@ CallbackObserver endSleepObserver = void tft_task_handler(void *param = nullptr) { while (true) { - if (deviceScreen) { - spiLock->lock(); - deviceScreen->task_handler(); - spiLock->unlock(); - deviceScreen->sleep(); - } + spiLock->lock(); + deviceScreen->task_handler(); + spiLock->unlock(); + deviceScreen->sleep(); } } @@ -44,10 +43,10 @@ void tftSetup(void) #else if (settingsMap[displayPanel] != no_screen) { DisplayDriverConfig displayConfig; - static char *panels[] = {"NOSCREEN", "X11", "ST7789", "ST7735", "ST7735S", "ST7796", - "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; + static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", + "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; -#ifdef USE_X11 +#if defined(USE_X11) if (settingsMap[displayPanel] == x11) { if (settingsMap[displayWidth] && settingsMap[displayHeight]) displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth], @@ -55,6 +54,14 @@ void tftSetup(void) else displayConfig.device(DisplayDriverConfig::device_t::X11); } else +#elif defined(USE_FRAMEBUFFER) + if (settingsMap[displayPanel] == fb) { + if (settingsMap[displayWidth] && settingsMap[displayHeight]) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)settingsMap[displayWidth], + (uint16_t)settingsMap[displayHeight]); + else + displayConfig.device(DisplayDriverConfig::device_t::FB); + } else #endif { displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) @@ -116,11 +123,15 @@ void tftSetup(void) } #endif + if (deviceScreen) { #ifdef ARCH_ESP32 - tftSleepObserver.observe(¬ifyLightSleep); - endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + tftSleepObserver.observe(¬ifyLightSleep); + endSleepObserver.observe(¬ifyLightSleepEnd); + xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); +#elif defined(ARCH_PORTDUINO) + std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif + } } #endif \ No newline at end of file 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
+ Meshtastic is an open source project for creating off-grid, affordable, and resilient communication with LoRa mesh networks. +