diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 62f0b7ead..d599f447f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,9 +1,10 @@ +# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue +# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions +# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12 USER root -# trunk-ignore(terrascan/AC_DOCKER_0002): Known terrascan issue -# trunk-ignore(hadolint/DL3008): Use latest version of packages RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ ca-certificates \ @@ -27,9 +28,11 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ hwdata \ gpg \ gnupg2 \ + libusb-1.0-0-dev \ + libi2c-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* -RUN pipx install platformio==6.1.15 +RUN pipx install platformio COPY 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml index f2d2f6507..f638b9018 100644 --- a/.github/ISSUE_TEMPLATE/Bug Report.yml +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -1,7 +1,7 @@ name: Bug Report description: File a bug report title: "[Bug]: " -labels: ["bug", "triage"] +labels: [bug, triage] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/New Board.yml b/.github/ISSUE_TEMPLATE/New Board.yml index c71ed4ba2..90b2a9bf9 100644 --- a/.github/ISSUE_TEMPLATE/New Board.yml +++ b/.github/ISSUE_TEMPLATE/New Board.yml @@ -1,7 +1,7 @@ name: New Board description: Request us to support new hardware title: "[Board]: " -labels: ["enhancement", "triage"] +labels: [enhancement, triage] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index b50ccac26..311f097c4 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,7 +1,7 @@ name: Feature Request description: Request a new feature title: "[Feature Request]: " -labels: ["enhancement"] +labels: [enhancement] body: - type: markdown attributes: diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 000000000..f7bf95f83 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +# Configuration related to self-hosted runner. +self-hosted-runner: + # Labels of self-hosted runner in array of strings. + labels: + - test-runner diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index b24a5fc12..2f0883fad 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -34,7 +34,7 @@ inputs: arch: description: Processor arch name required: true - default: "esp32" + default: esp32 runs: using: composite diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 7364c4ddb..7cd0dfcac 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -1,13 +1,13 @@ -name: "Setup Build Base Composite Action" -description: "Base build actions for Meshtastic Platform IO steps" +name: Setup Build Base Composite Action +description: Base build actions for Meshtastic Platform IO steps runs: - using: "composite" + using: composite steps: - name: Checkout code uses: actions/checkout@v4 with: - submodules: "recursive" + submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 616c16ce2..a7b4a0f1b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,26 +1,27 @@ +#trunk-ignore-all(yamllint/quoted-strings): required by dependabot syntax check version: 2 updates: - package-ecosystem: docker - directory: devcontainer + directory: /.devcontainer schedule: interval: daily - time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check + time: "05:00" timezone: US/Pacific - package-ecosystem: docker directory: / schedule: interval: daily - time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check + time: "05:00" timezone: US/Pacific - package-ecosystem: gitsubmodule directory: / schedule: interval: daily - time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check + time: "05:00" timezone: US/Pacific - package-ecosystem: github-actions directory: /.github/workflows schedule: interval: daily - time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check + time: "05:00" timezone: US/Pacific diff --git a/.github/workflows/build_nrf52.yml b/.github/workflows/build_nrf52.yml index ce26838f2..786508f86 100644 --- a/.github/workflows/build_nrf52.yml +++ b/.github/workflows/build_nrf52.yml @@ -7,6 +7,8 @@ on: required: true type: string +permissions: read-all + jobs: build-nrf52: runs-on: ubuntu-latest diff --git a/.github/workflows/build_rpi2040.yml b/.github/workflows/build_rpi2040.yml index 492a1f010..53fee34d2 100644 --- a/.github/workflows/build_rpi2040.yml +++ b/.github/workflows/build_rpi2040.yml @@ -7,6 +7,8 @@ on: required: true type: string +permissions: read-all + jobs: build-rpi2040: runs-on: ubuntu-latest diff --git a/.github/workflows/build_stm32.yml b/.github/workflows/build_stm32.yml index b463bab71..dc469d994 100644 --- a/.github/workflows/build_stm32.yml +++ b/.github/workflows/build_stm32.yml @@ -7,6 +7,8 @@ on: required: true type: string +permissions: read-all + jobs: build-stm32: runs-on: ubuntu-latest diff --git a/.github/workflows/generate-userprefs.yml b/.github/workflows/generate-userprefs.yml deleted file mode 100644 index 10dd1ff7d..000000000 --- a/.github/workflows/generate-userprefs.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Generate UsersPrefs JSON manifest - -on: - push: - paths: - - userPrefs.h - branches: - - master - -jobs: - generate-userprefs: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Clang - run: sudo apt-get install -y clang - - - name: Install trunk - run: curl https://get.trunk.io -fsSL | bash - - - name: Generate userPrefs.jsom - run: python3 ./bin/build-userprefs-json.py - - - name: Trunk format json - run: trunk format userPrefs.json - - - name: Commit userPrefs.json - run: | - git config --global user.email "actions@github.com" - git config --global user.name "GitHub Actions" - git add userPrefs.json - git commit -m "Update userPrefs.json" - git push diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 7062ef525..da4b4e6f3 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -135,10 +135,10 @@ jobs: build_location: local secrets: inherit - package-pio-deps-native: + package-pio-deps-native-tft: uses: ./.github/workflows/package_pio_deps.yml with: - pio_env: native + pio_env: native-tft secrets: inherit test-native: @@ -288,7 +288,7 @@ jobs: needs: - gather-artifacts - build-debian-src - - package-pio-deps-native + - package-pio-deps-native-tft steps: - name: Checkout uses: actions/checkout@v4 @@ -324,10 +324,10 @@ jobs: merge-multiple: true path: ./output/debian-src - - name: Download native pio deps + - name: Download `native-tft` pio deps uses: actions/download-artifact@v4 with: - pattern: platformio-deps-native-${{ steps.version.outputs.long }} + pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }} merge-multiple: true path: ./output/pio-deps-native @@ -352,6 +352,12 @@ jobs: run: >- bin/bump_version.py + - 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 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7a35e2b99..36ec22f17 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -9,7 +9,7 @@ permissions: read-all jobs: trunk_check: name: Trunk Check and Upload - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -21,8 +21,9 @@ jobs: trunk-token: ${{ secrets.TRUNK_TOKEN }} trunk_upgrade: + # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades name: Trunk Upgrade (PR) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write # For trunk to create PRs pull-requests: write # For trunk to create PRs @@ -30,6 +31,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # See https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades - name: Trunk Upgrade uses: trunk-io/trunk-action/upgrade@v1 + with: + base: master diff --git a/.github/workflows/sec_sast_flawfinder.yml b/.github/workflows/sec_sast_flawfinder.yml deleted file mode 100644 index 99cc72190..000000000 --- a/.github/workflows/sec_sast_flawfinder.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Flawfinder Scan - -on: - push: - branches: [master, develop] - paths-ignore: - - "**.md" - - "version.properties" - -jobs: - flawfinder: - runs-on: ubuntu-latest - name: Flawfinder - - steps: - # step 1 - - name: clone application source code - uses: actions/checkout@v4 - - # step 2 - - name: flawfinder_scan - uses: david-a-wheeler/flawfinder@2.0.19 - with: - arguments: "--sarif ./" - output: "flawfinder_report.sarif" - - # step 3 - - name: save report as pipeline artifact - uses: actions/upload-artifact@v4 - with: - name: flawfinder_report.sarif - overwrite: true - path: flawfinder_report.sarif - - # step 4 - - name: publish code scanning alerts - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: flawfinder_report.sarif - category: flawfinder diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index 54bbbe6d2..944103562 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -3,10 +3,10 @@ name: Semgrep Full Scan on: workflow_dispatch: - branches: - - master schedule: - - cron: "0 1 * * 6" + - cron: 0 1 * * 6 + +permissions: read-all jobs: semgrep-full: diff --git a/.github/workflows/sec_sast_semgrep_pull.yml b/.github/workflows/sec_sast_semgrep_pull.yml index 9013f1c74..527a5c076 100644 --- a/.github/workflows/sec_sast_semgrep_pull.yml +++ b/.github/workflows/sec_sast_semgrep_pull.yml @@ -2,6 +2,8 @@ name: Semgrep Differential Scan on: pull_request +permissions: read-all + jobs: semgrep-diff: runs-on: ubuntu-22.04 diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 0ce0579de..19b7cf7fd 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v9.0.0 + uses: actions/stale@v9.1.0 with: exempt-issue-labels: pinned,3.0 exempt-pr-labels: pinned,3.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c9489db1a..0f0ee0af4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,11 @@ name: End to end tests on: schedule: - - cron: "0 0 * * *" # Run every day at midnight + - cron: 0 0 * * * # Run every day at midnight workflow_dispatch: {} +permissions: read-all + jobs: native-tests: uses: ./.github/workflows/test_native.yml diff --git a/.github/workflows/trunk_annotate.pr.yml b/.github/workflows/trunk_annotate_pr.yml similarity index 95% rename from .github/workflows/trunk_annotate.pr.yml rename to .github/workflows/trunk_annotate_pr.yml index ac5cdc0d5..62c1c01b7 100644 --- a/.github/workflows/trunk_annotate.pr.yml +++ b/.github/workflows/trunk_annotate_pr.yml @@ -11,7 +11,7 @@ permissions: read-all jobs: trunk_check: name: Trunk Code Quality Annotate - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: checks: write # For trunk to post annotations contents: read # For repo checkout diff --git a/.github/workflows/trunk_check.yml b/.github/workflows/trunk_check.yml index 6ed905bc8..55656bf48 100644 --- a/.github/workflows/trunk_check.yml +++ b/.github/workflows/trunk_check.yml @@ -9,7 +9,7 @@ permissions: read-all jobs: trunk_check: name: Trunk Check Runner - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: checks: write # For trunk to post annotations contents: read # For repo checkout @@ -20,3 +20,5 @@ jobs: - name: Trunk Check uses: trunk-io/trunk-action@v1 + with: + save-annotations: true diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml index 0d6eb6041..33f4182eb 100644 --- a/.github/workflows/trunk_format_pr.yml +++ b/.github/workflows/trunk_format_pr.yml @@ -4,11 +4,15 @@ on: issue_comment: types: [created] +permissions: read-all + jobs: trunk-fmt: if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt') runs-on: ubuntu-latest - + permissions: + contents: write + pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index e7b3c1f40..5aa295b89 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -1,10 +1,14 @@ name: Update protobufs and regenerate classes on: workflow_dispatch +permissions: read-all + jobs: update-protobufs: runs-on: ubuntu-latest - + permissions: + contents: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.gitmodules b/.gitmodules index 7c54ad513..964204476 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ [submodule "protobufs"] path = protobufs url = https://github.com/meshtastic/protobufs.git +[submodule "lib/device-ui"] + path = lib/device-ui + url = https://github.com/meshtastic/device-ui.git [submodule "meshtestic"] path = meshtestic url = https://github.com/meshtastic/meshTestic diff --git a/.trunk/configs/.clang-tidy b/.trunk/configs/.clang-tidy new file mode 100644 index 000000000..e4bd819fa --- /dev/null +++ b/.trunk/configs/.clang-tidy @@ -0,0 +1,39 @@ +Checks: >- + bugprone-*, + cppcoreguidelines-*, + google-*, + misc-*, + modernize-*, + performance-*, + readability-*, + -bugprone-lambda-function-name, + -bugprone-reserved-identifier, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-type-vararg, + -google-readability-braces-around-statements, + -google-readability-function-size, + -misc-no-recursion, + -modernize-return-braced-init-list, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -performance-unnecessary-value-param, + -readability-magic-numbers, + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 100 + - key: readability-function-cognitive-complexity.IgnoreMacros + value: true + # Set naming conventions for your style below (there are dozens of naming settings possible): + # See https://clang.llvm.org/extra/clang-tidy/checks/readability/identifier-naming.html + # - key: readability-identifier-naming.ClassCase + # value: CamelCase + # - key: readability-identifier-naming.NamespaceCase + # value: lower_case + # - key: readability-identifier-naming.PrivateMemberSuffix + # value: _ + # - key: readability-identifier-naming.StructCase + # value: CamelCase diff --git a/.trunk/configs/.markdownlint.yaml b/.trunk/configs/.markdownlint.yaml index fb940393d..6486f050e 100644 --- a/.trunk/configs/.markdownlint.yaml +++ b/.trunk/configs/.markdownlint.yaml @@ -8,3 +8,4 @@ line_length: false spaces: false url: false whitespace: false +headings: false diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index f2393592c..78fdb8e45 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,37 +1,36 @@ version: 0.1 cli: - version: 1.22.8 + version: 1.22.10 plugins: sources: - id: trunk - ref: v1.6.6 + ref: v1.6.7 uri: https://github.com/trunk-io/plugins lint: enabled: - - prettier@3.4.2 - - trufflehog@3.86.1 + - prettier@3.5.2 + - trufflehog@3.88.14 - yamllint@1.35.1 - - bandit@1.8.0 - - checkov@3.2.334 + - bandit@1.8.3 + - checkov@3.2.378 - terrascan@1.19.9 - - trivy@0.58.0 - #- trufflehog@3.63.2-rc0 + - trivy@0.59.1 - taplo@0.9.3 - - ruff@0.8.3 - - isort@5.13.2 - - markdownlint@0.43.0 - - oxipng@9.1.3 + - ruff@0.9.7 + - isort@6.0.1 + - markdownlint@0.44.0 + - oxipng@9.1.4 - svgo@3.3.2 - - actionlint@1.7.4 - - flake8@7.1.1 + - actionlint@1.7.7 + - flake8@7.1.2 - hadolint@2.12.1-beta - shfmt@3.6.0 - shellcheck@0.10.0 - - black@24.10.0 + - black@25.1.0 - git-diff-check - - gitleaks@8.21.2 + - gitleaks@8.24.0 - clang-format@16.0.3 - #- prettier@3.3.3 + - clang-tidy@16.0.3 ignore: - linters: [ALL] paths: diff --git a/Dockerfile b/Dockerfile index f9a3b9962..fd1bb6164 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,23 @@ # trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue -# trunk-ignore-all(hadolint/DL3008): Use latest version of apt packages for buildchain # 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.12-bookworm AS builder +FROM python:3.13-bookworm AS builder 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 \ +RUN apt-get update && apt-get install --no-install-recommends -y \ + wget g++ zip git ca-certificates \ libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev \ - libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config && \ - apt-get clean && rm -rf /var/lib/apt/lists/* && \ - pip install --no-cache-dir -U platformio==6.1.16 && \ - mkdir /tmp/firmware + libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config \ + && apt-get clean && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir -U platformio \ + && mkdir /tmp/firmware # Copy source code WORKDIR /tmp/firmware @@ -35,8 +37,9 @@ ENV TZ=Etc/UTC # nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root USER root -RUN apt-get update && apt-get --no-install-recommends -y install libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 && \ - apt-get clean && rm -rf /var/lib/apt/lists/* \ +RUN apt-get update && apt-get --no-install-recommends -y install \ + libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 \ + && 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 diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 8b48eeca3..b6d91a75a 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -1,14 +1,18 @@ # 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.12-alpine3.21 AS builder +FROM python:3.13-alpine3.21 AS builder ENV PIP_ROOT_USER_ACTION=ignore -RUN apk add bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \ - libusb-dev i2c-tools-dev openssl-dev pkgconf argp-standalone && \ - pip install --no-cache-dir -U platformio==6.1.16 && \ - mkdir /tmp/firmware +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 openssl-dev pkgconf argp-standalone \ + && rm -rf /var/cache/apk/* \ + && pip install --no-cache-dir -U platformio \ + && mkdir /tmp/firmware WORKDIR /tmp/firmware COPY . /tmp/firmware @@ -27,7 +31,9 @@ FROM alpine:3.21 # nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root USER root -RUN apk add libstdc++ libgpiod yaml-cpp libusb i2c-tools \ +RUN apk --no-cache add \ + libstdc++ libgpiod yaml-cpp libusb i2c-tools \ + && rm -rf /var/cache/apk/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ && mkdir -p /etc/meshtasticd/ssl diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index d6a756bec..256781ba1 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -2,7 +2,7 @@ [esp32_base] extends = arduino_base custom_esp32_kind = esp32 -platform = platformio/espressif32@6.9.0 +platform = platformio/espressif32@6.10.0 build_src_filter = ${arduino_base.build_src_filter} - - - - - @@ -37,6 +37,7 @@ build_flags = -DLIBPAX_ARDUINO -DLIBPAX_WIFI -DLIBPAX_BLE + -DHAS_UDP_MULTICAST=1 ;-DDEBUG_HEAP lib_deps = @@ -45,9 +46,9 @@ lib_deps = ${environmental_base.lib_deps} ${radiolib_base.lib_deps} https://github.com/meshtastic/esp32_https_server.git#23665b3adc080a311dcbb586ed5941b5f94d6ea2 - h2zero/NimBLE-Arduino@^1.4.2 + h2zero/NimBLE-Arduino@^1.4.3 https://github.com/dbinfrago/libpax.git#3cdc0371c375676a97967547f4065607d4c53fd1 - lewisxhe/XPowersLib@^0.2.6 + lewisxhe/XPowersLib@^0.2.7 https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f rweather/Crypto@^0.4.0 @@ -65,4 +66,4 @@ lib_ignore = ; customize the partition table ; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables -board_build.partitions = partition-table.csv \ No newline at end of file +board_build.partitions = partition-table.csv diff --git a/arch/esp32/esp32c6.ini b/arch/esp32/esp32c6.ini index 3f8b1bdbe..d0425812f 100644 --- a/arch/esp32/esp32c6.ini +++ b/arch/esp32/esp32c6.ini @@ -24,7 +24,7 @@ lib_deps = ${networking_base.lib_deps} ${environmental_base.lib_deps} ${radiolib_base.lib_deps} - lewisxhe/XPowersLib@^0.2.6 + lewisxhe/XPowersLib@^0.2.7 https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f rweather/Crypto@^0.4.0 @@ -38,4 +38,4 @@ lib_ignore = NonBlockingRTTTL NimBLE-Arduino libpax - \ No newline at end of file + diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini index b68977c78..606cabac6 100644 --- a/arch/nrf52/nrf52.ini +++ b/arch/nrf52/nrf52.ini @@ -4,8 +4,8 @@ platform = platformio/nordicnrf52@^10.7.0 extends = arduino_base platform_packages = ; our custom Git version until they merge our PR - framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf - toolchain-gccarmnoneeabi@~1.90301.0 + platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf + platformio/toolchain-gccarmnoneeabi@~1.90301.0 build_type = debug build_flags = diff --git a/arch/rp2xx0/rp2040.ini b/arch/rp2xx0/rp2040.ini index 5cfa678d5..74644800d 100644 --- a/arch/rp2xx0/rp2040.ini +++ b/arch/rp2xx0/rp2040.ini @@ -18,6 +18,7 @@ build_src_filter = lib_ignore = BluetoothOTA + lvgl lib_deps = ${arduino_base.lib_deps} diff --git a/arch/rp2xx0/rp2350.ini b/arch/rp2xx0/rp2350.ini index ab16e24b4..6f1e4400e 100644 --- a/arch/rp2xx0/rp2350.ini +++ b/arch/rp2xx0/rp2350.ini @@ -1,8 +1,8 @@ ; Common settings for rp2040 Processor based targets [rp2350_base] -platform = https://github.com/maxgerhardt/platform-raspberrypi.git#19e30129fb1428b823be585c787dcb4ac0d9014c ; For arduino-pico >=4.2.1 +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 ; For arduino-pico >= 4.4.3 extends = arduino_base -platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#6024e9a7e82a72e38dd90f42029ba3748835eb2e ; 4.3.0 with fix MDNS +platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#4.4.3 board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -10,7 +10,6 @@ build_flags = ${arduino_base.build_flags} -Wno-unused-variable -Wcast-align -Isrc/platform/rp2xx0 -D__PLAT_RP2350__ -# -D _POSIX_THREADS build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index 46f41db3a..efa1ab0e4 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -1,7 +1,7 @@ [stm32_base] extends = arduino_base -platform = ststm32 -platform_packages = platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32.git#ea74156acd823b6d14739f389e6cdc648f8ee36e +platform = platformio/ststm32 +platform_packages = platformio/framework-arduinoststm32@^4.20900.0 build_type = release diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh index f8d808ced..a0635e997 100755 --- a/bin/build-esp32.sh +++ b/bin/build-esp32.sh @@ -35,11 +35,11 @@ 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-$VERSION.bin +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-$VERSION.bin +cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin cp bin/device-install.* $OUTDIR -cp bin/device-update.* $OUTDIR +cp bin/device-update.* $OUTDIR \ No newline at end of file diff --git a/bin/build-native.sh b/bin/build-native.sh index cda77b064..c6b1434dd 100755 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -24,7 +24,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 -platformio pkg update --environment native || platformioFailed +pio pkg update --environment native || platformioFailed pio run --environment native || platformioFailed cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)" cp bin/native-install.* $OUTDIR diff --git a/bin/config.d/MUI/X11_480x480.yaml b/bin/config.d/MUI/X11_480x480.yaml new file mode 100644 index 000000000..7bdf50453 --- /dev/null +++ b/bin/config.d/MUI/X11_480x480.yaml @@ -0,0 +1,4 @@ +Display: + Panel: X11 + Width: 480 + Height: 480 \ No newline at end of file diff --git a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml new file mode 100644 index 000000000..d9b64c7da --- /dev/null +++ b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml @@ -0,0 +1,10 @@ +# https://www.waveshare.com/core1262-868m.htm +# https://github.com/markbirss/lora-starter-edition-sx1262-i2c +Lora: + Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 8 + IRQ: 22 + Busy: 4 + Reset: 18 diff --git a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml new file mode 100644 index 000000000..1e1c325e7 --- /dev/null +++ b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml @@ -0,0 +1,10 @@ +# https://www.waveshare.com/pico-lora-sx1262-868m.htm +# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter +Lora: + Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 21 + IRQ: 16 + Busy: 20 + Reset: 18 diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index 4d8759ecc..7513ccff5 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -35,6 +35,11 @@ for subdir, dirs, files in os.walk(rootdir): outlist.append(section) else: outlist.append(section) + # Add the TFT variants if the base variant is selected + elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra": + outlist.append(section) + elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra": + outlist.append(section) if "board_check" in config[config[c].name]: if (config[config[c].name]["board_check"] == "true") & ( "check" in options @@ -43,4 +48,4 @@ for subdir, dirs, files in os.walk(rootdir): if ("quick" in options) & (len(outlist) > 3): print(json.dumps(random.sample(outlist, 3))) else: - print(json.dumps(outlist)) + print(json.dumps(outlist)) \ No newline at end of file diff --git a/debian/control b/debian/control index bb79d1958..b3a8eb58e 100644 --- a/debian/control +++ b/debian/control @@ -3,6 +3,7 @@ Section: misc Priority: optional Maintainer: Austin Lane Build-Depends: debhelper-compat (= 13), + lsb-release, tar, gzip, platformio, diff --git a/debian/rules b/debian/rules index a1a27c2f2..0612ba352 100755 --- a/debian/rules +++ b/debian/rules @@ -11,6 +11,15 @@ PIO_ENV:=\ PLATFORMIO_LIBDEPS_DIR=pio/libdeps \ PLATFORMIO_PACKAGES_DIR=pio/packages +# Raspbian armhf builds should be compatible with armv6-hardfloat +# https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags +ifneq (,$(findstring Raspbian,$(shell lsb_release -is))) +ifeq ($(DEB_BUILD_ARCH),armhf) +PIO_ENV+=\ + PLATFORMIO_BUILD_FLAGS="-mfloat-abi=hard -mfpu=vfp -march=armv6zk" +endif +endif + override_dh_auto_build: # Extract tarballs within source deb tar -xf pio.tar diff --git a/lib/device-ui b/lib/device-ui new file mode 160000 index 000000000..5c6156d2a --- /dev/null +++ b/lib/device-ui @@ -0,0 +1 @@ +Subproject commit 5c6156d2aa10d62cca3e57ffc117b934ef2fbffe diff --git a/monitor/filter_c3_exception_decoder.py b/monitor/filter_c3_exception_decoder.py index 6d7b5370c..5e74dc2b9 100644 --- a/monitor/filter_c3_exception_decoder.py +++ b/monitor/filter_c3_exception_decoder.py @@ -1,3 +1,6 @@ +# trunk-ignore-all(bandit/B404): subprocess is used to call addr2line +# trunk-ignore-all(bandit/B603): subprocess is used to call addr2line + # Copyright (c) 2014-present PlatformIO # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/platformio.ini b/platformio.ini index 171d6c829..1875c5324 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,7 +60,7 @@ lib_deps = https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159 https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4 https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0 - nanopb/Nanopb@0.4.9 + nanopb/Nanopb@0.4.91 erriez/ErriezCRC32@1.0.1 ; Used for the code analysis in PIO Home / Inspect diff --git a/protobufs b/protobufs index 068646653..2a3a67f04 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 068646653e8375fc145988026ad242a3cf70f7ab +Subproject commit 2a3a67f0431926dc3f32a8b216d264daab09b9bf diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h new file mode 100644 index 000000000..e29139001 --- /dev/null +++ b/src/BluetoothStatus.h @@ -0,0 +1,105 @@ +#pragma once +#include "Status.h" +#include "assert.h" +#include "configuration.h" +#include "meshUtils.h" +#include + +namespace meshtastic +{ + +// Describes the state of the Bluetooth connection +// Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code +class BluetoothStatus : public Status +{ + public: + enum class ConnectionState { + DISCONNECTED, + PAIRING, + CONNECTED, + }; + + private: + CallbackObserver statusObserver = + CallbackObserver(this, &BluetoothStatus::updateStatus); + + ConnectionState state = ConnectionState::DISCONNECTED; + std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero + + public: + BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; } + + // New BluetoothStatus: connected or disconnected + BluetoothStatus(ConnectionState state) + { + assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey + statusType = STATUS_TYPE_BLUETOOTH; + this->state = state; + } + + // New BluetoothStatus: pairing, with passkey + BluetoothStatus(std::string passkey) : Status() + { + statusType = STATUS_TYPE_BLUETOOTH; + this->state = ConnectionState::PAIRING; + this->passkey = passkey; + } + + ConnectionState getConnectionState() const { return this->state; } + + std::string getPasskey() const + { + assert(state == ConnectionState::PAIRING); + return this->passkey; + } + + void observe(Observable *source) { statusObserver.observe(source); } + + bool matches(const BluetoothStatus *newStatus) const + { + if (this->state == newStatus->getConnectionState()) { + // Same state: CONNECTED / DISCONNECTED + if (this->state != ConnectionState::PAIRING) + return true; + // Same state: PAIRING, and passkey matches + else if (this->getPasskey() == newStatus->getPasskey()) + return true; + } + + return false; + } + + int updateStatus(const BluetoothStatus *newStatus) + { + // Has the status changed? + if (!matches(newStatus)) { + // Copy the members + state = newStatus->getConnectionState(); + if (state == ConnectionState::PAIRING) + passkey = newStatus->getPasskey(); + + // Tell anyone interested that we have an update + onNewStatus.notifyObservers(this); + + // Debug only: + switch (state) { + case ConnectionState::PAIRING: + LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str()); + break; + case ConnectionState::CONNECTED: + LOG_DEBUG("BluetoothStatus CONNECTED"); + break; + + case ConnectionState::DISCONNECTED: + LOG_DEBUG("BluetoothStatus DISCONNECTED"); + break; + } + } + + return 0; + } +}; + +} // namespace meshtastic + +extern meshtastic::BluetoothStatus *bluetoothStatus; \ No newline at end of file diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 5175a2680..ec0bc5fc2 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -11,6 +11,7 @@ #include "main.h" #include "modules/ExternalNotificationModule.h" #include "power.h" +#include "sleep.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -99,6 +100,13 @@ ButtonThread::ButtonThread() : OSThread("Button") 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 } @@ -320,6 +328,26 @@ void ButtonThread::detachButtonInterrupts() #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 diff --git a/src/ButtonThread.h b/src/ButtonThread.h index a01a1718f..54b833d03 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -37,6 +37,12 @@ class ButtonThread : public concurrency::OSThread 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 @@ -48,6 +54,14 @@ class ButtonThread : public concurrency::OSThread 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; diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 461c72c26..31fe69c93 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -23,6 +23,10 @@ SPIClass SPI1(HSPI); #define SDHandler SPI #endif +#ifndef SD_SPI_FREQUENCY +#define SD_SPI_FREQUENCY 4000000U +#endif + #endif // HAS_SDCARD #if defined(ARCH_STM32WL) @@ -361,8 +365,7 @@ void setupSDCard() #ifdef HAS_SDCARD concurrency::LockGuard g(spiLock); SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); - - if (!SD.begin(SDCARD_CS, SDHandler)) { + if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { LOG_DEBUG("No SD_MMC card detected"); return; } diff --git a/src/Status.h b/src/Status.h index 65f3a252f..59d443ab7 100644 --- a/src/Status.h +++ b/src/Status.h @@ -7,6 +7,7 @@ #define STATUS_TYPE_POWER 1 #define STATUS_TYPE_GPS 2 #define STATUS_TYPE_NODE 3 +#define STATUS_TYPE_BLUETOOTH 4 namespace meshtastic { diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 880e5c131..41cfe1517 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -244,6 +244,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("BMP-388", (uint8_t)addr.address); type = BMP_3XX; break; + case 0x60: // BMP-390 should be 0x60 + logFoundDevice("BMP-390", (uint8_t)addr.address); + type = BMP_3XX; + break; case 0x58: // BMP-280 should be 0x58 default: logFoundDevice("BMP-280", (uint8_t)addr.address); @@ -521,4 +525,4 @@ void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) { LOG_INFO("%s found at address 0x%x", device, address); } -#endif +#endif \ No newline at end of file diff --git a/src/detect/einkScan.h b/src/detect/einkScan.h index 5bc218d00..d20c7b6e5 100644 --- a/src/detect/einkScan.h +++ b/src/detect/einkScan.h @@ -6,28 +6,28 @@ void d_writeCommand(uint8_t c) { - SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); if (PIN_EINK_DC >= 0) digitalWrite(PIN_EINK_DC, LOW); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, LOW); - SPI.transfer(c); + SPI1.transfer(c); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, HIGH); if (PIN_EINK_DC >= 0) digitalWrite(PIN_EINK_DC, HIGH); - SPI.endTransaction(); + SPI1.endTransaction(); } void d_writeData(uint8_t d) { - SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, LOW); - SPI.transfer(d); + SPI1.transfer(d); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, HIGH); - SPI.endTransaction(); + SPI1.endTransaction(); } unsigned long d_waitWhileBusy(uint16_t busy_time) @@ -53,7 +53,7 @@ unsigned long d_waitWhileBusy(uint16_t busy_time) void scanEInkDevice(void) { - SPI.begin(); + SPI1.begin(); d_writeCommand(0x22); d_writeData(0x83); d_writeCommand(0x20); @@ -62,6 +62,6 @@ void scanEInkDevice(void) LOG_DEBUG("EInk display found"); else LOG_DEBUG("EInk display not found"); - SPI.end(); + SPI1.end(); } #endif \ No newline at end of file diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index c2aae0381..2989a59bd 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -48,8 +48,6 @@ HardwareSerial *GPS::_serial_gps = nullptr; GPS *gps = nullptr; -static const char *ACK_SUCCESS_MESSAGE = "Get ack success!"; - static GPSUpdateScheduling scheduling; /// Multiple GPS instances might use the same serial port (in sequence), but we can @@ -437,6 +435,10 @@ static const int serialSpeeds[3] = {9600, 115200, 38400}; static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE}; #endif +#ifndef GPS_PROBETRIES +#define GPS_PROBETRIES 2 +#endif + /** * @brief Setup the GPS based on the model detected. * We detect the GPS by cycling through a set of baud rates, first common then rare. @@ -460,11 +462,7 @@ bool GPS::setup() digitalWrite(PIN_GPS_EN, HIGH); delay(1000); #endif -#ifdef TRACKER_T1000_E - if (probeTries < 5) { -#else - if (probeTries < 2) { -#endif + if (probeTries < GPS_PROBETRIES) { LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]); gnssModel = probe(serialSpeeds[speedSelect]); if (gnssModel == GNSS_MODEL_UNKNOWN) { @@ -475,11 +473,7 @@ bool GPS::setup() } } // Rare Serial Speeds -#ifdef TRACKER_T1000_E - if (probeTries == 5) { -#else - if (probeTries == 2) { -#endif + if (probeTries == GPS_PROBETRIES) { LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]); gnssModel = probe(rareSerialSpeeds[speedSelect]); if (gnssModel == GNSS_MODEL_UNKNOWN) { @@ -1043,14 +1037,6 @@ int32_t GPS::runOnce() if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { return disable(); } - // ONCE we will factory reset the GPS for bug #327 - if (!devicestate.did_gps_reset) { - LOG_WARN("GPS FactoryReset requested"); - if (gps->factoryReset()) { // If we don't succeed try again next time - devicestate.did_gps_reset = true; - nodeDB->saveToDisk(SEGMENT_DEVICESTATE); - } - } GPSInitFinished = true; publishUpdate(); } @@ -1063,24 +1049,6 @@ int32_t GPS::runOnce() if (whileActive()) { // if we have received valid NMEA claim we are connected setConnected(); - } else { - if ((config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) && - IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, - GNSS_MODEL_UBLOX10)) { - // reset the GPS on next bootup - if (devicestate.did_gps_reset && scheduling.elapsedSearchMs() > 60 * 1000UL && !hasFlow()) { - LOG_DEBUG("GPS is not found, try factory reset on next boot"); - devicestate.did_gps_reset = false; - nodeDB->saveToDisk(SEGMENT_DEVICESTATE); - return disable(); // Stop the GPS thread as it can do nothing useful until next reboot. - } - } - } - // At least one GPS has a bad habit of losing its mind from time to time - if (rebootsSeen > 2) { - rebootsSeen = 0; - LOG_DEBUG("Would normally factoryReset()"); - // gps->factoryReset(); } // If we're due for an update, wake the GPS @@ -1415,62 +1383,6 @@ static int32_t toDegInt(RawDegrees d) return r; } -bool GPS::factoryReset() -{ -#ifdef PIN_GPS_REINIT - // The L76K GNSS on the T-Echo requires the RESET pin to be pulled LOW - pinMode(PIN_GPS_REINIT, OUTPUT); - digitalWrite(PIN_GPS_REINIT, 0); - delay(150); // The L76K datasheet calls for at least 100MS delay - digitalWrite(PIN_GPS_REINIT, 1); -#endif - - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - byte _message_reset1[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1C, 0xA2}; - _serial_gps->write(_message_reset1, sizeof(_message_reset1)); - if (getACK(0x05, 0x01, 10000)) { - LOG_DEBUG(ACK_SUCCESS_MESSAGE); - } - delay(100); - byte _message_reset2[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1B, 0xA1}; - _serial_gps->write(_message_reset2, sizeof(_message_reset2)); - if (getACK(0x05, 0x01, 10000)) { - LOG_DEBUG(ACK_SUCCESS_MESSAGE); - } - delay(100); - byte _message_reset3[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0x1D, 0xB3}; - _serial_gps->write(_message_reset3, sizeof(_message_reset3)); - if (getACK(0x05, 0x01, 10000)) { - LOG_DEBUG(ACK_SUCCESS_MESSAGE); - } - } else if (gnssModel == GNSS_MODEL_MTK) { - // send the CAS10 to perform a factory restart of the device (and other device that support PCAS statements) - LOG_INFO("GNSS Factory Reset via PCAS10,3"); - _serial_gps->write("$PCAS10,3*1F\r\n"); - delay(100); - } else if (gnssModel == GNSS_MODEL_ATGM336H) { - LOG_INFO("Factory Reset via CAS-CFG-RST"); - uint8_t msglen = makeCASPacket(0x06, 0x02, sizeof(_message_CAS_CFG_RST_FACTORY), _message_CAS_CFG_RST_FACTORY); - _serial_gps->write(UBXscratch, msglen); - delay(100); - } else { - // fire this for good measure, if we have an L76B - won't harm other devices. - _serial_gps->write("$PMTK104*37\r\n"); - // No PMTK_ACK for this command. - delay(100); - // send the UBLOX Factory Reset Command regardless of detect state, something is very wrong, just assume it's - // UBLOX. Factory Reset - byte _message_reset[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFB, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x17, 0x2B, 0x7E}; - _serial_gps->write(_message_reset, sizeof(_message_reset)); - } - delay(1000); - return true; -} - /** * Perform any processing that should be done only while the GPS is awake and looking for a fix. * Override this method to check for new locations diff --git a/src/gps/GPS.h b/src/gps/GPS.h index df85b7cbf..01a4fe745 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -101,8 +101,6 @@ class GPS : private concurrency::OSThread // Empty the input buffer as quickly as possible void clearBuffer(); - virtual bool factoryReset(); - // Creates an instance of the GPS class. // Returns the new instance or null if the GPS is not present. static GPS *createGps(); diff --git a/src/graphics/fonts/OLEDDisplayFontsPL.cpp b/src/graphics/fonts/OLEDDisplayFontsPL.cpp index 1f43967aa..0767e24e7 100644 --- a/src/graphics/fonts/OLEDDisplayFontsPL.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsPL.cpp @@ -1,1312 +1,1313 @@ +// trunk-ignore-all(clang-format): Preserve long lines #include "OLEDDisplayFontsPL.h" const uint8_t ArialMT_Plain_10_PL[] PROGMEM = { -0x0A, // Width: 10 -0x0D, // Height: 13 -0x20, // First char: 32 -0xE0, // Number of chars: 224 -// Jump Table: -0xFF, 0xFF, 0x00, 0x03, // 32 -0x00, 0x00, 0x04, 0x03, // 33 -0x00, 0x04, 0x05, 0x04, // 34 -0x00, 0x09, 0x09, 0x06, // 35 -0x00, 0x12, 0x0A, 0x06, // 36 -0x00, 0x1C, 0x10, 0x09, // 37 -0x00, 0x2C, 0x0E, 0x08, // 38 -0x00, 0x3A, 0x01, 0x02, // 39 -0x00, 0x3B, 0x06, 0x04, // 40 -0x00, 0x41, 0x06, 0x04, // 41 -0x00, 0x47, 0x05, 0x04, // 42 -0x00, 0x4C, 0x09, 0x06, // 43 -0x00, 0x55, 0x04, 0x03, // 44 -0x00, 0x59, 0x03, 0x03, // 45 -0x00, 0x5C, 0x04, 0x03, // 46 -0x00, 0x60, 0x05, 0x04, // 47 -0x00, 0x65, 0x0A, 0x06, // 48 -0x00, 0x6F, 0x08, 0x05, // 49 -0x00, 0x77, 0x0A, 0x06, // 50 -0x00, 0x81, 0x0A, 0x06, // 51 -0x00, 0x8B, 0x0B, 0x07, // 52 -0x00, 0x96, 0x0A, 0x06, // 53 -0x00, 0xA0, 0x0A, 0x06, // 54 -0x00, 0xAA, 0x09, 0x06, // 55 -0x00, 0xB3, 0x0A, 0x06, // 56 -0x00, 0xBD, 0x0A, 0x06, // 57 -0x00, 0xC7, 0x04, 0x03, // 58 -0x00, 0xCB, 0x04, 0x03, // 59 -0x00, 0xCF, 0x0A, 0x06, // 60 -0x00, 0xD9, 0x09, 0x06, // 61 -0x00, 0xE2, 0x09, 0x06, // 62 -0x00, 0xEB, 0x0B, 0x07, // 63 -0x00, 0xF6, 0x14, 0x0B, // 64 -0x01, 0x0A, 0x0E, 0x08, // 65 -0x01, 0x18, 0x0C, 0x07, // 66 -0x01, 0x24, 0x0C, 0x07, // 67 -0x01, 0x30, 0x0B, 0x07, // 68 -0x01, 0x3B, 0x0C, 0x07, // 69 -0x01, 0x47, 0x09, 0x06, // 70 -0x01, 0x50, 0x0D, 0x08, // 71 -0x01, 0x5D, 0x0C, 0x07, // 72 -0x01, 0x69, 0x04, 0x03, // 73 -0x01, 0x6D, 0x08, 0x05, // 74 -0x01, 0x75, 0x0E, 0x08, // 75 -0x01, 0x83, 0x0C, 0x07, // 76 -0x01, 0x8F, 0x10, 0x09, // 77 -0x01, 0x9F, 0x0C, 0x07, // 78 -0x01, 0xAB, 0x0E, 0x08, // 79 -0x01, 0xB9, 0x0B, 0x07, // 80 -0x01, 0xC4, 0x0E, 0x08, // 81 -0x01, 0xD2, 0x0C, 0x07, // 82 -0x01, 0xDE, 0x0C, 0x07, // 83 -0x01, 0xEA, 0x0B, 0x07, // 84 -0x01, 0xF5, 0x0C, 0x07, // 85 -0x02, 0x01, 0x0D, 0x08, // 86 -0x02, 0x0E, 0x11, 0x0A, // 87 -0x02, 0x1F, 0x0E, 0x08, // 88 -0x02, 0x2D, 0x0D, 0x08, // 89 -0x02, 0x3A, 0x0C, 0x07, // 90 -0x02, 0x46, 0x06, 0x04, // 91 -0x02, 0x4C, 0x06, 0x04, // 92 -0x02, 0x52, 0x04, 0x03, // 93 -0x02, 0x56, 0x09, 0x06, // 94 -0x02, 0x5F, 0x0C, 0x07, // 95 -0x02, 0x6B, 0x03, 0x03, // 96 -0x02, 0x6E, 0x0A, 0x06, // 97 -0x02, 0x78, 0x0A, 0x06, // 98 -0x02, 0x82, 0x0A, 0x06, // 99 -0x02, 0x8C, 0x0A, 0x06, // 100 -0x02, 0x96, 0x0A, 0x06, // 101 -0x02, 0xA0, 0x05, 0x04, // 102 -0x02, 0xA5, 0x0A, 0x06, // 103 -0x02, 0xAF, 0x0A, 0x06, // 104 -0x02, 0xB9, 0x04, 0x03, // 105 -0x02, 0xBD, 0x04, 0x03, // 106 -0x02, 0xC1, 0x08, 0x05, // 107 -0x02, 0xC9, 0x04, 0x03, // 108 -0x02, 0xCD, 0x10, 0x09, // 109 -0x02, 0xDD, 0x0A, 0x06, // 110 -0x02, 0xE7, 0x0A, 0x06, // 111 -0x02, 0xF1, 0x0A, 0x06, // 112 -0x02, 0xFB, 0x0A, 0x06, // 113 -0x03, 0x05, 0x05, 0x04, // 114 -0x03, 0x0A, 0x08, 0x05, // 115 -0x03, 0x12, 0x06, 0x04, // 116 -0x03, 0x18, 0x0A, 0x06, // 117 -0x03, 0x22, 0x09, 0x06, // 118 -0x03, 0x2B, 0x0E, 0x08, // 119 -0x03, 0x39, 0x0A, 0x06, // 120 -0x03, 0x43, 0x09, 0x06, // 121 -0x03, 0x4C, 0x0A, 0x06, // 122 -0x03, 0x56, 0x06, 0x04, // 123 -0x03, 0x5C, 0x04, 0x03, // 124 -0x03, 0x60, 0x05, 0x04, // 125 -0x03, 0x65, 0x09, 0x06, // 126 -0xFF, 0xFF, 0x00, 0x0A, // 127 -0xFF, 0xFF, 0x00, 0x0A, // 128 -0x03, 0x6E, 0x0C, 0x07, // 129 -0x03, 0x7A, 0x05, 0x04, // 130 -0x03, 0x7F, 0x0C, 0x07, // 131 -0x03, 0x8B, 0x0E, 0x08, // 132 -0x03, 0x99, 0x0A, 0x06, // 133 -0x03, 0xA3, 0x0C, 0x07, // 134 -0x03, 0xAF, 0x0A, 0x06, // 135 -0x03, 0xB9, 0x0A, 0x06, // 136 -0x03, 0xC3, 0x0A, 0x06, // 137 -0xFF, 0xFF, 0x00, 0x0A, // 138 -0xFF, 0xFF, 0x00, 0x0A, // 139 -0xFF, 0xFF, 0x00, 0x0A, // 140 -0xFF, 0xFF, 0x00, 0x0A, // 141 -0xFF, 0xFF, 0x00, 0x0A, // 142 -0xFF, 0xFF, 0x00, 0x0A, // 143 -0xFF, 0xFF, 0x00, 0x0A, // 144 -0xFF, 0xFF, 0x00, 0x0A, // 145 -0xFF, 0xFF, 0x00, 0x0A, // 146 -0x03, 0xCD, 0x0E, 0x08, // 147 -0x03, 0xDB, 0x0A, 0x06, // 148 -0xFF, 0xFF, 0x00, 0x0A, // 149 -0xFF, 0xFF, 0x00, 0x0A, // 150 -0xFF, 0xFF, 0x00, 0x0A, // 151 -0x03, 0xE5, 0x0C, 0x07, // 152 -0x03, 0xF1, 0x0A, 0x06, // 153 -0x03, 0xFB, 0x0C, 0x07, // 154 -0x04, 0x07, 0x08, 0x05, // 155 -0xFF, 0xFF, 0x00, 0x0A, // 156 -0xFF, 0xFF, 0x00, 0x0A, // 157 -0xFF, 0xFF, 0x00, 0x0A, // 158 -0xFF, 0xFF, 0x00, 0x0A, // 159 -0xFF, 0xFF, 0x00, 0x0A, // 160 -0x04, 0x0F, 0x04, 0x03, // 161 -0x04, 0x13, 0x0A, 0x06, // 162 -0x04, 0x1D, 0x0C, 0x07, // 163 -0x04, 0x29, 0x0A, 0x06, // 164 -0x04, 0x33, 0x0A, 0x06, // 165 -0x04, 0x3D, 0x04, 0x03, // 166 -0x04, 0x41, 0x0A, 0x06, // 167 -0x04, 0x4B, 0x05, 0x04, // 168 -0x04, 0x50, 0x0D, 0x08, // 169 -0x04, 0x5D, 0x07, 0x05, // 170 -0x04, 0x64, 0x0A, 0x06, // 171 -0x04, 0x6E, 0x09, 0x06, // 172 -0x04, 0x77, 0x03, 0x03, // 173 -0x04, 0x7A, 0x0D, 0x08, // 174 -0x04, 0x87, 0x0B, 0x07, // 175 -0x04, 0x92, 0x07, 0x05, // 176 -0x04, 0x99, 0x0A, 0x06, // 177 -0x04, 0xA3, 0x05, 0x04, // 178 -0x04, 0xA8, 0x05, 0x04, // 179 -0x04, 0xAD, 0x05, 0x04, // 180 -0x04, 0xB2, 0x0A, 0x06, // 181 -0x04, 0xBC, 0x09, 0x06, // 182 -0x04, 0xC5, 0x03, 0x03, // 183 -0x04, 0xC8, 0x06, 0x04, // 184 -0x04, 0xCE, 0x0C, 0x07, // 185 -0x04, 0xDA, 0x07, 0x05, // 186 -0x04, 0xE1, 0x0C, 0x07, // 187 -0x04, 0xED, 0x0A, 0x06, // 188 -0x04, 0xF7, 0x10, 0x09, // 189 -0x05, 0x07, 0x10, 0x09, // 190 -0x05, 0x17, 0x0A, 0x06, // 191 -0x05, 0x21, 0x0E, 0x08, // 192 -0x05, 0x2F, 0x0E, 0x08, // 193 -0x05, 0x3D, 0x0E, 0x08, // 194 -0x05, 0x4B, 0x0E, 0x08, // 195 -0x05, 0x59, 0x0E, 0x08, // 196 -0x05, 0x67, 0x0E, 0x08, // 197 -0x05, 0x75, 0x12, 0x0A, // 198 -0x05, 0x87, 0x0C, 0x07, // 199 -0x05, 0x93, 0x0C, 0x07, // 200 -0x05, 0x9F, 0x0C, 0x07, // 201 -0x05, 0xAB, 0x0C, 0x07, // 202 -0x05, 0xB7, 0x0C, 0x07, // 203 -0x05, 0xC3, 0x05, 0x04, // 204 -0x05, 0xC8, 0x04, 0x03, // 205 -0x05, 0xCC, 0x04, 0x03, // 206 -0x05, 0xD0, 0x05, 0x04, // 207 -0x05, 0xD5, 0x0B, 0x07, // 208 -0x05, 0xE0, 0x0C, 0x07, // 209 -0x05, 0xEC, 0x0E, 0x08, // 210 -0x05, 0xFA, 0x0E, 0x08, // 211 -0x06, 0x08, 0x0E, 0x08, // 212 -0x06, 0x16, 0x0E, 0x08, // 213 -0x06, 0x24, 0x0E, 0x08, // 214 -0x06, 0x32, 0x0A, 0x06, // 215 -0x06, 0x3C, 0x0D, 0x08, // 216 -0x06, 0x49, 0x0C, 0x07, // 217 -0x06, 0x55, 0x0C, 0x07, // 218 -0x06, 0x61, 0x0C, 0x07, // 219 -0x06, 0x6D, 0x0C, 0x07, // 220 -0x06, 0x79, 0x0D, 0x08, // 221 -0x06, 0x86, 0x0B, 0x07, // 222 -0x06, 0x91, 0x0C, 0x07, // 223 -0x06, 0x9D, 0x0A, 0x06, // 224 -0x06, 0xA7, 0x0A, 0x06, // 225 -0x06, 0xB1, 0x0A, 0x06, // 226 -0x06, 0xBB, 0x0A, 0x06, // 227 -0x06, 0xC5, 0x0A, 0x06, // 228 -0x06, 0xCF, 0x0A, 0x06, // 229 -0x06, 0xD9, 0x10, 0x09, // 230 -0x06, 0xE9, 0x0A, 0x06, // 231 -0x06, 0xF3, 0x0A, 0x06, // 232 -0x06, 0xFD, 0x0A, 0x06, // 233 -0x07, 0x07, 0x0A, 0x06, // 234 -0x07, 0x11, 0x0A, 0x06, // 235 -0x07, 0x1B, 0x05, 0x04, // 236 -0x07, 0x20, 0x04, 0x03, // 237 -0x07, 0x24, 0x05, 0x04, // 238 -0x07, 0x29, 0x05, 0x04, // 239 -0x07, 0x2E, 0x0A, 0x06, // 240 -0x07, 0x38, 0x0A, 0x06, // 241 -0x07, 0x42, 0x0A, 0x06, // 242 -0x07, 0x4C, 0x0A, 0x06, // 243 -0x07, 0x56, 0x0A, 0x06, // 244 -0x07, 0x60, 0x0A, 0x06, // 245 -0x07, 0x6A, 0x0A, 0x06, // 246 -0x07, 0x74, 0x09, 0x06, // 247 -0x07, 0x7D, 0x0A, 0x06, // 248 -0x07, 0x87, 0x0A, 0x06, // 249 -0x07, 0x91, 0x0A, 0x06, // 250 -0x07, 0x9B, 0x0A, 0x06, // 251 -0x07, 0xA5, 0x0A, 0x06, // 252 -0x07, 0xAF, 0x09, 0x06, // 253 -0x07, 0xB8, 0x0A, 0x06, // 254 -0x07, 0xC2, 0x09, 0x06, // 255 -// Font Data: -0x00, 0x00, 0xF8, 0x02, // 33 -0x38, 0x00, 0x00, 0x00, 0x38, // 34 -0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 -0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 -0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 -0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 -0x38, // 39 -0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 -0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 -0x28, 0x00, 0x18, 0x00, 0x28, // 42 -0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 -0x00, 0x00, 0x00, 0x06, // 44 -0x80, 0x00, 0x80, // 45 -0x00, 0x00, 0x00, 0x02, // 46 -0x00, 0x03, 0xE0, 0x00, 0x18, // 47 -0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 -0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 -0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 -0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 -0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 -0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 -0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 -0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 -0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 -0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 -0x00, 0x00, 0x20, 0x02, // 58 -0x00, 0x00, 0x20, 0x06, // 59 -0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 -0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 -0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 -0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 -0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 -0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 -0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 -0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 -0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 -0x00, 0x00, 0xF8, 0x03, // 73 -0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 -0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 -0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 -0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 -0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 -0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 -0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 -0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 -0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 -0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 -0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 -0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 -0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 -0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 -0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 -0x08, 0x08, 0xF8, 0x0F, // 93 -0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 -0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 -0x08, 0x00, 0x10, // 96 -0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 -0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 -0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 -0x20, 0x00, 0xF0, 0x03, 0x28, // 102 -0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 -0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 -0x00, 0x00, 0xE8, 0x03, // 105 -0x00, 0x08, 0xE8, 0x07, // 106 -0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 -0x00, 0x00, 0xF8, 0x03, // 108 -0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 -0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 -0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 -0x00, 0x00, 0xE0, 0x03, 0x20, // 114 -0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 -0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 -0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 -0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 -0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 -0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 -0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 -0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 -0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 -0x00, 0x00, 0xF8, 0x0F, // 124 -0x08, 0x08, 0x78, 0x0F, 0x80, // 125 -0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 -0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x20, 0x02, 0x00, 0x02, 0x00, 0x02, // 129 -0x40, 0x00, 0xF8, 0x03, 0x20, // 130 -0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x44, 0x00, 0x82, 0x01, 0xF8, 0x03, // 131 -0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x0D, 0x00, 0x0A, // 132 -0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x0E, 0xE0, 0x0B, // 133 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 134 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x28, 0x02, 0x44, 0x01, // 135 -0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x28, 0x00, 0xC4, 0x03, // 136 -0x20, 0x02, 0x20, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 137 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 147 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x28, 0x02, 0xC4, 0x01, // 148 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x0E, 0x48, 0x0A, 0x48, 0x02, // 152 -0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x0E, 0xC0, 0x0A, // 153 -0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 154 -0x40, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x24, 0x01, // 155 -0x00, 0x00, 0xA0, 0x0F, // 161 -0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 -0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 -0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 -0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 -0x00, 0x00, 0x38, 0x0F, // 166 -0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 -0x08, 0x00, 0x00, 0x00, 0x08, // 168 -0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 -0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 -0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 -0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 -0x80, 0x00, 0x80, // 173 -0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 -0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 -0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 -0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 -0x48, 0x00, 0x68, 0x00, 0x58, // 178 -0x48, 0x00, 0x58, 0x00, 0x68, // 179 -0x00, 0x00, 0x10, 0x00, 0x08, // 180 -0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 -0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 -0x00, 0x00, 0x40, // 183 -0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 -0x08, 0x03, 0x88, 0x02, 0xCA, 0x02, 0x69, 0x02, 0x38, 0x02, 0x18, 0x02, // 185 -0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 -0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x6A, 0x02, 0x38, 0x02, 0x18, 0x02, // 187 -0x20, 0x02, 0x20, 0x03, 0xA8, 0x02, 0x60, 0x02, 0x20, 0x02, // 188 -0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 -0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 -0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 -0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 -0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 -0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 -0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 -0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 -0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 -0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 -0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 -0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 -0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 -0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 -0x00, 0x00, 0xF9, 0x03, 0x02, // 204 -0x02, 0x00, 0xF9, 0x03, // 205 -0x01, 0x00, 0xFA, 0x03, // 206 -0x02, 0x00, 0xF8, 0x03, 0x02, // 207 -0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 -0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 -0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 -0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 -0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 -0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 -0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 -0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 -0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 -0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 -0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 -0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 -0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 -0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 -0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 -0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 -0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 -0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 -0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 -0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 -0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 -0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 -0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 -0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 -0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 -0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 -0x00, 0x00, 0xE4, 0x03, 0x08, // 236 -0x08, 0x00, 0xE4, 0x03, // 237 -0x08, 0x00, 0xE4, 0x03, 0x08, // 238 -0x08, 0x00, 0xE0, 0x03, 0x08, // 239 -0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 -0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 -0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 -0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 -0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 -0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 -0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 -0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 -0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 -0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 -0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 -0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 -0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 -0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 -0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 -0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + // Jump Table: + 0xFF, 0xFF, 0x00, 0x03, // 32 + 0x00, 0x00, 0x04, 0x03, // 33 + 0x00, 0x04, 0x05, 0x04, // 34 + 0x00, 0x09, 0x09, 0x06, // 35 + 0x00, 0x12, 0x0A, 0x06, // 36 + 0x00, 0x1C, 0x10, 0x09, // 37 + 0x00, 0x2C, 0x0E, 0x08, // 38 + 0x00, 0x3A, 0x01, 0x02, // 39 + 0x00, 0x3B, 0x06, 0x04, // 40 + 0x00, 0x41, 0x06, 0x04, // 41 + 0x00, 0x47, 0x05, 0x04, // 42 + 0x00, 0x4C, 0x09, 0x06, // 43 + 0x00, 0x55, 0x04, 0x03, // 44 + 0x00, 0x59, 0x03, 0x03, // 45 + 0x00, 0x5C, 0x04, 0x03, // 46 + 0x00, 0x60, 0x05, 0x04, // 47 + 0x00, 0x65, 0x0A, 0x06, // 48 + 0x00, 0x6F, 0x08, 0x05, // 49 + 0x00, 0x77, 0x0A, 0x06, // 50 + 0x00, 0x81, 0x0A, 0x06, // 51 + 0x00, 0x8B, 0x0B, 0x07, // 52 + 0x00, 0x96, 0x0A, 0x06, // 53 + 0x00, 0xA0, 0x0A, 0x06, // 54 + 0x00, 0xAA, 0x09, 0x06, // 55 + 0x00, 0xB3, 0x0A, 0x06, // 56 + 0x00, 0xBD, 0x0A, 0x06, // 57 + 0x00, 0xC7, 0x04, 0x03, // 58 + 0x00, 0xCB, 0x04, 0x03, // 59 + 0x00, 0xCF, 0x0A, 0x06, // 60 + 0x00, 0xD9, 0x09, 0x06, // 61 + 0x00, 0xE2, 0x09, 0x06, // 62 + 0x00, 0xEB, 0x0B, 0x07, // 63 + 0x00, 0xF6, 0x14, 0x0B, // 64 + 0x01, 0x0A, 0x0E, 0x08, // 65 + 0x01, 0x18, 0x0C, 0x07, // 66 + 0x01, 0x24, 0x0C, 0x07, // 67 + 0x01, 0x30, 0x0B, 0x07, // 68 + 0x01, 0x3B, 0x0C, 0x07, // 69 + 0x01, 0x47, 0x09, 0x06, // 70 + 0x01, 0x50, 0x0D, 0x08, // 71 + 0x01, 0x5D, 0x0C, 0x07, // 72 + 0x01, 0x69, 0x04, 0x03, // 73 + 0x01, 0x6D, 0x08, 0x05, // 74 + 0x01, 0x75, 0x0E, 0x08, // 75 + 0x01, 0x83, 0x0C, 0x07, // 76 + 0x01, 0x8F, 0x10, 0x09, // 77 + 0x01, 0x9F, 0x0C, 0x07, // 78 + 0x01, 0xAB, 0x0E, 0x08, // 79 + 0x01, 0xB9, 0x0B, 0x07, // 80 + 0x01, 0xC4, 0x0E, 0x08, // 81 + 0x01, 0xD2, 0x0C, 0x07, // 82 + 0x01, 0xDE, 0x0C, 0x07, // 83 + 0x01, 0xEA, 0x0B, 0x07, // 84 + 0x01, 0xF5, 0x0C, 0x07, // 85 + 0x02, 0x01, 0x0D, 0x08, // 86 + 0x02, 0x0E, 0x11, 0x0A, // 87 + 0x02, 0x1F, 0x0E, 0x08, // 88 + 0x02, 0x2D, 0x0D, 0x08, // 89 + 0x02, 0x3A, 0x0C, 0x07, // 90 + 0x02, 0x46, 0x06, 0x04, // 91 + 0x02, 0x4C, 0x06, 0x04, // 92 + 0x02, 0x52, 0x04, 0x03, // 93 + 0x02, 0x56, 0x09, 0x06, // 94 + 0x02, 0x5F, 0x0C, 0x07, // 95 + 0x02, 0x6B, 0x03, 0x03, // 96 + 0x02, 0x6E, 0x0A, 0x06, // 97 + 0x02, 0x78, 0x0A, 0x06, // 98 + 0x02, 0x82, 0x0A, 0x06, // 99 + 0x02, 0x8C, 0x0A, 0x06, // 100 + 0x02, 0x96, 0x0A, 0x06, // 101 + 0x02, 0xA0, 0x05, 0x04, // 102 + 0x02, 0xA5, 0x0A, 0x06, // 103 + 0x02, 0xAF, 0x0A, 0x06, // 104 + 0x02, 0xB9, 0x04, 0x03, // 105 + 0x02, 0xBD, 0x04, 0x03, // 106 + 0x02, 0xC1, 0x08, 0x05, // 107 + 0x02, 0xC9, 0x04, 0x03, // 108 + 0x02, 0xCD, 0x10, 0x09, // 109 + 0x02, 0xDD, 0x0A, 0x06, // 110 + 0x02, 0xE7, 0x0A, 0x06, // 111 + 0x02, 0xF1, 0x0A, 0x06, // 112 + 0x02, 0xFB, 0x0A, 0x06, // 113 + 0x03, 0x05, 0x05, 0x04, // 114 + 0x03, 0x0A, 0x08, 0x05, // 115 + 0x03, 0x12, 0x06, 0x04, // 116 + 0x03, 0x18, 0x0A, 0x06, // 117 + 0x03, 0x22, 0x09, 0x06, // 118 + 0x03, 0x2B, 0x0E, 0x08, // 119 + 0x03, 0x39, 0x0A, 0x06, // 120 + 0x03, 0x43, 0x09, 0x06, // 121 + 0x03, 0x4C, 0x0A, 0x06, // 122 + 0x03, 0x56, 0x06, 0x04, // 123 + 0x03, 0x5C, 0x04, 0x03, // 124 + 0x03, 0x60, 0x05, 0x04, // 125 + 0x03, 0x65, 0x09, 0x06, // 126 + 0xFF, 0xFF, 0x00, 0x0A, // 127 + 0xFF, 0xFF, 0x00, 0x0A, // 128 + 0x03, 0x6E, 0x0C, 0x07, // 129 + 0x03, 0x7A, 0x05, 0x04, // 130 + 0x03, 0x7F, 0x0C, 0x07, // 131 + 0x03, 0x8B, 0x0E, 0x08, // 132 + 0x03, 0x99, 0x0A, 0x06, // 133 + 0x03, 0xA3, 0x0C, 0x07, // 134 + 0x03, 0xAF, 0x0A, 0x06, // 135 + 0x03, 0xB9, 0x0A, 0x06, // 136 + 0x03, 0xC3, 0x0A, 0x06, // 137 + 0xFF, 0xFF, 0x00, 0x0A, // 138 + 0xFF, 0xFF, 0x00, 0x0A, // 139 + 0xFF, 0xFF, 0x00, 0x0A, // 140 + 0xFF, 0xFF, 0x00, 0x0A, // 141 + 0xFF, 0xFF, 0x00, 0x0A, // 142 + 0xFF, 0xFF, 0x00, 0x0A, // 143 + 0xFF, 0xFF, 0x00, 0x0A, // 144 + 0xFF, 0xFF, 0x00, 0x0A, // 145 + 0xFF, 0xFF, 0x00, 0x0A, // 146 + 0x03, 0xCD, 0x0E, 0x08, // 147 + 0x03, 0xDB, 0x0A, 0x06, // 148 + 0xFF, 0xFF, 0x00, 0x0A, // 149 + 0xFF, 0xFF, 0x00, 0x0A, // 150 + 0xFF, 0xFF, 0x00, 0x0A, // 151 + 0x03, 0xE5, 0x0C, 0x07, // 152 + 0x03, 0xF1, 0x0A, 0x06, // 153 + 0x03, 0xFB, 0x0C, 0x07, // 154 + 0x04, 0x07, 0x08, 0x05, // 155 + 0xFF, 0xFF, 0x00, 0x0A, // 156 + 0xFF, 0xFF, 0x00, 0x0A, // 157 + 0xFF, 0xFF, 0x00, 0x0A, // 158 + 0xFF, 0xFF, 0x00, 0x0A, // 159 + 0xFF, 0xFF, 0x00, 0x0A, // 160 + 0x04, 0x0F, 0x04, 0x03, // 161 + 0x04, 0x13, 0x0A, 0x06, // 162 + 0x04, 0x1D, 0x0C, 0x07, // 163 + 0x04, 0x29, 0x0A, 0x06, // 164 + 0x04, 0x33, 0x0A, 0x06, // 165 + 0x04, 0x3D, 0x04, 0x03, // 166 + 0x04, 0x41, 0x0A, 0x06, // 167 + 0x04, 0x4B, 0x05, 0x04, // 168 + 0x04, 0x50, 0x0D, 0x08, // 169 + 0x04, 0x5D, 0x07, 0x05, // 170 + 0x04, 0x64, 0x0A, 0x06, // 171 + 0x04, 0x6E, 0x09, 0x06, // 172 + 0x04, 0x77, 0x03, 0x03, // 173 + 0x04, 0x7A, 0x0D, 0x08, // 174 + 0x04, 0x87, 0x0B, 0x07, // 175 + 0x04, 0x92, 0x07, 0x05, // 176 + 0x04, 0x99, 0x0A, 0x06, // 177 + 0x04, 0xA3, 0x05, 0x04, // 178 + 0x04, 0xA8, 0x05, 0x04, // 179 + 0x04, 0xAD, 0x05, 0x04, // 180 + 0x04, 0xB2, 0x0A, 0x06, // 181 + 0x04, 0xBC, 0x09, 0x06, // 182 + 0x04, 0xC5, 0x03, 0x03, // 183 + 0x04, 0xC8, 0x06, 0x04, // 184 + 0x04, 0xCE, 0x0C, 0x07, // 185 + 0x04, 0xDA, 0x07, 0x05, // 186 + 0x04, 0xE1, 0x0C, 0x07, // 187 + 0x04, 0xED, 0x0A, 0x06, // 188 + 0x04, 0xF7, 0x10, 0x09, // 189 + 0x05, 0x07, 0x10, 0x09, // 190 + 0x05, 0x17, 0x0A, 0x06, // 191 + 0x05, 0x21, 0x0E, 0x08, // 192 + 0x05, 0x2F, 0x0E, 0x08, // 193 + 0x05, 0x3D, 0x0E, 0x08, // 194 + 0x05, 0x4B, 0x0E, 0x08, // 195 + 0x05, 0x59, 0x0E, 0x08, // 196 + 0x05, 0x67, 0x0E, 0x08, // 197 + 0x05, 0x75, 0x12, 0x0A, // 198 + 0x05, 0x87, 0x0C, 0x07, // 199 + 0x05, 0x93, 0x0C, 0x07, // 200 + 0x05, 0x9F, 0x0C, 0x07, // 201 + 0x05, 0xAB, 0x0C, 0x07, // 202 + 0x05, 0xB7, 0x0C, 0x07, // 203 + 0x05, 0xC3, 0x05, 0x04, // 204 + 0x05, 0xC8, 0x04, 0x03, // 205 + 0x05, 0xCC, 0x04, 0x03, // 206 + 0x05, 0xD0, 0x05, 0x04, // 207 + 0x05, 0xD5, 0x0B, 0x07, // 208 + 0x05, 0xE0, 0x0C, 0x07, // 209 + 0x05, 0xEC, 0x0E, 0x08, // 210 + 0x05, 0xFA, 0x0E, 0x08, // 211 + 0x06, 0x08, 0x0E, 0x08, // 212 + 0x06, 0x16, 0x0E, 0x08, // 213 + 0x06, 0x24, 0x0E, 0x08, // 214 + 0x06, 0x32, 0x0A, 0x06, // 215 + 0x06, 0x3C, 0x0D, 0x08, // 216 + 0x06, 0x49, 0x0C, 0x07, // 217 + 0x06, 0x55, 0x0C, 0x07, // 218 + 0x06, 0x61, 0x0C, 0x07, // 219 + 0x06, 0x6D, 0x0C, 0x07, // 220 + 0x06, 0x79, 0x0D, 0x08, // 221 + 0x06, 0x86, 0x0B, 0x07, // 222 + 0x06, 0x91, 0x0C, 0x07, // 223 + 0x06, 0x9D, 0x0A, 0x06, // 224 + 0x06, 0xA7, 0x0A, 0x06, // 225 + 0x06, 0xB1, 0x0A, 0x06, // 226 + 0x06, 0xBB, 0x0A, 0x06, // 227 + 0x06, 0xC5, 0x0A, 0x06, // 228 + 0x06, 0xCF, 0x0A, 0x06, // 229 + 0x06, 0xD9, 0x10, 0x09, // 230 + 0x06, 0xE9, 0x0A, 0x06, // 231 + 0x06, 0xF3, 0x0A, 0x06, // 232 + 0x06, 0xFD, 0x0A, 0x06, // 233 + 0x07, 0x07, 0x0A, 0x06, // 234 + 0x07, 0x11, 0x0A, 0x06, // 235 + 0x07, 0x1B, 0x05, 0x04, // 236 + 0x07, 0x20, 0x04, 0x03, // 237 + 0x07, 0x24, 0x05, 0x04, // 238 + 0x07, 0x29, 0x05, 0x04, // 239 + 0x07, 0x2E, 0x0A, 0x06, // 240 + 0x07, 0x38, 0x0A, 0x06, // 241 + 0x07, 0x42, 0x0A, 0x06, // 242 + 0x07, 0x4C, 0x0A, 0x06, // 243 + 0x07, 0x56, 0x0A, 0x06, // 244 + 0x07, 0x60, 0x0A, 0x06, // 245 + 0x07, 0x6A, 0x0A, 0x06, // 246 + 0x07, 0x74, 0x09, 0x06, // 247 + 0x07, 0x7D, 0x0A, 0x06, // 248 + 0x07, 0x87, 0x0A, 0x06, // 249 + 0x07, 0x91, 0x0A, 0x06, // 250 + 0x07, 0x9B, 0x0A, 0x06, // 251 + 0x07, 0xA5, 0x0A, 0x06, // 252 + 0x07, 0xAF, 0x09, 0x06, // 253 + 0x07, 0xB8, 0x0A, 0x06, // 254 + 0x07, 0xC2, 0x09, 0x06, // 255 + // Font Data: + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x20, 0x02, 0x00, 0x02, 0x00, 0x02, // 129 + 0x40, 0x00, 0xF8, 0x03, 0x20, // 130 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x44, 0x00, 0x82, 0x01, 0xF8, 0x03, // 131 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x0D, 0x00, 0x0A, // 132 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x0E, 0xE0, 0x0B, // 133 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 134 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x28, 0x02, 0x44, 0x01, // 135 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x28, 0x00, 0xC4, 0x03, // 136 + 0x20, 0x02, 0x20, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 137 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 147 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x28, 0x02, 0xC4, 0x01, // 148 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x0E, 0x48, 0x0A, 0x48, 0x02, // 152 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x0E, 0xC0, 0x0A, // 153 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 154 + 0x40, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x24, 0x01, // 155 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x08, 0x00, 0x00, 0x00, 0x08, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x48, 0x00, 0x68, 0x00, 0x58, // 178 + 0x48, 0x00, 0x58, 0x00, 0x68, // 179 + 0x00, 0x00, 0x10, 0x00, 0x08, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 + 0x08, 0x03, 0x88, 0x02, 0xCA, 0x02, 0x69, 0x02, 0x38, 0x02, 0x18, 0x02, // 185 + 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x6A, 0x02, 0x38, 0x02, 0x18, 0x02, // 187 + 0x20, 0x02, 0x20, 0x03, 0xA8, 0x02, 0x60, 0x02, 0x20, 0x02, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 + 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 + 0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 + 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 + 0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 + 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 + 0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 + 0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 + 0x00, 0x00, 0xF9, 0x03, 0x02, // 204 + 0x02, 0x00, 0xF9, 0x03, // 205 + 0x01, 0x00, 0xFA, 0x03, // 206 + 0x02, 0x00, 0xF8, 0x03, 0x02, // 207 + 0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 + 0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 + 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 + 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 + 0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 + 0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 + 0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 + 0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 + 0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 + 0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 + 0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 + 0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 + 0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 + 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 + 0x00, 0x00, 0xE4, 0x03, 0x08, // 236 + 0x08, 0x00, 0xE4, 0x03, // 237 + 0x08, 0x00, 0xE4, 0x03, 0x08, // 238 + 0x08, 0x00, 0xE0, 0x03, 0x08, // 239 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 + 0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 + 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 + 0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 + 0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 + 0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 + 0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 + 0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 + 0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 + 0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 }; const uint8_t ArialMT_Plain_16_PL[] PROGMEM = { -0x10, // Width: 16 -0x13, // Height: 19 -0x20, // First char: 32 -0xE0, // Number of chars: 224 -// Jump Table: -0xFF, 0xFF, 0x00, 0x04, // 32 -0x00, 0x00, 0x08, 0x04, // 33 -0x00, 0x08, 0x0D, 0x06, // 34 -0x00, 0x15, 0x1A, 0x0A, // 35 -0x00, 0x2F, 0x17, 0x09, // 36 -0x00, 0x46, 0x26, 0x0E, // 37 -0x00, 0x6C, 0x1D, 0x0B, // 38 -0x00, 0x89, 0x04, 0x03, // 39 -0x00, 0x8D, 0x0C, 0x05, // 40 -0x00, 0x99, 0x0B, 0x05, // 41 -0x00, 0xA4, 0x0D, 0x06, // 42 -0x00, 0xB1, 0x17, 0x09, // 43 -0x00, 0xC8, 0x09, 0x04, // 44 -0x00, 0xD1, 0x0B, 0x05, // 45 -0x00, 0xDC, 0x08, 0x04, // 46 -0x00, 0xE4, 0x0A, 0x05, // 47 -0x00, 0xEE, 0x17, 0x09, // 48 -0x01, 0x05, 0x11, 0x07, // 49 -0x01, 0x16, 0x17, 0x09, // 50 -0x01, 0x2D, 0x17, 0x09, // 51 -0x01, 0x44, 0x17, 0x09, // 52 -0x01, 0x5B, 0x17, 0x09, // 53 -0x01, 0x72, 0x17, 0x09, // 54 -0x01, 0x89, 0x16, 0x09, // 55 -0x01, 0x9F, 0x17, 0x09, // 56 -0x01, 0xB6, 0x17, 0x09, // 57 -0x01, 0xCD, 0x05, 0x03, // 58 -0x01, 0xD2, 0x06, 0x03, // 59 -0x01, 0xD8, 0x17, 0x09, // 60 -0x01, 0xEF, 0x17, 0x09, // 61 -0x02, 0x06, 0x17, 0x09, // 62 -0x02, 0x1D, 0x16, 0x09, // 63 -0x02, 0x33, 0x2F, 0x11, // 64 -0x02, 0x62, 0x1D, 0x0B, // 65 -0x02, 0x7F, 0x1D, 0x0B, // 66 -0x02, 0x9C, 0x20, 0x0C, // 67 -0x02, 0xBC, 0x20, 0x0C, // 68 -0x02, 0xDC, 0x1D, 0x0B, // 69 -0x02, 0xF9, 0x19, 0x0A, // 70 -0x03, 0x12, 0x20, 0x0C, // 71 -0x03, 0x32, 0x1D, 0x0B, // 72 -0x03, 0x4F, 0x05, 0x03, // 73 -0x03, 0x54, 0x14, 0x08, // 74 -0x03, 0x68, 0x1D, 0x0B, // 75 -0x03, 0x85, 0x17, 0x09, // 76 -0x03, 0x9C, 0x23, 0x0D, // 77 -0x03, 0xBF, 0x1D, 0x0B, // 78 -0x03, 0xDC, 0x20, 0x0C, // 79 -0x03, 0xFC, 0x1C, 0x0B, // 80 -0x04, 0x18, 0x20, 0x0C, // 81 -0x04, 0x38, 0x1D, 0x0B, // 82 -0x04, 0x55, 0x1D, 0x0B, // 83 -0x04, 0x72, 0x19, 0x0A, // 84 -0x04, 0x8B, 0x1D, 0x0B, // 85 -0x04, 0xA8, 0x1C, 0x0B, // 86 -0x04, 0xC4, 0x2B, 0x10, // 87 -0x04, 0xEF, 0x20, 0x0C, // 88 -0x05, 0x0F, 0x19, 0x0A, // 89 -0x05, 0x28, 0x1A, 0x0A, // 90 -0x05, 0x42, 0x0C, 0x05, // 91 -0x05, 0x4E, 0x0B, 0x05, // 92 -0x05, 0x59, 0x09, 0x04, // 93 -0x05, 0x62, 0x14, 0x08, // 94 -0x05, 0x76, 0x1B, 0x0A, // 95 -0x05, 0x91, 0x07, 0x04, // 96 -0x05, 0x98, 0x17, 0x09, // 97 -0x05, 0xAF, 0x17, 0x09, // 98 -0x05, 0xC6, 0x14, 0x08, // 99 -0x05, 0xDA, 0x17, 0x09, // 100 -0x05, 0xF1, 0x17, 0x09, // 101 -0x06, 0x08, 0x0A, 0x05, // 102 -0x06, 0x12, 0x17, 0x09, // 103 -0x06, 0x29, 0x14, 0x08, // 104 -0x06, 0x3D, 0x05, 0x03, // 105 -0x06, 0x42, 0x06, 0x03, // 106 -0x06, 0x48, 0x17, 0x09, // 107 -0x06, 0x5F, 0x05, 0x03, // 108 -0x06, 0x64, 0x23, 0x0D, // 109 -0x06, 0x87, 0x14, 0x08, // 110 -0x06, 0x9B, 0x17, 0x09, // 111 -0x06, 0xB2, 0x17, 0x09, // 112 -0x06, 0xC9, 0x18, 0x09, // 113 -0x06, 0xE1, 0x0D, 0x06, // 114 -0x06, 0xEE, 0x14, 0x08, // 115 -0x07, 0x02, 0x0B, 0x05, // 116 -0x07, 0x0D, 0x14, 0x08, // 117 -0x07, 0x21, 0x13, 0x08, // 118 -0x07, 0x34, 0x1F, 0x0C, // 119 -0x07, 0x53, 0x14, 0x08, // 120 -0x07, 0x67, 0x13, 0x08, // 121 -0x07, 0x7A, 0x14, 0x08, // 122 -0x07, 0x8E, 0x0F, 0x06, // 123 -0x07, 0x9D, 0x06, 0x03, // 124 -0x07, 0xA3, 0x0E, 0x06, // 125 -0x07, 0xB1, 0x17, 0x09, // 126 -0xFF, 0xFF, 0x00, 0x10, // 127 -0xFF, 0xFF, 0x00, 0x10, // 128 -0x07, 0xC8, 0x17, 0x09, // 129 -0x07, 0xDF, 0x07, 0x04, // 130 -0x07, 0xE6, 0x1D, 0x0B, // 131 -0x08, 0x03, 0x1E, 0x0B, // 132 -0x08, 0x21, 0x1B, 0x0A, // 133 -0x08, 0x3C, 0x20, 0x0C, // 134 -0x08, 0x5C, 0x14, 0x08, // 135 -0x08, 0x70, 0x14, 0x08, // 136 -0x08, 0x84, 0x14, 0x08, // 137 -0xFF, 0xFF, 0x00, 0x10, // 138 -0xFF, 0xFF, 0x00, 0x10, // 139 -0xFF, 0xFF, 0x00, 0x10, // 140 -0xFF, 0xFF, 0x00, 0x10, // 141 -0xFF, 0xFF, 0x00, 0x10, // 142 -0xFF, 0xFF, 0x00, 0x10, // 143 -0xFF, 0xFF, 0x00, 0x10, // 144 -0xFF, 0xFF, 0x00, 0x10, // 145 -0xFF, 0xFF, 0x00, 0x10, // 146 -0x08, 0x98, 0x20, 0x0C, // 147 -0x08, 0xB8, 0x17, 0x09, // 148 -0xFF, 0xFF, 0x00, 0x10, // 149 -0xFF, 0xFF, 0x00, 0x10, // 150 -0xFF, 0xFF, 0x00, 0x10, // 151 -0x08, 0xCF, 0x1D, 0x0B, // 152 -0x08, 0xEC, 0x17, 0x09, // 153 -0x09, 0x03, 0x1D, 0x0B, // 154 -0x09, 0x20, 0x14, 0x08, // 155 -0xFF, 0xFF, 0x00, 0x10, // 156 -0xFF, 0xFF, 0x00, 0x10, // 157 -0xFF, 0xFF, 0x00, 0x10, // 158 -0xFF, 0xFF, 0x00, 0x10, // 159 -0xFF, 0xFF, 0x00, 0x10, // 160 -0x09, 0x34, 0x09, 0x04, // 161 -0x09, 0x3D, 0x17, 0x09, // 162 -0x09, 0x54, 0x17, 0x09, // 163 -0x09, 0x6B, 0x14, 0x08, // 164 -0x09, 0x7F, 0x1A, 0x0A, // 165 -0x09, 0x99, 0x06, 0x03, // 166 -0x09, 0x9F, 0x17, 0x09, // 167 -0x09, 0xB6, 0x07, 0x04, // 168 -0x09, 0xBD, 0x23, 0x0D, // 169 -0x09, 0xE0, 0x0E, 0x06, // 170 -0x09, 0xEE, 0x14, 0x08, // 171 -0x0A, 0x02, 0x17, 0x09, // 172 -0x0A, 0x19, 0x0B, 0x05, // 173 -0x0A, 0x24, 0x23, 0x0D, // 174 -0x0A, 0x47, 0x19, 0x0A, // 175 -0x0A, 0x60, 0x0D, 0x06, // 176 -0x0A, 0x6D, 0x17, 0x09, // 177 -0x0A, 0x84, 0x0E, 0x06, // 178 -0x0A, 0x92, 0x0D, 0x06, // 179 -0x0A, 0x9F, 0x0A, 0x05, // 180 -0x0A, 0xA9, 0x17, 0x09, // 181 -0x0A, 0xC0, 0x19, 0x0A, // 182 -0x0A, 0xD9, 0x08, 0x04, // 183 -0x0A, 0xE1, 0x0C, 0x05, // 184 -0x0A, 0xED, 0x1A, 0x0A, // 185 -0x0B, 0x07, 0x0D, 0x06, // 186 -0x0B, 0x14, 0x1A, 0x0A, // 187 -0x0B, 0x2E, 0x14, 0x08, // 188 -0x0B, 0x42, 0x26, 0x0E, // 189 -0x0B, 0x68, 0x26, 0x0E, // 190 -0x0B, 0x8E, 0x1A, 0x0A, // 191 -0x0B, 0xA8, 0x1D, 0x0B, // 192 -0x0B, 0xC5, 0x1D, 0x0B, // 193 -0x0B, 0xE2, 0x1D, 0x0B, // 194 -0x0B, 0xFF, 0x1D, 0x0B, // 195 -0x0C, 0x1C, 0x1D, 0x0B, // 196 -0x0C, 0x39, 0x1D, 0x0B, // 197 -0x0C, 0x56, 0x2C, 0x10, // 198 -0x0C, 0x82, 0x20, 0x0C, // 199 -0x0C, 0xA2, 0x1D, 0x0B, // 200 -0x0C, 0xBF, 0x1D, 0x0B, // 201 -0x0C, 0xDC, 0x1D, 0x0B, // 202 -0x0C, 0xF9, 0x1D, 0x0B, // 203 -0x0D, 0x16, 0x05, 0x03, // 204 -0x0D, 0x1B, 0x07, 0x04, // 205 -0x0D, 0x22, 0x0A, 0x05, // 206 -0x0D, 0x2C, 0x07, 0x04, // 207 -0x0D, 0x33, 0x20, 0x0C, // 208 -0x0D, 0x53, 0x1D, 0x0B, // 209 -0x0D, 0x70, 0x20, 0x0C, // 210 -0x0D, 0x90, 0x20, 0x0C, // 211 -0x0D, 0xB0, 0x20, 0x0C, // 212 -0x0D, 0xD0, 0x20, 0x0C, // 213 -0x0D, 0xF0, 0x20, 0x0C, // 214 -0x0E, 0x10, 0x17, 0x09, // 215 -0x0E, 0x27, 0x20, 0x0C, // 216 -0x0E, 0x47, 0x1D, 0x0B, // 217 -0x0E, 0x64, 0x1D, 0x0B, // 218 -0x0E, 0x81, 0x1D, 0x0B, // 219 -0x0E, 0x9E, 0x1D, 0x0B, // 220 -0x0E, 0xBB, 0x19, 0x0A, // 221 -0x0E, 0xD4, 0x1D, 0x0B, // 222 -0x0E, 0xF1, 0x17, 0x09, // 223 -0x0F, 0x08, 0x17, 0x09, // 224 -0x0F, 0x1F, 0x17, 0x09, // 225 -0x0F, 0x36, 0x17, 0x09, // 226 -0x0F, 0x4D, 0x17, 0x09, // 227 -0x0F, 0x64, 0x17, 0x09, // 228 -0x0F, 0x7B, 0x17, 0x09, // 229 -0x0F, 0x92, 0x29, 0x0F, // 230 -0x0F, 0xBB, 0x14, 0x08, // 231 -0x0F, 0xCF, 0x17, 0x09, // 232 -0x0F, 0xE6, 0x17, 0x09, // 233 -0x0F, 0xFD, 0x17, 0x09, // 234 -0x10, 0x14, 0x17, 0x09, // 235 -0x10, 0x2B, 0x05, 0x03, // 236 -0x10, 0x30, 0x07, 0x04, // 237 -0x10, 0x37, 0x0A, 0x05, // 238 -0x10, 0x41, 0x07, 0x04, // 239 -0x10, 0x48, 0x17, 0x09, // 240 -0x10, 0x5F, 0x14, 0x08, // 241 -0x10, 0x73, 0x17, 0x09, // 242 -0x10, 0x8A, 0x17, 0x09, // 243 -0x10, 0xA1, 0x17, 0x09, // 244 -0x10, 0xB8, 0x17, 0x09, // 245 -0x10, 0xCF, 0x17, 0x09, // 246 -0x10, 0xE6, 0x17, 0x09, // 247 -0x10, 0xFD, 0x17, 0x09, // 248 -0x11, 0x14, 0x14, 0x08, // 249 -0x11, 0x28, 0x14, 0x08, // 250 -0x11, 0x3C, 0x14, 0x08, // 251 -0x11, 0x50, 0x14, 0x08, // 252 -0x11, 0x64, 0x13, 0x08, // 253 -0x11, 0x77, 0x17, 0x09, // 254 -0x11, 0x8E, 0x13, 0x08, // 255 -// Font Data: -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 -0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 -0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 -0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 36 -0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 -0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 -0x00, 0x00, 0x00, 0x78, // 39 -0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 -0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 -0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 -0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 -0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 -0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 -0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 -0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 -0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 -0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 -0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 -0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 -0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x08, 0x07, 0x00, 0xC8, 0x00, 0x00, 0x28, 0x00, 0x00, 0x18, // 55 -0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 -0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 -0x00, 0x00, 0x00, 0x40, 0x40, // 58 -0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 -0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 -0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 -0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 -0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 -0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, // 70 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 -0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 -0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 -0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 -0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 84 -0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 -0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 -0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 -0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 -0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 89 -0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 90 -0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 -0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 -0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 -0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 -0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 -0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 -0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 -0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 -0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 -0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 -0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 -0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 -0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, 0x03, // 113 -0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 -0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 -0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 -0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 -0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 -0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 -0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 -0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 -0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 -0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 -0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 -0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 -0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x42, 0x00, 0x00, 0x41, 0x00, 0x80, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 129 -0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x80, // 130 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x08, 0x03, 0x00, 0x04, 0x04, 0x00, 0x02, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 131 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x9C, 0x01, 0x00, 0x60, 0x02, // 132 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, 0x03, 0x00, 0x80, 0x04, // 133 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 134 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, // 135 -0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 136 -0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x50, 0x44, 0x00, 0x48, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 137 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 147 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 148 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0xC1, 0x01, 0x08, 0x41, 0x02, 0x08, 0x41, 0x00, 0x08, 0x40, // 152 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x03, 0x40, 0xC4, 0x04, 0x80, 0x24, 0x00, 0x00, 0x17, // 153 -0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 154 -0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x38, // 155 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 -0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 -0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 -0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 -0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 -0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 -0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 -0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 -0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 -0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 -0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 -0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 -0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 -0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 175 -0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 -0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 -0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 -0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 -0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 -0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, // 182 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 -0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 -0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x89, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 185 -0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 -0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 187 -0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x50, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 188 -0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 -0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 -0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0xC0, // 191 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, 0x82, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, 0x81, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 -0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, 0x08, 0x04, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, // 198 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, 0x08, 0x40, 0x03, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 199 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 200 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 201 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 202 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 203 -0x01, 0x00, 0x00, 0xFA, 0x7F, // 204 -0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 205 -0x02, 0x00, 0x00, 0xF9, 0x7F, 0x00, 0x01, 0x00, 0x00, 0x02, // 206 -0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 207 -0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 208 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 210 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 211 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 212 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 213 -0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 214 -0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 215 -0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, 0xC8, 0x40, 0x00, 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, 0x0F, // 216 -0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 -0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 -0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 -0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 -0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 221 -0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 222 -0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 223 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 225 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, 0x80, 0x7F, // 226 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, 0x80, 0x7F, // 227 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 228 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 229 -0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x3F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, 0x20, // 231 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 232 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 233 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 234 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 235 -0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 -0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 -0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 -0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, 0x00, 0x1F, // 240 -0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 241 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 242 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 243 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, 0x00, 0x1F, // 244 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, 0x00, 0x1F, // 245 -0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 246 -0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 247 -0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, 0x40, 0x1F, // 248 -0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 249 -0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, 0x7F, // 250 -0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, 0x7F, // 251 -0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 252 -0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 253 -0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 254 -0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 255 + 0x10, // Width: 16 + 0x13, // Height: 19 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + // Jump Table: + 0xFF, 0xFF, 0x00, 0x04, // 32 + 0x00, 0x00, 0x08, 0x04, // 33 + 0x00, 0x08, 0x0D, 0x06, // 34 + 0x00, 0x15, 0x1A, 0x0A, // 35 + 0x00, 0x2F, 0x17, 0x09, // 36 + 0x00, 0x46, 0x26, 0x0E, // 37 + 0x00, 0x6C, 0x1D, 0x0B, // 38 + 0x00, 0x89, 0x04, 0x03, // 39 + 0x00, 0x8D, 0x0C, 0x05, // 40 + 0x00, 0x99, 0x0B, 0x05, // 41 + 0x00, 0xA4, 0x0D, 0x06, // 42 + 0x00, 0xB1, 0x17, 0x09, // 43 + 0x00, 0xC8, 0x09, 0x04, // 44 + 0x00, 0xD1, 0x0B, 0x05, // 45 + 0x00, 0xDC, 0x08, 0x04, // 46 + 0x00, 0xE4, 0x0A, 0x05, // 47 + 0x00, 0xEE, 0x17, 0x09, // 48 + 0x01, 0x05, 0x11, 0x07, // 49 + 0x01, 0x16, 0x17, 0x09, // 50 + 0x01, 0x2D, 0x17, 0x09, // 51 + 0x01, 0x44, 0x17, 0x09, // 52 + 0x01, 0x5B, 0x17, 0x09, // 53 + 0x01, 0x72, 0x17, 0x09, // 54 + 0x01, 0x89, 0x16, 0x09, // 55 + 0x01, 0x9F, 0x17, 0x09, // 56 + 0x01, 0xB6, 0x17, 0x09, // 57 + 0x01, 0xCD, 0x05, 0x03, // 58 + 0x01, 0xD2, 0x06, 0x03, // 59 + 0x01, 0xD8, 0x17, 0x09, // 60 + 0x01, 0xEF, 0x17, 0x09, // 61 + 0x02, 0x06, 0x17, 0x09, // 62 + 0x02, 0x1D, 0x16, 0x09, // 63 + 0x02, 0x33, 0x2F, 0x11, // 64 + 0x02, 0x62, 0x1D, 0x0B, // 65 + 0x02, 0x7F, 0x1D, 0x0B, // 66 + 0x02, 0x9C, 0x20, 0x0C, // 67 + 0x02, 0xBC, 0x20, 0x0C, // 68 + 0x02, 0xDC, 0x1D, 0x0B, // 69 + 0x02, 0xF9, 0x19, 0x0A, // 70 + 0x03, 0x12, 0x20, 0x0C, // 71 + 0x03, 0x32, 0x1D, 0x0B, // 72 + 0x03, 0x4F, 0x05, 0x03, // 73 + 0x03, 0x54, 0x14, 0x08, // 74 + 0x03, 0x68, 0x1D, 0x0B, // 75 + 0x03, 0x85, 0x17, 0x09, // 76 + 0x03, 0x9C, 0x23, 0x0D, // 77 + 0x03, 0xBF, 0x1D, 0x0B, // 78 + 0x03, 0xDC, 0x20, 0x0C, // 79 + 0x03, 0xFC, 0x1C, 0x0B, // 80 + 0x04, 0x18, 0x20, 0x0C, // 81 + 0x04, 0x38, 0x1D, 0x0B, // 82 + 0x04, 0x55, 0x1D, 0x0B, // 83 + 0x04, 0x72, 0x19, 0x0A, // 84 + 0x04, 0x8B, 0x1D, 0x0B, // 85 + 0x04, 0xA8, 0x1C, 0x0B, // 86 + 0x04, 0xC4, 0x2B, 0x10, // 87 + 0x04, 0xEF, 0x20, 0x0C, // 88 + 0x05, 0x0F, 0x19, 0x0A, // 89 + 0x05, 0x28, 0x1A, 0x0A, // 90 + 0x05, 0x42, 0x0C, 0x05, // 91 + 0x05, 0x4E, 0x0B, 0x05, // 92 + 0x05, 0x59, 0x09, 0x04, // 93 + 0x05, 0x62, 0x14, 0x08, // 94 + 0x05, 0x76, 0x1B, 0x0A, // 95 + 0x05, 0x91, 0x07, 0x04, // 96 + 0x05, 0x98, 0x17, 0x09, // 97 + 0x05, 0xAF, 0x17, 0x09, // 98 + 0x05, 0xC6, 0x14, 0x08, // 99 + 0x05, 0xDA, 0x17, 0x09, // 100 + 0x05, 0xF1, 0x17, 0x09, // 101 + 0x06, 0x08, 0x0A, 0x05, // 102 + 0x06, 0x12, 0x17, 0x09, // 103 + 0x06, 0x29, 0x14, 0x08, // 104 + 0x06, 0x3D, 0x05, 0x03, // 105 + 0x06, 0x42, 0x06, 0x03, // 106 + 0x06, 0x48, 0x17, 0x09, // 107 + 0x06, 0x5F, 0x05, 0x03, // 108 + 0x06, 0x64, 0x23, 0x0D, // 109 + 0x06, 0x87, 0x14, 0x08, // 110 + 0x06, 0x9B, 0x17, 0x09, // 111 + 0x06, 0xB2, 0x17, 0x09, // 112 + 0x06, 0xC9, 0x18, 0x09, // 113 + 0x06, 0xE1, 0x0D, 0x06, // 114 + 0x06, 0xEE, 0x14, 0x08, // 115 + 0x07, 0x02, 0x0B, 0x05, // 116 + 0x07, 0x0D, 0x14, 0x08, // 117 + 0x07, 0x21, 0x13, 0x08, // 118 + 0x07, 0x34, 0x1F, 0x0C, // 119 + 0x07, 0x53, 0x14, 0x08, // 120 + 0x07, 0x67, 0x13, 0x08, // 121 + 0x07, 0x7A, 0x14, 0x08, // 122 + 0x07, 0x8E, 0x0F, 0x06, // 123 + 0x07, 0x9D, 0x06, 0x03, // 124 + 0x07, 0xA3, 0x0E, 0x06, // 125 + 0x07, 0xB1, 0x17, 0x09, // 126 + 0xFF, 0xFF, 0x00, 0x10, // 127 + 0xFF, 0xFF, 0x00, 0x10, // 128 + 0x07, 0xC8, 0x17, 0x09, // 129 + 0x07, 0xDF, 0x07, 0x04, // 130 + 0x07, 0xE6, 0x1D, 0x0B, // 131 + 0x08, 0x03, 0x1E, 0x0B, // 132 + 0x08, 0x21, 0x1B, 0x0A, // 133 + 0x08, 0x3C, 0x20, 0x0C, // 134 + 0x08, 0x5C, 0x14, 0x08, // 135 + 0x08, 0x70, 0x14, 0x08, // 136 + 0x08, 0x84, 0x14, 0x08, // 137 + 0xFF, 0xFF, 0x00, 0x10, // 138 + 0xFF, 0xFF, 0x00, 0x10, // 139 + 0xFF, 0xFF, 0x00, 0x10, // 140 + 0xFF, 0xFF, 0x00, 0x10, // 141 + 0xFF, 0xFF, 0x00, 0x10, // 142 + 0xFF, 0xFF, 0x00, 0x10, // 143 + 0xFF, 0xFF, 0x00, 0x10, // 144 + 0xFF, 0xFF, 0x00, 0x10, // 145 + 0xFF, 0xFF, 0x00, 0x10, // 146 + 0x08, 0x98, 0x20, 0x0C, // 147 + 0x08, 0xB8, 0x17, 0x09, // 148 + 0xFF, 0xFF, 0x00, 0x10, // 149 + 0xFF, 0xFF, 0x00, 0x10, // 150 + 0xFF, 0xFF, 0x00, 0x10, // 151 + 0x08, 0xCF, 0x1D, 0x0B, // 152 + 0x08, 0xEC, 0x17, 0x09, // 153 + 0x09, 0x03, 0x1D, 0x0B, // 154 + 0x09, 0x20, 0x14, 0x08, // 155 + 0xFF, 0xFF, 0x00, 0x10, // 156 + 0xFF, 0xFF, 0x00, 0x10, // 157 + 0xFF, 0xFF, 0x00, 0x10, // 158 + 0xFF, 0xFF, 0x00, 0x10, // 159 + 0xFF, 0xFF, 0x00, 0x10, // 160 + 0x09, 0x34, 0x09, 0x04, // 161 + 0x09, 0x3D, 0x17, 0x09, // 162 + 0x09, 0x54, 0x17, 0x09, // 163 + 0x09, 0x6B, 0x14, 0x08, // 164 + 0x09, 0x7F, 0x1A, 0x0A, // 165 + 0x09, 0x99, 0x06, 0x03, // 166 + 0x09, 0x9F, 0x17, 0x09, // 167 + 0x09, 0xB6, 0x07, 0x04, // 168 + 0x09, 0xBD, 0x23, 0x0D, // 169 + 0x09, 0xE0, 0x0E, 0x06, // 170 + 0x09, 0xEE, 0x14, 0x08, // 171 + 0x0A, 0x02, 0x17, 0x09, // 172 + 0x0A, 0x19, 0x0B, 0x05, // 173 + 0x0A, 0x24, 0x23, 0x0D, // 174 + 0x0A, 0x47, 0x19, 0x0A, // 175 + 0x0A, 0x60, 0x0D, 0x06, // 176 + 0x0A, 0x6D, 0x17, 0x09, // 177 + 0x0A, 0x84, 0x0E, 0x06, // 178 + 0x0A, 0x92, 0x0D, 0x06, // 179 + 0x0A, 0x9F, 0x0A, 0x05, // 180 + 0x0A, 0xA9, 0x17, 0x09, // 181 + 0x0A, 0xC0, 0x19, 0x0A, // 182 + 0x0A, 0xD9, 0x08, 0x04, // 183 + 0x0A, 0xE1, 0x0C, 0x05, // 184 + 0x0A, 0xED, 0x1A, 0x0A, // 185 + 0x0B, 0x07, 0x0D, 0x06, // 186 + 0x0B, 0x14, 0x1A, 0x0A, // 187 + 0x0B, 0x2E, 0x14, 0x08, // 188 + 0x0B, 0x42, 0x26, 0x0E, // 189 + 0x0B, 0x68, 0x26, 0x0E, // 190 + 0x0B, 0x8E, 0x1A, 0x0A, // 191 + 0x0B, 0xA8, 0x1D, 0x0B, // 192 + 0x0B, 0xC5, 0x1D, 0x0B, // 193 + 0x0B, 0xE2, 0x1D, 0x0B, // 194 + 0x0B, 0xFF, 0x1D, 0x0B, // 195 + 0x0C, 0x1C, 0x1D, 0x0B, // 196 + 0x0C, 0x39, 0x1D, 0x0B, // 197 + 0x0C, 0x56, 0x2C, 0x10, // 198 + 0x0C, 0x82, 0x20, 0x0C, // 199 + 0x0C, 0xA2, 0x1D, 0x0B, // 200 + 0x0C, 0xBF, 0x1D, 0x0B, // 201 + 0x0C, 0xDC, 0x1D, 0x0B, // 202 + 0x0C, 0xF9, 0x1D, 0x0B, // 203 + 0x0D, 0x16, 0x05, 0x03, // 204 + 0x0D, 0x1B, 0x07, 0x04, // 205 + 0x0D, 0x22, 0x0A, 0x05, // 206 + 0x0D, 0x2C, 0x07, 0x04, // 207 + 0x0D, 0x33, 0x20, 0x0C, // 208 + 0x0D, 0x53, 0x1D, 0x0B, // 209 + 0x0D, 0x70, 0x20, 0x0C, // 210 + 0x0D, 0x90, 0x20, 0x0C, // 211 + 0x0D, 0xB0, 0x20, 0x0C, // 212 + 0x0D, 0xD0, 0x20, 0x0C, // 213 + 0x0D, 0xF0, 0x20, 0x0C, // 214 + 0x0E, 0x10, 0x17, 0x09, // 215 + 0x0E, 0x27, 0x20, 0x0C, // 216 + 0x0E, 0x47, 0x1D, 0x0B, // 217 + 0x0E, 0x64, 0x1D, 0x0B, // 218 + 0x0E, 0x81, 0x1D, 0x0B, // 219 + 0x0E, 0x9E, 0x1D, 0x0B, // 220 + 0x0E, 0xBB, 0x19, 0x0A, // 221 + 0x0E, 0xD4, 0x1D, 0x0B, // 222 + 0x0E, 0xF1, 0x17, 0x09, // 223 + 0x0F, 0x08, 0x17, 0x09, // 224 + 0x0F, 0x1F, 0x17, 0x09, // 225 + 0x0F, 0x36, 0x17, 0x09, // 226 + 0x0F, 0x4D, 0x17, 0x09, // 227 + 0x0F, 0x64, 0x17, 0x09, // 228 + 0x0F, 0x7B, 0x17, 0x09, // 229 + 0x0F, 0x92, 0x29, 0x0F, // 230 + 0x0F, 0xBB, 0x14, 0x08, // 231 + 0x0F, 0xCF, 0x17, 0x09, // 232 + 0x0F, 0xE6, 0x17, 0x09, // 233 + 0x0F, 0xFD, 0x17, 0x09, // 234 + 0x10, 0x14, 0x17, 0x09, // 235 + 0x10, 0x2B, 0x05, 0x03, // 236 + 0x10, 0x30, 0x07, 0x04, // 237 + 0x10, 0x37, 0x0A, 0x05, // 238 + 0x10, 0x41, 0x07, 0x04, // 239 + 0x10, 0x48, 0x17, 0x09, // 240 + 0x10, 0x5F, 0x14, 0x08, // 241 + 0x10, 0x73, 0x17, 0x09, // 242 + 0x10, 0x8A, 0x17, 0x09, // 243 + 0x10, 0xA1, 0x17, 0x09, // 244 + 0x10, 0xB8, 0x17, 0x09, // 245 + 0x10, 0xCF, 0x17, 0x09, // 246 + 0x10, 0xE6, 0x17, 0x09, // 247 + 0x10, 0xFD, 0x17, 0x09, // 248 + 0x11, 0x14, 0x14, 0x08, // 249 + 0x11, 0x28, 0x14, 0x08, // 250 + 0x11, 0x3C, 0x14, 0x08, // 251 + 0x11, 0x50, 0x14, 0x08, // 252 + 0x11, 0x64, 0x13, 0x08, // 253 + 0x11, 0x77, 0x17, 0x09, // 254 + 0x11, 0x8E, 0x13, 0x08, // 255 + // Font Data: + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 + 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x08, 0x07, 0x00, 0xC8, 0x00, 0x00, 0x28, 0x00, 0x00, 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x42, 0x00, 0x00, 0x41, 0x00, 0x80, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 129 + 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x80, // 130 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x08, 0x03, 0x00, 0x04, 0x04, 0x00, 0x02, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 131 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x9C, 0x01, 0x00, 0x60, 0x02, // 132 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, 0x03, 0x00, 0x80, 0x04, // 133 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 134 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, // 135 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 136 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x50, 0x44, 0x00, 0x48, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 137 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 147 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 148 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0xC1, 0x01, 0x08, 0x41, 0x02, 0x08, 0x41, 0x00, 0x08, 0x40, // 152 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x03, 0x40, 0xC4, 0x04, 0x80, 0x24, 0x00, 0x00, 0x17, // 153 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 154 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x38, // 155 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 + 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 + 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 175 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 + 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x89, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 185 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 187 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x50, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 188 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0xC0, // 191 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, 0x82, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, 0x81, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 + 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, 0x08, 0x04, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, // 198 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, 0x08, 0x40, 0x03, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 199 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 200 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 201 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 202 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 203 + 0x01, 0x00, 0x00, 0xFA, 0x7F, // 204 + 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 205 + 0x02, 0x00, 0x00, 0xF9, 0x7F, 0x00, 0x01, 0x00, 0x00, 0x02, // 206 + 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 207 + 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 208 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 210 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 211 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 212 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 213 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 214 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 215 + 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, 0xC8, 0x40, 0x00, 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, 0x0F, // 216 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 221 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 222 + 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 223 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 225 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, 0x80, 0x7F, // 226 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, 0x80, 0x7F, // 227 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 228 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 229 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x3F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, 0x20, // 231 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 232 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 233 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 234 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 235 + 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 + 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 + 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 + 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, 0x00, 0x1F, // 240 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 242 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 243 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, 0x00, 0x1F, // 244 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, 0x00, 0x1F, // 245 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 246 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 247 + 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, 0x40, 0x1F, // 248 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 249 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, 0x7F, // 250 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, 0x7F, // 251 + 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 252 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 253 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 254 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 255 }; const uint8_t ArialMT_Plain_24_PL[] PROGMEM = { -0x18, // Width: 24 -0x1C, // Height: 28 -0x20, // First char: 32 -0xE0, // Number of chars: 224 -// Jump Table: -0xFF, 0xFF, 0x00, 0x06, // 32 -0x00, 0x00, 0x13, 0x06, // 33 -0x00, 0x13, 0x1A, 0x08, // 34 -0x00, 0x2D, 0x33, 0x0E, // 35 -0x00, 0x60, 0x2F, 0x0D, // 36 -0x00, 0x8F, 0x4F, 0x15, // 37 -0x00, 0xDE, 0x3B, 0x10, // 38 -0x01, 0x19, 0x0A, 0x04, // 39 -0x01, 0x23, 0x1C, 0x08, // 40 -0x01, 0x3F, 0x1B, 0x08, // 41 -0x01, 0x5A, 0x21, 0x0A, // 42 -0x01, 0x7B, 0x32, 0x0E, // 43 -0x01, 0xAD, 0x10, 0x05, // 44 -0x01, 0xBD, 0x1B, 0x08, // 45 -0x01, 0xD8, 0x0F, 0x05, // 46 -0x01, 0xE7, 0x19, 0x08, // 47 -0x02, 0x00, 0x2F, 0x0D, // 48 -0x02, 0x2F, 0x23, 0x0A, // 49 -0x02, 0x52, 0x2F, 0x0D, // 50 -0x02, 0x81, 0x2F, 0x0D, // 51 -0x02, 0xB0, 0x2F, 0x0D, // 52 -0x02, 0xDF, 0x2F, 0x0D, // 53 -0x03, 0x0E, 0x2F, 0x0D, // 54 -0x03, 0x3D, 0x2D, 0x0D, // 55 -0x03, 0x6A, 0x2F, 0x0D, // 56 -0x03, 0x99, 0x2F, 0x0D, // 57 -0x03, 0xC8, 0x0F, 0x05, // 58 -0x03, 0xD7, 0x10, 0x05, // 59 -0x03, 0xE7, 0x2F, 0x0D, // 60 -0x04, 0x16, 0x2F, 0x0D, // 61 -0x04, 0x45, 0x2E, 0x0D, // 62 -0x04, 0x73, 0x2E, 0x0D, // 63 -0x04, 0xA1, 0x5B, 0x18, // 64 -0x04, 0xFC, 0x3B, 0x10, // 65 -0x05, 0x37, 0x3B, 0x10, // 66 -0x05, 0x72, 0x3F, 0x11, // 67 -0x05, 0xB1, 0x3F, 0x11, // 68 -0x05, 0xF0, 0x3B, 0x10, // 69 -0x06, 0x2B, 0x35, 0x0F, // 70 -0x06, 0x60, 0x43, 0x12, // 71 -0x06, 0xA3, 0x3B, 0x10, // 72 -0x06, 0xDE, 0x0F, 0x05, // 73 -0x06, 0xED, 0x27, 0x0B, // 74 -0x07, 0x14, 0x3F, 0x11, // 75 -0x07, 0x53, 0x2F, 0x0D, // 76 -0x07, 0x82, 0x43, 0x12, // 77 -0x07, 0xC5, 0x3B, 0x10, // 78 -0x08, 0x00, 0x47, 0x13, // 79 -0x08, 0x47, 0x3A, 0x10, // 80 -0x08, 0x81, 0x47, 0x13, // 81 -0x08, 0xC8, 0x3F, 0x11, // 82 -0x09, 0x07, 0x3B, 0x10, // 83 -0x09, 0x42, 0x35, 0x0F, // 84 -0x09, 0x77, 0x3B, 0x10, // 85 -0x09, 0xB2, 0x39, 0x10, // 86 -0x09, 0xEB, 0x59, 0x18, // 87 -0x0A, 0x44, 0x3B, 0x10, // 88 -0x0A, 0x7F, 0x3D, 0x11, // 89 -0x0A, 0xBC, 0x37, 0x0F, // 90 -0x0A, 0xF3, 0x14, 0x06, // 91 -0x0B, 0x07, 0x1B, 0x08, // 92 -0x0B, 0x22, 0x18, 0x07, // 93 -0x0B, 0x3A, 0x2A, 0x0C, // 94 -0x0B, 0x64, 0x34, 0x0E, // 95 -0x0B, 0x98, 0x11, 0x06, // 96 -0x0B, 0xA9, 0x2F, 0x0D, // 97 -0x0B, 0xD8, 0x33, 0x0E, // 98 -0x0C, 0x0B, 0x2B, 0x0C, // 99 -0x0C, 0x36, 0x2F, 0x0D, // 100 -0x0C, 0x65, 0x2F, 0x0D, // 101 -0x0C, 0x94, 0x1A, 0x08, // 102 -0x0C, 0xAE, 0x2F, 0x0D, // 103 -0x0C, 0xDD, 0x2F, 0x0D, // 104 -0x0D, 0x0C, 0x0F, 0x05, // 105 -0x0D, 0x1B, 0x10, 0x05, // 106 -0x0D, 0x2B, 0x2F, 0x0D, // 107 -0x0D, 0x5A, 0x0F, 0x05, // 108 -0x0D, 0x69, 0x47, 0x13, // 109 -0x0D, 0xB0, 0x2F, 0x0D, // 110 -0x0D, 0xDF, 0x2F, 0x0D, // 111 -0x0E, 0x0E, 0x33, 0x0E, // 112 -0x0E, 0x41, 0x30, 0x0D, // 113 -0x0E, 0x71, 0x1E, 0x09, // 114 -0x0E, 0x8F, 0x2B, 0x0C, // 115 -0x0E, 0xBA, 0x1B, 0x08, // 116 -0x0E, 0xD5, 0x2F, 0x0D, // 117 -0x0F, 0x04, 0x2A, 0x0C, // 118 -0x0F, 0x2E, 0x42, 0x12, // 119 -0x0F, 0x70, 0x2B, 0x0C, // 120 -0x0F, 0x9B, 0x2A, 0x0C, // 121 -0x0F, 0xC5, 0x2B, 0x0C, // 122 -0x0F, 0xF0, 0x1C, 0x08, // 123 -0x10, 0x0C, 0x10, 0x05, // 124 -0x10, 0x1C, 0x1B, 0x08, // 125 -0x10, 0x37, 0x32, 0x0E, // 126 -0xFF, 0xFF, 0x00, 0x18, // 127 -0xFF, 0xFF, 0x00, 0x18, // 128 -0x10, 0x69, 0x2F, 0x0D, // 129 -0x10, 0x98, 0x16, 0x07, // 130 -0x10, 0xAE, 0x3B, 0x10, // 131 -0x10, 0xE9, 0x40, 0x11, // 132 -0x11, 0x29, 0x34, 0x0E, // 133 -0x11, 0x5D, 0x3F, 0x11, // 134 -0x11, 0x9C, 0x2B, 0x0C, // 135 -0x11, 0xC7, 0x2F, 0x0D, // 136 -0x11, 0xF6, 0x2B, 0x0C, // 137 -0xFF, 0xFF, 0x00, 0x18, // 138 -0xFF, 0xFF, 0x00, 0x18, // 139 -0xFF, 0xFF, 0x00, 0x18, // 140 -0xFF, 0xFF, 0x00, 0x18, // 141 -0xFF, 0xFF, 0x00, 0x18, // 142 -0xFF, 0xFF, 0x00, 0x18, // 143 -0xFF, 0xFF, 0x00, 0x18, // 144 -0xFF, 0xFF, 0x00, 0x18, // 145 -0xFF, 0xFF, 0x00, 0x18, // 146 -0x12, 0x21, 0x47, 0x13, // 147 -0x12, 0x68, 0x2F, 0x0D, // 148 -0xFF, 0xFF, 0x00, 0x18, // 149 -0xFF, 0xFF, 0x00, 0x18, // 150 -0xFF, 0xFF, 0x00, 0x18, // 151 -0x12, 0x97, 0x3B, 0x10, // 152 -0x12, 0xD2, 0x2F, 0x0D, // 153 -0x13, 0x01, 0x3B, 0x10, // 154 -0x13, 0x3C, 0x2B, 0x0C, // 155 -0xFF, 0xFF, 0x00, 0x18, // 156 -0xFF, 0xFF, 0x00, 0x18, // 157 -0xFF, 0xFF, 0x00, 0x18, // 158 -0xFF, 0xFF, 0x00, 0x18, // 159 -0xFF, 0xFF, 0x00, 0x18, // 160 -0x13, 0x67, 0x14, 0x06, // 161 -0x13, 0x7B, 0x2B, 0x0C, // 162 -0x13, 0xA6, 0x2F, 0x0D, // 163 -0x13, 0xD5, 0x33, 0x0E, // 164 -0x14, 0x08, 0x31, 0x0E, // 165 -0x14, 0x39, 0x10, 0x05, // 166 -0x14, 0x49, 0x2F, 0x0D, // 167 -0x14, 0x78, 0x19, 0x08, // 168 -0x14, 0x91, 0x46, 0x13, // 169 -0x14, 0xD7, 0x1A, 0x08, // 170 -0x14, 0xF1, 0x27, 0x0B, // 171 -0x15, 0x18, 0x2F, 0x0D, // 172 -0x15, 0x47, 0x1B, 0x08, // 173 -0x15, 0x62, 0x46, 0x13, // 174 -0x15, 0xA8, 0x31, 0x0E, // 175 -0x15, 0xD9, 0x1E, 0x09, // 176 -0x15, 0xF7, 0x33, 0x0E, // 177 -0x16, 0x2A, 0x1A, 0x08, // 178 -0x16, 0x44, 0x1A, 0x08, // 179 -0x16, 0x5E, 0x19, 0x08, // 180 -0x16, 0x77, 0x2F, 0x0D, // 181 -0x16, 0xA6, 0x31, 0x0E, // 182 -0x16, 0xD7, 0x12, 0x06, // 183 -0x16, 0xE9, 0x18, 0x07, // 184 -0x17, 0x01, 0x37, 0x0F, // 185 -0x17, 0x38, 0x1E, 0x09, // 186 -0x17, 0x56, 0x37, 0x0F, // 187 -0x17, 0x8D, 0x2B, 0x0C, // 188 -0x17, 0xB8, 0x4B, 0x14, // 189 -0x18, 0x03, 0x4B, 0x14, // 190 -0x18, 0x4E, 0x33, 0x0E, // 191 -0x18, 0x81, 0x3B, 0x10, // 192 -0x18, 0xBC, 0x3B, 0x10, // 193 -0x18, 0xF7, 0x3B, 0x10, // 194 -0x19, 0x32, 0x3B, 0x10, // 195 -0x19, 0x6D, 0x3B, 0x10, // 196 -0x19, 0xA8, 0x3B, 0x10, // 197 -0x19, 0xE3, 0x5B, 0x18, // 198 -0x1A, 0x3E, 0x3F, 0x11, // 199 -0x1A, 0x7D, 0x3B, 0x10, // 200 -0x1A, 0xB8, 0x3B, 0x10, // 201 -0x1A, 0xF3, 0x3B, 0x10, // 202 -0x1B, 0x2E, 0x3B, 0x10, // 203 -0x1B, 0x69, 0x11, 0x06, // 204 -0x1B, 0x7A, 0x11, 0x06, // 205 -0x1B, 0x8B, 0x15, 0x07, // 206 -0x1B, 0xA0, 0x15, 0x07, // 207 -0x1B, 0xB5, 0x3F, 0x11, // 208 -0x1B, 0xF4, 0x3B, 0x10, // 209 -0x1C, 0x2F, 0x47, 0x13, // 210 -0x1C, 0x76, 0x47, 0x13, // 211 -0x1C, 0xBD, 0x47, 0x13, // 212 -0x1D, 0x04, 0x47, 0x13, // 213 -0x1D, 0x4B, 0x47, 0x13, // 214 -0x1D, 0x92, 0x2B, 0x0C, // 215 -0x1D, 0xBD, 0x47, 0x13, // 216 -0x1E, 0x04, 0x3B, 0x10, // 217 -0x1E, 0x3F, 0x3B, 0x10, // 218 -0x1E, 0x7A, 0x3B, 0x10, // 219 -0x1E, 0xB5, 0x3B, 0x10, // 220 -0x1E, 0xF0, 0x3D, 0x11, // 221 -0x1F, 0x2D, 0x3A, 0x10, // 222 -0x1F, 0x67, 0x37, 0x0F, // 223 -0x1F, 0x9E, 0x2F, 0x0D, // 224 -0x1F, 0xCD, 0x2F, 0x0D, // 225 -0x1F, 0xFC, 0x2F, 0x0D, // 226 -0x20, 0x2B, 0x2F, 0x0D, // 227 -0x20, 0x5A, 0x2F, 0x0D, // 228 -0x20, 0x89, 0x2F, 0x0D, // 229 -0x20, 0xB8, 0x53, 0x16, // 230 -0x21, 0x0B, 0x2B, 0x0C, // 231 -0x21, 0x36, 0x2F, 0x0D, // 232 -0x21, 0x65, 0x2F, 0x0D, // 233 -0x21, 0x94, 0x2F, 0x0D, // 234 -0x21, 0xC3, 0x2F, 0x0D, // 235 -0x21, 0xF2, 0x11, 0x06, // 236 -0x22, 0x03, 0x11, 0x06, // 237 -0x22, 0x14, 0x15, 0x07, // 238 -0x22, 0x29, 0x15, 0x07, // 239 -0x22, 0x3E, 0x2F, 0x0D, // 240 -0x22, 0x6D, 0x2F, 0x0D, // 241 -0x22, 0x9C, 0x2F, 0x0D, // 242 -0x22, 0xCB, 0x2F, 0x0D, // 243 -0x22, 0xFA, 0x2F, 0x0D, // 244 -0x23, 0x29, 0x2F, 0x0D, // 245 -0x23, 0x58, 0x2F, 0x0D, // 246 -0x23, 0x87, 0x32, 0x0E, // 247 -0x23, 0xB9, 0x33, 0x0E, // 248 -0x23, 0xEC, 0x2F, 0x0D, // 249 -0x24, 0x1B, 0x2F, 0x0D, // 250 -0x24, 0x4A, 0x2F, 0x0D, // 251 -0x24, 0x79, 0x2F, 0x0D, // 252 -0x24, 0xA8, 0x2A, 0x0C, // 253 -0x24, 0xD2, 0x2F, 0x0D, // 254 -0x25, 0x01, 0x2A, 0x0C, // 255 -// Font Data: -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 -0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 -0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 -0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 -0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 -0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 -0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 -0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 -0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 -0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 -0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, // 47 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 -0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 -0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 -0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 -0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 -0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 -0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 -0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 -0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 -0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 -0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 -0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 -0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 -0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 -0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 -0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 -0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 -0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 -0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, // 93 -0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 -0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 -0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 98 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 -0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, // 102 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 -0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 112 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 -0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 -0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 -0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 -0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 -0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 -0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 121 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 -0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 -0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 -0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x03, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 129 -0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, // 130 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x3C, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x04, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 131 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xB0, 0x03, 0x00, 0x00, 0x00, 0x03, // 132 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x00, 0xA0, 0x03, 0x00, 0x00, 0x00, 0x03, // 133 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 134 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 135 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00, 0xE0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 136 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x80, 0xC6, 0x33, 0x00, 0xE0, 0xE6, 0x30, 0x00, 0x60, 0x76, 0x30, 0x00, 0x20, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 137 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0xE2, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 147 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 148 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0xF0, 0x01, 0x60, 0x30, 0xB0, 0x03, 0x60, 0x30, 0x30, 0x03, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 152 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0xF0, 0x01, 0x00, 0xC6, 0xB0, 0x03, 0x00, 0xCE, 0x38, 0x03, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 153 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 154 -0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 155 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 -0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 -0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 -0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 -0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 168 -0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 -0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 -0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 -0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 -0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 -0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 -0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 -0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 -0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 -0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 -0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 180 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 -0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x01, // 184 -0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x68, 0xE0, 0x30, 0x00, 0x6E, 0x38, 0x30, 0x00, 0x66, 0x1C, 0x30, 0x00, 0x62, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 185 -0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 -0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x6C, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 187 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0xC0, 0xC6, 0x33, 0x00, 0xC0, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 188 -0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 -0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, // 191 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 -0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xBC, 0x01, 0x00, 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 199 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 -0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x08, // 204 -0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x02, // 205 -0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, // 206 -0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 207 -0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 208 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, 0x03, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, 0x00, 0x18, 0x00, 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, 0x03, // 215 -0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, 0x00, 0x1F, 0x00, 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 -0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 222 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x08, 0x00, 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 -0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, 0x86, 0x31, 0x00, 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 -0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, 0x06, 0x30, 0x02, 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 231 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 -0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 -0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 -0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 -0x80, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 238 -0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 239 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 -0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 -0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 247 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0x07, // 248 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 -0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 -0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 + 0x18, // Width: 24 + 0x1C, // Height: 28 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + // Jump Table: + 0xFF, 0xFF, 0x00, 0x06, // 32 + 0x00, 0x00, 0x13, 0x06, // 33 + 0x00, 0x13, 0x1A, 0x08, // 34 + 0x00, 0x2D, 0x33, 0x0E, // 35 + 0x00, 0x60, 0x2F, 0x0D, // 36 + 0x00, 0x8F, 0x4F, 0x15, // 37 + 0x00, 0xDE, 0x3B, 0x10, // 38 + 0x01, 0x19, 0x0A, 0x04, // 39 + 0x01, 0x23, 0x1C, 0x08, // 40 + 0x01, 0x3F, 0x1B, 0x08, // 41 + 0x01, 0x5A, 0x21, 0x0A, // 42 + 0x01, 0x7B, 0x32, 0x0E, // 43 + 0x01, 0xAD, 0x10, 0x05, // 44 + 0x01, 0xBD, 0x1B, 0x08, // 45 + 0x01, 0xD8, 0x0F, 0x05, // 46 + 0x01, 0xE7, 0x19, 0x08, // 47 + 0x02, 0x00, 0x2F, 0x0D, // 48 + 0x02, 0x2F, 0x23, 0x0A, // 49 + 0x02, 0x52, 0x2F, 0x0D, // 50 + 0x02, 0x81, 0x2F, 0x0D, // 51 + 0x02, 0xB0, 0x2F, 0x0D, // 52 + 0x02, 0xDF, 0x2F, 0x0D, // 53 + 0x03, 0x0E, 0x2F, 0x0D, // 54 + 0x03, 0x3D, 0x2D, 0x0D, // 55 + 0x03, 0x6A, 0x2F, 0x0D, // 56 + 0x03, 0x99, 0x2F, 0x0D, // 57 + 0x03, 0xC8, 0x0F, 0x05, // 58 + 0x03, 0xD7, 0x10, 0x05, // 59 + 0x03, 0xE7, 0x2F, 0x0D, // 60 + 0x04, 0x16, 0x2F, 0x0D, // 61 + 0x04, 0x45, 0x2E, 0x0D, // 62 + 0x04, 0x73, 0x2E, 0x0D, // 63 + 0x04, 0xA1, 0x5B, 0x18, // 64 + 0x04, 0xFC, 0x3B, 0x10, // 65 + 0x05, 0x37, 0x3B, 0x10, // 66 + 0x05, 0x72, 0x3F, 0x11, // 67 + 0x05, 0xB1, 0x3F, 0x11, // 68 + 0x05, 0xF0, 0x3B, 0x10, // 69 + 0x06, 0x2B, 0x35, 0x0F, // 70 + 0x06, 0x60, 0x43, 0x12, // 71 + 0x06, 0xA3, 0x3B, 0x10, // 72 + 0x06, 0xDE, 0x0F, 0x05, // 73 + 0x06, 0xED, 0x27, 0x0B, // 74 + 0x07, 0x14, 0x3F, 0x11, // 75 + 0x07, 0x53, 0x2F, 0x0D, // 76 + 0x07, 0x82, 0x43, 0x12, // 77 + 0x07, 0xC5, 0x3B, 0x10, // 78 + 0x08, 0x00, 0x47, 0x13, // 79 + 0x08, 0x47, 0x3A, 0x10, // 80 + 0x08, 0x81, 0x47, 0x13, // 81 + 0x08, 0xC8, 0x3F, 0x11, // 82 + 0x09, 0x07, 0x3B, 0x10, // 83 + 0x09, 0x42, 0x35, 0x0F, // 84 + 0x09, 0x77, 0x3B, 0x10, // 85 + 0x09, 0xB2, 0x39, 0x10, // 86 + 0x09, 0xEB, 0x59, 0x18, // 87 + 0x0A, 0x44, 0x3B, 0x10, // 88 + 0x0A, 0x7F, 0x3D, 0x11, // 89 + 0x0A, 0xBC, 0x37, 0x0F, // 90 + 0x0A, 0xF3, 0x14, 0x06, // 91 + 0x0B, 0x07, 0x1B, 0x08, // 92 + 0x0B, 0x22, 0x18, 0x07, // 93 + 0x0B, 0x3A, 0x2A, 0x0C, // 94 + 0x0B, 0x64, 0x34, 0x0E, // 95 + 0x0B, 0x98, 0x11, 0x06, // 96 + 0x0B, 0xA9, 0x2F, 0x0D, // 97 + 0x0B, 0xD8, 0x33, 0x0E, // 98 + 0x0C, 0x0B, 0x2B, 0x0C, // 99 + 0x0C, 0x36, 0x2F, 0x0D, // 100 + 0x0C, 0x65, 0x2F, 0x0D, // 101 + 0x0C, 0x94, 0x1A, 0x08, // 102 + 0x0C, 0xAE, 0x2F, 0x0D, // 103 + 0x0C, 0xDD, 0x2F, 0x0D, // 104 + 0x0D, 0x0C, 0x0F, 0x05, // 105 + 0x0D, 0x1B, 0x10, 0x05, // 106 + 0x0D, 0x2B, 0x2F, 0x0D, // 107 + 0x0D, 0x5A, 0x0F, 0x05, // 108 + 0x0D, 0x69, 0x47, 0x13, // 109 + 0x0D, 0xB0, 0x2F, 0x0D, // 110 + 0x0D, 0xDF, 0x2F, 0x0D, // 111 + 0x0E, 0x0E, 0x33, 0x0E, // 112 + 0x0E, 0x41, 0x30, 0x0D, // 113 + 0x0E, 0x71, 0x1E, 0x09, // 114 + 0x0E, 0x8F, 0x2B, 0x0C, // 115 + 0x0E, 0xBA, 0x1B, 0x08, // 116 + 0x0E, 0xD5, 0x2F, 0x0D, // 117 + 0x0F, 0x04, 0x2A, 0x0C, // 118 + 0x0F, 0x2E, 0x42, 0x12, // 119 + 0x0F, 0x70, 0x2B, 0x0C, // 120 + 0x0F, 0x9B, 0x2A, 0x0C, // 121 + 0x0F, 0xC5, 0x2B, 0x0C, // 122 + 0x0F, 0xF0, 0x1C, 0x08, // 123 + 0x10, 0x0C, 0x10, 0x05, // 124 + 0x10, 0x1C, 0x1B, 0x08, // 125 + 0x10, 0x37, 0x32, 0x0E, // 126 + 0xFF, 0xFF, 0x00, 0x18, // 127 + 0xFF, 0xFF, 0x00, 0x18, // 128 + 0x10, 0x69, 0x2F, 0x0D, // 129 + 0x10, 0x98, 0x16, 0x07, // 130 + 0x10, 0xAE, 0x3B, 0x10, // 131 + 0x10, 0xE9, 0x40, 0x11, // 132 + 0x11, 0x29, 0x34, 0x0E, // 133 + 0x11, 0x5D, 0x3F, 0x11, // 134 + 0x11, 0x9C, 0x2B, 0x0C, // 135 + 0x11, 0xC7, 0x2F, 0x0D, // 136 + 0x11, 0xF6, 0x2B, 0x0C, // 137 + 0xFF, 0xFF, 0x00, 0x18, // 138 + 0xFF, 0xFF, 0x00, 0x18, // 139 + 0xFF, 0xFF, 0x00, 0x18, // 140 + 0xFF, 0xFF, 0x00, 0x18, // 141 + 0xFF, 0xFF, 0x00, 0x18, // 142 + 0xFF, 0xFF, 0x00, 0x18, // 143 + 0xFF, 0xFF, 0x00, 0x18, // 144 + 0xFF, 0xFF, 0x00, 0x18, // 145 + 0xFF, 0xFF, 0x00, 0x18, // 146 + 0x12, 0x21, 0x47, 0x13, // 147 + 0x12, 0x68, 0x2F, 0x0D, // 148 + 0xFF, 0xFF, 0x00, 0x18, // 149 + 0xFF, 0xFF, 0x00, 0x18, // 150 + 0xFF, 0xFF, 0x00, 0x18, // 151 + 0x12, 0x97, 0x3B, 0x10, // 152 + 0x12, 0xD2, 0x2F, 0x0D, // 153 + 0x13, 0x01, 0x3B, 0x10, // 154 + 0x13, 0x3C, 0x2B, 0x0C, // 155 + 0xFF, 0xFF, 0x00, 0x18, // 156 + 0xFF, 0xFF, 0x00, 0x18, // 157 + 0xFF, 0xFF, 0x00, 0x18, // 158 + 0xFF, 0xFF, 0x00, 0x18, // 159 + 0xFF, 0xFF, 0x00, 0x18, // 160 + 0x13, 0x67, 0x14, 0x06, // 161 + 0x13, 0x7B, 0x2B, 0x0C, // 162 + 0x13, 0xA6, 0x2F, 0x0D, // 163 + 0x13, 0xD5, 0x33, 0x0E, // 164 + 0x14, 0x08, 0x31, 0x0E, // 165 + 0x14, 0x39, 0x10, 0x05, // 166 + 0x14, 0x49, 0x2F, 0x0D, // 167 + 0x14, 0x78, 0x19, 0x08, // 168 + 0x14, 0x91, 0x46, 0x13, // 169 + 0x14, 0xD7, 0x1A, 0x08, // 170 + 0x14, 0xF1, 0x27, 0x0B, // 171 + 0x15, 0x18, 0x2F, 0x0D, // 172 + 0x15, 0x47, 0x1B, 0x08, // 173 + 0x15, 0x62, 0x46, 0x13, // 174 + 0x15, 0xA8, 0x31, 0x0E, // 175 + 0x15, 0xD9, 0x1E, 0x09, // 176 + 0x15, 0xF7, 0x33, 0x0E, // 177 + 0x16, 0x2A, 0x1A, 0x08, // 178 + 0x16, 0x44, 0x1A, 0x08, // 179 + 0x16, 0x5E, 0x19, 0x08, // 180 + 0x16, 0x77, 0x2F, 0x0D, // 181 + 0x16, 0xA6, 0x31, 0x0E, // 182 + 0x16, 0xD7, 0x12, 0x06, // 183 + 0x16, 0xE9, 0x18, 0x07, // 184 + 0x17, 0x01, 0x37, 0x0F, // 185 + 0x17, 0x38, 0x1E, 0x09, // 186 + 0x17, 0x56, 0x37, 0x0F, // 187 + 0x17, 0x8D, 0x2B, 0x0C, // 188 + 0x17, 0xB8, 0x4B, 0x14, // 189 + 0x18, 0x03, 0x4B, 0x14, // 190 + 0x18, 0x4E, 0x33, 0x0E, // 191 + 0x18, 0x81, 0x3B, 0x10, // 192 + 0x18, 0xBC, 0x3B, 0x10, // 193 + 0x18, 0xF7, 0x3B, 0x10, // 194 + 0x19, 0x32, 0x3B, 0x10, // 195 + 0x19, 0x6D, 0x3B, 0x10, // 196 + 0x19, 0xA8, 0x3B, 0x10, // 197 + 0x19, 0xE3, 0x5B, 0x18, // 198 + 0x1A, 0x3E, 0x3F, 0x11, // 199 + 0x1A, 0x7D, 0x3B, 0x10, // 200 + 0x1A, 0xB8, 0x3B, 0x10, // 201 + 0x1A, 0xF3, 0x3B, 0x10, // 202 + 0x1B, 0x2E, 0x3B, 0x10, // 203 + 0x1B, 0x69, 0x11, 0x06, // 204 + 0x1B, 0x7A, 0x11, 0x06, // 205 + 0x1B, 0x8B, 0x15, 0x07, // 206 + 0x1B, 0xA0, 0x15, 0x07, // 207 + 0x1B, 0xB5, 0x3F, 0x11, // 208 + 0x1B, 0xF4, 0x3B, 0x10, // 209 + 0x1C, 0x2F, 0x47, 0x13, // 210 + 0x1C, 0x76, 0x47, 0x13, // 211 + 0x1C, 0xBD, 0x47, 0x13, // 212 + 0x1D, 0x04, 0x47, 0x13, // 213 + 0x1D, 0x4B, 0x47, 0x13, // 214 + 0x1D, 0x92, 0x2B, 0x0C, // 215 + 0x1D, 0xBD, 0x47, 0x13, // 216 + 0x1E, 0x04, 0x3B, 0x10, // 217 + 0x1E, 0x3F, 0x3B, 0x10, // 218 + 0x1E, 0x7A, 0x3B, 0x10, // 219 + 0x1E, 0xB5, 0x3B, 0x10, // 220 + 0x1E, 0xF0, 0x3D, 0x11, // 221 + 0x1F, 0x2D, 0x3A, 0x10, // 222 + 0x1F, 0x67, 0x37, 0x0F, // 223 + 0x1F, 0x9E, 0x2F, 0x0D, // 224 + 0x1F, 0xCD, 0x2F, 0x0D, // 225 + 0x1F, 0xFC, 0x2F, 0x0D, // 226 + 0x20, 0x2B, 0x2F, 0x0D, // 227 + 0x20, 0x5A, 0x2F, 0x0D, // 228 + 0x20, 0x89, 0x2F, 0x0D, // 229 + 0x20, 0xB8, 0x53, 0x16, // 230 + 0x21, 0x0B, 0x2B, 0x0C, // 231 + 0x21, 0x36, 0x2F, 0x0D, // 232 + 0x21, 0x65, 0x2F, 0x0D, // 233 + 0x21, 0x94, 0x2F, 0x0D, // 234 + 0x21, 0xC3, 0x2F, 0x0D, // 235 + 0x21, 0xF2, 0x11, 0x06, // 236 + 0x22, 0x03, 0x11, 0x06, // 237 + 0x22, 0x14, 0x15, 0x07, // 238 + 0x22, 0x29, 0x15, 0x07, // 239 + 0x22, 0x3E, 0x2F, 0x0D, // 240 + 0x22, 0x6D, 0x2F, 0x0D, // 241 + 0x22, 0x9C, 0x2F, 0x0D, // 242 + 0x22, 0xCB, 0x2F, 0x0D, // 243 + 0x22, 0xFA, 0x2F, 0x0D, // 244 + 0x23, 0x29, 0x2F, 0x0D, // 245 + 0x23, 0x58, 0x2F, 0x0D, // 246 + 0x23, 0x87, 0x32, 0x0E, // 247 + 0x23, 0xB9, 0x33, 0x0E, // 248 + 0x23, 0xEC, 0x2F, 0x0D, // 249 + 0x24, 0x1B, 0x2F, 0x0D, // 250 + 0x24, 0x4A, 0x2F, 0x0D, // 251 + 0x24, 0x79, 0x2F, 0x0D, // 252 + 0x24, 0xA8, 0x2A, 0x0C, // 253 + 0x24, 0xD2, 0x2F, 0x0D, // 254 + 0x25, 0x01, 0x2A, 0x0C, // 255 + // Font Data: + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 + 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 + 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 121 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 + 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x03, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 129 + 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, // 130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x3C, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x04, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 131 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xB0, 0x03, 0x00, 0x00, 0x00, 0x03, // 132 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x00, 0xA0, 0x03, 0x00, 0x00, 0x00, 0x03, // 133 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 134 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 135 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00, 0xE0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 136 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x80, 0xC6, 0x33, 0x00, 0xE0, 0xE6, 0x30, 0x00, 0x60, 0x76, 0x30, 0x00, 0x20, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 137 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0xE2, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 147 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 148 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0xF0, 0x01, 0x60, 0x30, 0xB0, 0x03, 0x60, 0x30, 0x30, 0x03, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 152 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0xF0, 0x01, 0x00, 0xC6, 0xB0, 0x03, 0x00, 0xCE, 0x38, 0x03, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 153 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 154 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 155 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 + 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 + 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 168 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 + 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 + 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x01, // 184 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x68, 0xE0, 0x30, 0x00, 0x6E, 0x38, 0x30, 0x00, 0x66, 0x1C, 0x30, 0x00, 0x62, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 185 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x6C, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 187 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0xC0, 0xC6, 0x33, 0x00, 0xC0, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 188 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, // 191 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xBC, 0x01, 0x00, 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 199 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x08, // 204 + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x02, // 205 + 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, // 206 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 207 + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 208 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, 0x03, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, 0x00, 0x18, 0x00, 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, 0x03, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, 0x00, 0x1F, 0x00, 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x08, 0x00, 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, 0x86, 0x31, 0x00, 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, 0x06, 0x30, 0x02, 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 231 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 + 0x80, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 238 + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 239 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 247 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0x07, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 }; \ No newline at end of file diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp new file mode 100644 index 000000000..7e4f0b709 --- /dev/null +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp @@ -0,0 +1,110 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./LatchingBacklight.h" + +#include "assert.h" + +#include "sleep.h" + +using namespace NicheGraphics::Drivers; + +// Private constructor +// Called by getInstance +LatchingBacklight::LatchingBacklight() +{ + // Attach the deep sleep callback + deepSleepObserver.observe(¬ifyDeepSleep); +} + +// Get access to (or create) the singleton instance of this class +LatchingBacklight *LatchingBacklight::getInstance() +{ + // Instantiate the class the first time this method is called + static LatchingBacklight *const singletonInstance = new LatchingBacklight; + + return singletonInstance; +} + +// Which pin controls the backlight? +// Is the light active HIGH (default) or active LOW? +void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) +{ + this->pin = pin; + this->logicActive = activeWhen; + + pinMode(pin, OUTPUT); + off(); // Explicit off seem required by T-Echo? +} + +// Called when device is shutting down +// Ensures the backlight is off +int LatchingBacklight::beforeDeepSleep(void *unused) +{ + // We shouldn't need to guard the block like this + // Contingency for: + // - settings corruption: settings.optionalMenuItems.backlight guards backlight code in MenuApplet + // - improper use in the future + if (pin != (uint8_t)-1) { + off(); + pinMode(pin, INPUT); // High impedence - unnecessary? + } else + LOG_WARN("LatchingBacklight instantiated, but pin not set"); + return 0; // Continue with deep sleep +} + +// Turn the backlight on *temporarily* +// This should be used for momentary illumination, such as while a button is held +// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling +void LatchingBacklight::peek() +{ + assert(pin != (uint8_t)-1); + digitalWrite(pin, logicActive); // On + on = true; + latched = false; +} + +// Turn the backlight on, and keep it on +// This should be used when the backlight should remain active, even after user input ends +// e.g. when enabled via the menu +// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling +void LatchingBacklight::latch() +{ + assert(pin != (uint8_t)-1); + + // Blink if moving from peek to latch + // Indicates to user that the transition has taken place + if (on && !latched) { + digitalWrite(pin, !logicActive); // Off + delay(25); + digitalWrite(pin, logicActive); // On + delay(25); + digitalWrite(pin, !logicActive); // Off + delay(25); + } + + digitalWrite(pin, logicActive); // On + on = true; + latched = true; +} + +// Turn the backlight off +// Suitable for ending both peek and latch +void LatchingBacklight::off() +{ + assert(pin != (uint8_t)-1); + digitalWrite(pin, !logicActive); // Off + on = false; + latched = false; +} + +bool LatchingBacklight::isOn() +{ + return on; +} + +bool LatchingBacklight::isLatched() +{ + return latched; +} + +#endif diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h new file mode 100644 index 000000000..0097cae4c --- /dev/null +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h @@ -0,0 +1,50 @@ +/* + + Singleton class + On-demand control of a display's backlight, connected to a GPIO + Initial use case is control of T-Echo's frontlight, via the capacitive touch button + + - momentary on + - latched on + +*/ + +#pragma once + +#include "configuration.h" + +#include "Observer.h" + +namespace NicheGraphics::Drivers +{ + +class LatchingBacklight +{ + public: + static LatchingBacklight *getInstance(); // Create or get the singleton instance + void setPin(uint8_t pin, bool activeWhen = HIGH); + + int beforeDeepSleep(void *unused); // Callback for auto-shutoff + + void peek(); // Backlight on temporarily, e.g. while button held + void latch(); // Backlight on permanently, e.g. toggled via menu + void off(); // Backlight off. Suitable for both peek and latch + + bool isOn(); // Either peek or latch + bool isLatched(); + + private: + LatchingBacklight(); // Constructor made private: force use of getInstance + + // Get notified when the system is shutting down + CallbackObserver deepSleepObserver = + CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); + + uint8_t pin = (uint8_t)-1; + bool logicActive = HIGH; // Is light active HIGH or active LOW + + bool on = false; // Is light on (either peek or latched) + bool latched = false; // Is light latched on +}; + +} // namespace NicheGraphics::Drivers \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp new file mode 100644 index 000000000..b8715ed1d --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp @@ -0,0 +1 @@ +#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 new file mode 100644 index 000000000..62d42ef57 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h @@ -0,0 +1,34 @@ +/* + +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/DEPG0290BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp new file mode 100644 index 000000000..5f3a05670 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp @@ -0,0 +1,120 @@ +#include "./DEPG0290BNS800.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Describes the operation performed when a "fast refresh" is performed +// Source: custom, with DEPG0150BNS810 as a reference +static const uint8_t LUT_FAST[] = { + // 1 2 3 4 + 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels) + 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels) + 0x00, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels) + 0x00, 0x00, 0x00, 0x80, 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 + + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1. Tap existing black pixels back into place + 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. Move new pixels + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. New pixels, and also existing black pixels + 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // 4. All pixels, then 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, // + + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, +}; + +// How strongly the pixels are pulled and pushed +void DEPG0290BNS800::configVoltages() +{ + switch (updateType) { + case FAST: + // Listed as "typical" in datasheet + sendCommand(0x04); + sendData(0x41); // VSH1 15V + sendData(0x00); // VSH2 NA + sendData(0x32); // VSL -15V + 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 DEPG0290BNS800::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x60); // Actively hold screen border during update + + 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 DEPG0290BNS800::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 DEPG0290BNS800::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 450); // At least 450ms for fast refresh + case FULL: + default: + return beginPolling(100, 3000); // At least 3 seconds for full refresh + } +} + +// 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 DEPG0290BNS800::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(); + } +} +#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 new file mode 100644 index 000000000..72062e0d6 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -0,0 +1,42 @@ +/* + +E-Ink display driver + - DEPG0290BNS800 + - Manufacturer: DKE + - Size: 2.9 inch + - Resolution: 128px x 296px + - Flex connector marking: FPC-7519 rev.b + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class DEPG0290BNS800 : 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: + DEPG0290BNS800() : 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/EInk.cpp b/src/graphics/niche/Drivers/EInk/EInk.cpp new file mode 100644 index 000000000..0abe20bf9 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/EInk.cpp @@ -0,0 +1,70 @@ +#include "./EInk.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants +EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported) + : concurrency::OSThread("E-Ink Driver"), width(width), height(height), supportedUpdateTypes(supported) +{ + OSThread::disable(); +} + +// Used by NicheGraphics implementations to check if a display supports a specific refresh operation. +// Whether or the update type is supported is specified in the constructor +bool EInk::supports(UpdateTypes type) +{ + // The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set. + if (supportedUpdateTypes & type) + return true; + else + return false; +} + +// Begins using the OSThread to detect when a display update is complete +// This allows the refresh operation to run "asynchronously". +// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin +// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes. +// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", +// provided its isUpdateDone() override always returns true. +void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) +{ + updateRunning = true; + updateBegunAt = millis(); + pollingInterval = interval; + + // To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take + // By default, expectedDuration is 0, and we'll start polling immediately + OSThread::setIntervalFromNow(expectedDuration); + OSThread::enabled = true; +} + +// Meshtastic's pseudo-threading layer +// We're using this as a timer, to periodically check if an update is complete +// This is what allows us to update the display asynchronously +int32_t EInk::runOnce() +{ + if (!isUpdateDone()) + return pollingInterval; // Poll again in a few ms + + // If update done: + finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc + updateRunning = false; // Change what we report via EInk::busy() + return disable(); // Stop polling +} + +// Wait for an in progress update to complete before continuing +// Run a normal (async) update first, *then* call await +void EInk::await() +{ + // Stop our concurrency thread + OSThread::disable(); + + // Sit and block until the update is complete + while (updateRunning) { + runOnce(); + yield(); + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/EInk.h b/src/graphics/niche/Drivers/EInk/EInk.h new file mode 100644 index 000000000..1fbc25a14 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/EInk.h @@ -0,0 +1,56 @@ +/* + + Base class for E-Ink display drivers + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +#include "configuration.h" + +#include "concurrency/OSThread.h" +#include + +namespace NicheGraphics::Drivers +{ + +class EInk : private concurrency::OSThread +{ + public: + // Different possible operations used to update an E-Ink display + // Some displays will not support all operations + // Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType) + enum UpdateTypes : uint8_t { + UNSPECIFIED = 0, + FULL = 1 << 0, + FAST = 1 << 1, + }; + + EInk(uint16_t width, uint16_t height, UpdateTypes supported); + virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0; + virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image + void await(); // Wait for an in-progress update to complete before proceeding + bool supports(UpdateTypes type); // Can display perfom a certain update type + bool busy() { return updateRunning; } // Display able to update right now? + + const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const. + const uint16_t height; + + protected: + void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished + virtual bool isUpdateDone() = 0; // Check once if update finished + virtual void finalizeUpdate() {} // Run any post-update code + + private: + int32_t runOnce() override; // Repeated checking if update finished + + const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class + bool updateRunning = false; // see EInk::busy() + uint32_t updateBegunAt; // For initial pause before polling for update completion + uint32_t pollingInterval; // How often to check if update complete (ms) +}; + +} // 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 new file mode 100644 index 000000000..bfc5ac681 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp @@ -0,0 +1,61 @@ +#include "./GDEY0154D67.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the conected panel +void GDEY0154D67::configScanning() +{ + // "Driver output control" + sendCommand(0x01); + sendData(0xC7); + 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 +// - 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 GDEY0154D67::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 GDEY0154D67::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 GDEY0154D67::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/GDEY0154D67.h b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h new file mode 100644 index 000000000..fc4d93d12 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h @@ -0,0 +1,42 @@ +/* + +E-Ink display driver + - GDEY0154D67 + - Manufacturer: Goodisplay + - Size: 1.54 inch + - Resolution: 200px x 200px + - Flex connector marking: FPC-B001 + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class GDEY0154D67 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 200; + static constexpr uint32_t height = 200; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + GDEY0154D67() : 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/LCMEN2R13EFC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp new file mode 100644 index 000000000..c54769fc2 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp @@ -0,0 +1,301 @@ +#include "./LCMEN2R13EFC1.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include + +using namespace NicheGraphics::Drivers; + +// Look up table: fast refresh, common electrode +static const uint8_t LUT_FAST_VCOMDC[] = { + 0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01, // + 0x01, 0x06, 0x02, 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, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +// Look up table: fast refresh, pixels which remain white +static const uint8_t LUT_FAST_WW[] = { + 0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01, // + 0x01, 0x06, 0x02, 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, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +// Look up table: fast refresh, pixel which change from black to white +static const uint8_t LUT_FAST_BW[] = { + 0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01, // + 0x01, 0x86, 0x82, 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, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +// Look up table: fash 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, // + 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, // +}; + +// Look up table: fash 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, // + 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, // +}; + +LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) +{ + // Pre-calculate size of the image buffer, for convenience + + // Determine the X dimension of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + bufferRowSize = ((width - 1) / 8) + 1; + + // Total size of image buffer, in bytes. + bufferSize = bufferRowSize * height; +} + +void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; + + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); + + // Reset is active low, hold high + pinMode(pin_rst, INPUT_PULLUP); + + reset(); +} + +void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) +{ + this->updateType = type; + this->buffer = imageData; + + reset(); + + // Config + if (updateType == FULL) + configFull(); + else + configFast(); + + // Transfer image data + if (updateType == FULL) { + writeNewImage(); + writeOldImage(); + } else { + writeNewImage(); + } + + sendCommand(0x04); // Power on the panel voltage + wait(); + + sendCommand(0x12); // Begin executing the update + + // Let the update run async, on display hardware. Base class will poll completion, then finalize. + // For a blocking update, call await after update + detachFromUpdate(); +} + +void LCMEN213EFC1::wait() +{ + // Busy when LOW + while (digitalRead(pin_busy) == LOW) + yield(); +} + +void LCMEN213EFC1::reset() +{ + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, LOW); + delay(10); + pinMode(pin_rst, INPUT_PULLUP); + wait(); + + sendCommand(0x12); + wait(); +} + +void LCMEN213EFC1::sendCommand(const uint8_t command) +{ + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); // DC pin low indicates command + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); +} + +void LCMEN213EFC1::sendData(uint8_t data) +{ + // spi->beginTransaction(spiSettings); + // digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + // digitalWrite(pin_cs, LOW); + // spi->transfer(data); + // digitalWrite(pin_cs, HIGH); + // digitalWrite(pin_dc, HIGH); + // spi->endTransaction(); + sendData(&data, 1); +} + +void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) +{ + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + digitalWrite(pin_cs, LOW); + + // Platform-specific SPI command + // Mothballing. This display model is only used by Heltec Wireless Paper (ESP32) +#if defined(ARCH_ESP32) + spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer +#elif defined(ARCH_NRF52) + spi->transfer(data, NULL, size); // NULL for a "write only" transfer +#else +#error Not implemented yet? Feel free to add other platforms here. +#endif + + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); +} + +void LCMEN213EFC1::configFull() +{ + sendCommand(0x00); // Panel setting register + sendData(0b11 << 6 // Display resolution + | 1 << 4 // B&W only + | 1 << 3 // Vertical scan direction + | 1 << 2 // Horizontal scan direction + | 1 << 1 // Shutdown: no + | 1 << 0 // Reset: no + ); + + sendCommand(0x50); // VCOM and data interval setting register + sendData(0b10 << 6 // Border driven white + | 0b11 << 4 // Invert image colors: no + | 0b0111 << 0 // Interval between VCOM on and image data (default) + ); +} + +void LCMEN213EFC1::configFast() +{ + sendCommand(0x00); // Panel setting register + sendData(0b11 << 6 // Display resolution + | 1 << 5 // LUT from registers (set below) + | 1 << 4 // B&W only + | 1 << 3 // Vertical scan direction + | 1 << 2 // Horizontal scan direction + | 1 << 1 // Shutdown: no + | 1 << 0 // Reset: no + ); + + sendCommand(0x50); // VCOM and data interval setting register + sendData(0b11 << 6 // Border floating + | 0b01 << 4 // Invert image colors: no + | 0b0111 << 0 // Interval between VCOM on and image data (default) + ); + + // Load the various LUTs + sendCommand(0x20); // VCOM + sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC)); + + sendCommand(0x21); // White -> White + sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW)); + + sendCommand(0x22); // Black -> White + sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW)); + + sendCommand(0x23); // White -> Black + sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB)); + + sendCommand(0x24); // Black -> Black + sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB)); +} + +void LCMEN213EFC1::writeNewImage() +{ + sendCommand(0x13); + sendData(buffer, bufferSize); +} + +void LCMEN213EFC1::writeOldImage() +{ + sendCommand(0x10); + sendData(buffer, bufferSize); +} + +void LCMEN213EFC1::detachFromUpdate() +{ + // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types + // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed + // If not implemented, we'll just poll right from the get-go + switch (updateType) { + case FULL: + EInk::beginPolling(10, 3650); + break; + case FAST: + EInk::beginPolling(10, 720); + break; + default: + assert(false); + } +} + +bool LCMEN213EFC1::isUpdateDone() +{ + // Busy when LOW + if (digitalRead(pin_busy) == LOW) + return false; + else + return true; +} + +void LCMEN213EFC1::finalizeUpdate() +{ + // Power off the panel voltages + sendCommand(0x02); + wait(); + + // 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) { + writeOldImage(); + wait(); + } +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h new file mode 100644 index 000000000..5c801c014 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h @@ -0,0 +1,68 @@ +/* + +E-Ink display driver + - LCMEN213EFC1 + - Manufacturer: Wisevast + - Size: 2.13 inch + - Resolution: 122px x 250px + - Flex connector marking: 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. + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./EInk.h" + +namespace NicheGraphics::Drivers +{ + +class LCMEN213EFC1 : public EInk +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + LCMEN213EFC1(); + void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst); + void update(uint8_t *imageData, UpdateTypes type) override; + + protected: + void wait(); + void reset(); + void sendCommand(const uint8_t command); + void sendData(const uint8_t data); + void sendData(const uint8_t *data, uint32_t size); + void configFull(); // Configure display for FULL refresh + void configFast(); // Configure display for FAST refresh + void writeNewImage(); + void writeOldImage(); + + void detachFromUpdate(); + bool isUpdateDone(); + void finalizeUpdate(); + + protected: + uint8_t bufferOffsetX; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? + uint8_t bufferRowSize; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) + uint32_t bufferSize; // In bytes. Rows * Columns + uint8_t *buffer; + UpdateTypes updateType; + + uint8_t pin_dc, pin_cs, pin_busy, pin_rst; + SPIClass *spi; + SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0); +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/README.md b/src/graphics/niche/Drivers/EInk/README.md new file mode 100644 index 000000000..ffe21e507 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/README.md @@ -0,0 +1,82 @@ +# NicheGraphics - E-Ink Driver + +A driver for E-Ink SPI displays. Suitable for re-use by various NicheGraphics UIs. + +Your UI should use the class `NicheGraphics::Drivers::EInk` . +When you set up a hardware variant, you will use one of specific display model classes, which extend the EInk class. + +An example setup might look like this: + +```cpp +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // An imaginary UI + YourCustomUI *yourUI = new YourCustomUI(); + + // Setup SPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // Setup Enk driver + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + + // Pass the driver to your UI + YourUI::driver = driver; +} +``` + +## Methods + +### `update(uint8_t *imageData, UpdateTypes type, bool async=true)` + +Update the image on the display + +- _`imageData`_ to draw to the display. +- _`type`_ which type of update to perform. + - `FULL` + - `FAST` + - (Other custom types may be possible) +- _`async`_ whether to wait for update to complete, or continue code execution + +The imageData is a 1-bit image. X-Pixels are 8-per byte, with the MSB being the leftmost pixel. This was not an InkHUD design decision; it is the raw format accepted by the E-Ink display controllers ICs. + +_To-do: add a helper method to `InkHUD::Drivers::EInk` to do this arithmetic for you._ + +```cpp +uint16_t w = driver::width(); +uint16_t h = driver::height(); + +uint8_t image[ (w/8) * h ]; // X pixels are 8-per-byte + +image[0] |= (1 << 7); // Set pixel x=0, y=0 +image[0] |= (1 << 0); // Set pixel x=7, y=0 +image[1] |= (1 << 7); // Set pixel x=8, y=0 + +uint8_t x = 12; +uint8_t y = 2; +uint8_t yBytes = y * (w/8); +uint8_t xBytes = x / 8; +uint8_t xBits = (7-x) % 8; +image[yByte + xByte] |= (1 << xBits); // Set pixel x=12, y=2 +``` + +### `supports(UpdateTypes type)` + +Check if display supports a specific update type. `true` if supported. + +- _`type`_ type to check + +### `busy()` + +Check if display is already performing an `update()`. `true` if already updating. + +### `width()` + +Width of the display, in pixels. Note: most displays are portait. Your UI will need to implement rotation in software. + +### `height()` + +Height of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software. diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp new file mode 100644 index 000000000..d58e5b37a --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp @@ -0,0 +1,227 @@ +#include "./SSD16XX.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +using namespace NicheGraphics::Drivers; + +SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX) + : EInk(width, height, supported), bufferOffsetX(bufferOffsetX) +{ + // Pre-calculate size of the image buffer, for convenience + + // Determine the X dimension of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + bufferRowSize = ((width - 1) / 8) + 1; + + // Total size of image buffer, in bytes. + bufferSize = bufferRowSize * height; +} + +void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; + + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); + + // If using a reset pin, hold high + // Reset is active low for solmon systech ICs + if (pin_rst != 0xFF) + pinMode(pin_rst, INPUT_PULLUP); + + reset(); +} + +void SSD16XX::wait() +{ + // Busy when HIGH + while (digitalRead(pin_busy) == HIGH) + yield(); +} + +void SSD16XX::reset() +{ + // Check if reset pin is defined + if (pin_rst != 0xFF) { + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, LOW); + delay(50); + pinMode(pin_rst, INPUT_PULLUP); + wait(); + } + + sendCommand(0x12); + wait(); +} + +void SSD16XX::sendCommand(const uint8_t command) +{ + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); // DC pin low indicates command + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); +} + +void SSD16XX::sendData(uint8_t data) +{ + // spi->beginTransaction(spiSettings); + // digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + // digitalWrite(pin_cs, LOW); + // spi->transfer(data); + // digitalWrite(pin_cs, HIGH); + // digitalWrite(pin_dc, HIGH); + // spi->endTransaction(); + sendData(&data, 1); +} + +void SSD16XX::sendData(const uint8_t *data, uint32_t size) +{ + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + digitalWrite(pin_cs, LOW); + + // Platform-specific SPI command +#if defined(ARCH_ESP32) + spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer +#elif defined(ARCH_NRF52) + spi->transfer(data, NULL, size); // NULL for a "write only" transfer +#else +#error Not implemented yet? Feel free to add other platforms here. +#endif + + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); +} + +void SSD16XX::configFullscreen() +{ + // Placing this code in a separate method because it's probably pretty consistent between displays + // Should make it tidier to override SSD16XX::configure + + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint16_t sx = bufferOffsetX; // Notice the offset + static const uint16_t sy = 0; + static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint16_t ey = height; + + // Split into bytes + static const uint8_t sy1 = sy & 0xFF; + static const uint8_t sy2 = (sy >> 8) & 0xFF; + static const uint8_t ey1 = ey & 0xFF; + static const uint8_t ey2 = (ey >> 8) & 0xFF; + + // 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(sy1); + sendData(sy2); + sendData(ey1); + sendData(ey2); + + // 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(sy1); + sendData(sy2); +} + +void SSD16XX::update(uint8_t *imageData, UpdateTypes type) +{ + this->updateType = type; + this->buffer = imageData; + + reset(); + + configFullscreen(); + configScanning(); // Virtual, unused by base class + configVoltages(); // Virtual, unused by base class + configWaveform(); // Virtual, unused by base class + wait(); + + if (updateType == FULL) { + writeNewImage(); + writeOldImage(); + } else { + writeNewImage(); + } + + configUpdateSequence(); + sendCommand(0x20); // Begin executing the update + + // Let the update run async, on display hardware. Base class will poll completion, then finalize. + // For a blocking update, call await after update + detachFromUpdate(); +} + +// Send SPI commands for controller IC to begin executing the refresh operation +void SSD16XX::configUpdateSequence() +{ + switch (updateType) { + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } +} + +void SSD16XX::writeNewImage() +{ + sendCommand(0x24); + sendData(buffer, bufferSize); +} + +void SSD16XX::writeOldImage() +{ + sendCommand(0x26); + sendData(buffer, bufferSize); +} + +void SSD16XX::detachFromUpdate() +{ + // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types + // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed + // If not implemented, we'll just poll right from the get-go + switch (updateType) { + default: + EInk::beginPolling(100, 0); + } +} + +bool SSD16XX::isUpdateDone() +{ + // Busy when HIGH + if (digitalRead(pin_busy) == HIGH) + return false; + else + return true; +} + +void SSD16XX::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(); // Only required by some controller variants. Todo: Override just for GDEY0154D678? + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } +} +#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 new file mode 100644 index 000000000..f9077f188 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.h @@ -0,0 +1,62 @@ +/* + +E-Ink base class for displays based on SSD16XX + +Most (but not all) SPI E-Ink displays use this family of controller IC. +Implementing new SSD16XX displays should be fairly painless. +See DEPG0154BNS800 and DEPG0290BNS800 for examples. + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./EInk.h" + +namespace NicheGraphics::Drivers +{ + +class SSD16XX : public EInk +{ + public: + SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1); + virtual void update(uint8_t *imageData, UpdateTypes type) override; + + protected: + virtual void wait(); + virtual void reset(); + virtual void sendCommand(const uint8_t command); + virtual void sendData(const uint8_t data); + virtual void sendData(const uint8_t *data, uint32_t size); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void configScanning() {} // Optional. First & last gates, scan direction, etc + virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc + virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc + virtual void configUpdateSequence(); // Tell controller IC which operations to run + + virtual void writeNewImage(); + virtual void writeOldImage(); + + virtual void detachFromUpdate(); + virtual bool isUpdateDone() override; + virtual void finalizeUpdate() override; + + protected: + uint8_t bufferOffsetX; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? + uint8_t bufferRowSize; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) + uint32_t bufferSize; // In bytes. Rows * Columns + uint8_t *buffer; + UpdateTypes updateType; + + uint8_t pin_dc, pin_cs, pin_busy, pin_rst; + SPIClass *spi; + SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/README.md b/src/graphics/niche/Drivers/README.md new file mode 100644 index 000000000..566558658 --- /dev/null +++ b/src/graphics/niche/Drivers/README.md @@ -0,0 +1,3 @@ +# NicheGraphics - Drivers + +Common drivers which can be used by various NicheGrapihcs UIs diff --git a/src/graphics/niche/FlashData.h b/src/graphics/niche/FlashData.h new file mode 100644 index 000000000..4a436d387 --- /dev/null +++ b/src/graphics/niche/FlashData.h @@ -0,0 +1,140 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics tool + +Save settings / data to flash, without use of the Meshtastic Protobufs +Avoid bloating everyone's protobuf code for our one-off UI implementations + +*/ + +#pragma once + +#include "configuration.h" + +#include "SafeFile.h" + +namespace NicheGraphics +{ + +template class FlashData +{ + private: + static std::string getFilename(const char *label) + { + std::string filename; + filename += "/NicheGraphics"; + filename += "/"; + filename += label; + filename += ".data"; + + return filename; + } + + static uint32_t getHash(T *data) + { + uint32_t hash = 0; + + // Sum all bytes of the image buffer together + for (uint32_t i = 0; i < sizeof(T); i++) + hash ^= ((uint8_t *)data)[i] + 1; + + return hash; + } + + public: + static bool load(T *data, const char *label) + { + // Set false if we run into issues + bool okay = true; + + // Get a filename based on the label + std::string filename = getFilename(label); + +#ifdef FSCom + + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_WARN("'%s' not found. Using default values", filename.c_str()); + okay = false; + return okay; + } + + // Open the file + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + + // If opened, start reading + if (f) { + LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str()); + + // Create an object which will received data from flash + // We read here first, so we can verify the checksum, without committing to overwriting the *data object + // Allows us to retain any defaults that might be set after we declared *data, but before loading settings, + // in case the flash values are corrupt + T flashData; + + // Read the actual data + f.readBytes((char *)&flashData, sizeof(T)); + + // Read the hash + uint32_t savedHash = 0; + f.readBytes((char *)&savedHash, sizeof(savedHash)); + + // Calculate hash of the loaded data, then compare with the saved hash + // If hash looks good, copy the values to the main data object + uint32_t calculatedHash = getHash(&flashData); + if (savedHash != calculatedHash) { + LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str()); + okay = false; + } else + *data = flashData; + + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename.c_str()); + okay = false; + } +#else + LOG_ERROR("Filesystem not implemented"); + state = LoadFileState::NO_FILESYSTEM; + okay = false; +#endif + return okay; + } + + // Save module's custom data (settings?) to flash. Does use protobufs + static void save(T *data, const char *label) + { + // Get a filename based on the label + std::string filename = getFilename(label); + +#ifdef FSCom + FSCom.mkdir("/NicheGraphics"); + + auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. + + LOG_INFO("Saving %s", filename.c_str()); + + // Calculate a hash of the data + uint32_t hash = getHash(data); + + f.write((uint8_t *)data, sizeof(T)); // Write the actualy data + f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash + + // f.flush(); + + bool writeSucceeded = f.close(); + + if (!writeSucceeded) { + LOG_ERROR("Can't write data!"); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented\n"); +#endif + } +}; + +} // namespace NicheGraphics + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Fonts/FreeSans6pt7b.h b/src/graphics/niche/Fonts/FreeSans6pt7b.h new file mode 100644 index 000000000..c5bcc32c4 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt7b.h @@ -0,0 +1,129 @@ +#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}; + +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 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 new file mode 100644 index 000000000..49f03d4e1 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h @@ -0,0 +1,302 @@ +/* + +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 Niche Graphics implementation needs to identify these, and subsitute 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/README.md b/src/graphics/niche/Fonts/README.md new file mode 100644 index 000000000..e79927786 --- /dev/null +++ b/src/graphics/niche/Fonts/README.md @@ -0,0 +1,4 @@ +# NicheGraphics - Fonts + +A common area to store fonts which might be reused by different Niche Graphics UIs +In future, we may want to separate these by library (AdafruitGFX, u8g2, etc) diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp new file mode 100644 index 000000000..ebd0acc78 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -0,0 +1,843 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./Applet.h" + +#include "RTC.h" + +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 +constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo + +InkHUD::Applet::Applet() : GFX(0, 0) +{ + // GFX is given initial dimensions of 0 + // The width and height will change dynamically, depending on Applet tiling + // If you're getting a "divide by zero error", consider it an assert: + // WindowManager should be the only one controlling the rendering +} + +// The raw pixel output generated by AdafruitGFX drawing +// Hand off to the applet's tile, which will in-turn pass to the window manager +void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) +{ + // Only render pixels if they fall within user's cropped region + if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) + assignedTile->handleAppletPixel(x, y, (Color)color); +} + +// Sets which tile the applet renders for +// Pixel output is passed to tile during render() +// This should only be called by Tile::assignApplet +void InkHUD::Applet::setTile(Tile *t) +{ + // If we're setting (not clearing), make sure the link is "reciprocal" + if (t) + assert(t->getAssignedApplet() == this); + + assignedTile = t; +} + +// Which tile will the applet render() to? +InkHUD::Tile *InkHUD::Applet::getTile() +{ + return assignedTile; +} + +void InkHUD::Applet::render() +{ + assert(assignedTile); // Ensure that we have a tile + assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile + + wantRender = false; // Clear the flag set by requestUpdate + wantAutoshow = false; // If we're rendering now, it means our request was considered. It may or may not have been granted. + wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Our requested type has been considered by now. Tidy up. + + updateDimensions(); + resetDrawingSpace(); + onRender(); // Derived applet's drawing takes place here + + // If our tile is (or was) highlighted, to indicate a change in focus + if (Tile::highlightTarget == assignedTile) { + // Draw the highlight + if (!Tile::highlightShown) { + drawRect(0, 0, width(), height(), BLACK); + Tile::startHighlightTimeout(); + Tile::highlightShown = true; + } + + // Clear the highlight + else { + Tile::cancelHighlightTimeout(); + Tile::highlightShown = false; + Tile::highlightTarget = nullptr; + } + } +} + +// Does the applet want to render now? +// Checks whether the applet called requestUpdate() recently, in response to an event +bool InkHUD::Applet::wantsToRender() +{ + return wantRender; +} + +// Does the applet want to be moved to foreground before next render, to show new data? +// User specifies whether an applet has permission for this, using the on-screen menu +bool InkHUD::Applet::wantsToAutoshow() +{ + return wantAutoshow; +} + +// Which technique would this applet prefer that the display use to change the image? +Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() +{ + return wantUpdateType; +} + +// Get size of the applet's drawing space from its tile +void InkHUD::Applet::updateDimensions() +{ + assert(assignedTile); + WIDTH = assignedTile->getWidth(); + HEIGHT = assignedTile->getHeight(); + _width = WIDTH; + _height = HEIGHT; +} + +// Ensure that render() always starts with the same initial drawing config +void InkHUD::Applet::resetDrawingSpace() +{ + resetCrop(); // Allow pixel from any region of the applet to draw + setTextColor(BLACK); // Reset text params + setCursor(0, 0); + setTextWrap(false); + setFont(AppletFont()); // Restore the default AdafruitGFX font +} + +// Tell the window manager that we want to render now +// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc +// When an applet decides it has heard something important, and wants to redraw, it calls this method +// Once the window manager has given other applets a chance to process whatever event we just detected, +// it will run Applet::render(), which may draw our applet to screen, if it is shown (forgeround) +void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) +{ + wantRender = true; + wantUpdateType = type; + WindowManager::getInstance()->requestUpdate(); +} + +// Ask window manager to move this applet to foreground at start of next render +// Users select which applets have permission for this using the on-screen menu +void InkHUD::Applet::requestAutoshow() +{ + wantAutoshow = true; +} + +// Called when an Applet begins running +// Active applets are considered "enabled" +// They should now listen for events, and request their own updates +// They may also be force rendered by the window manager at any time +// Applets can be activated at run-time through the on-screen menu +void InkHUD::Applet::activate() +{ + onActivate(); // Call derived class' handler + active = true; +} + +// Called when an Applet stop running +// Inactive applets are considered "disabled" +// They should not listen for events, process data +// They will not be rendered +// Applets can be deactivated at run-time through the on-screen menu +void InkHUD::Applet::deactivate() +{ + // If applet is still in foreground, run its onBackground code first + if (isForeground()) + sendToBackground(); + + // If applet is active, run its onDeactivate code first + if (isActive()) + onDeactivate(); // Derived class' handler + active = false; +} + +// Is the Applet running? +// Note: active / inactive is not related to background / foreground +// An inactive applet is *fully* disabled +bool InkHUD::Applet::isActive() +{ + return active; +} + +// Begin showing the Applet +// It will be rendered immediately to whichever tile it is assigned +// The window manager will also now honor requestUpdate() calls from this applet +void InkHUD::Applet::bringToForeground() +{ + if (!foreground) { + foreground = true; + onForeground(); // Run derived applet class' handler + } + + requestUpdate(); +} + +// Stop showing the Applet +// Calls to requestUpdate() will no longer be honored +// When one applet moves to background, another should move to foreground +void InkHUD::Applet::sendToBackground() +{ + if (foreground) { + foreground = false; + onBackground(); // Run derived applet class' handler + } +} + +// Is the applet currently displayed on a tile +bool InkHUD::Applet::isForeground() +{ + return foreground; +} + +// Limit drawing to a certain region of the applet +// Pixels outside this region will be discarded +void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) +{ + cropLeft = left; + cropTop = top; + cropWidth = width; + cropHeight = height; +} + +// Allow drawing to any region of the Applet +// Reverses Applet::setCrop +void InkHUD::Applet::resetCrop() +{ + setCrop(0, 0, width(), height()); +} + +// Convert relative width to absolute width, in px +// X(0) is 0 +// X(0.5) is width() / 2 +// X(1) is width() +uint16_t InkHUD::Applet::X(float f) +{ + return width() * f; +} + +// Convert relative hight to absolute height, in px +// Y(0) is 0 +// Y(0.5) is height() / 2 +// Y(1) is height() +uint16_t InkHUD::Applet::Y(float f) +{ + return height() * 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 glpyh 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); + + int16_t cursorX = 0; + int16_t cursorY = 0; + + switch (ha) { + case LEFT: + cursorX = x - textOffsetX; + break; + case CENTER: + cursorX = (x - textOffsetX) - (textWidth / 2); + break; + case RIGHT: + cursorX = (x - textOffsetX) - textWidth; + break; + } + + // We're using a fixed line height (getFontDimensions), rather than sizing to text (getTextBounds) + // Note: the FontDimensions values for this are unsigned + + switch (va) { + case TOP: + cursorY = y + currentFont.heightAboveCursor(); + break; + case MIDDLE: + cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); + break; + case BOTTOM: + cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); + break; + } + + setCursor(cursorX, cursorY); + print(text.c_str()); +} + +// Set which font should be used for subsequent drawing +// This is AppletFont type, which is a wrapper for AdfruitGFX font, with some precalculated dimension data +void InkHUD::Applet::setFont(AppletFont f) +{ + GFX::setFont(f.gfxFont); + currentFont = f; +} + +// Get which font is currently being used for drawing +// This is AppletFont type, which is a wrapper for AdfruitGFX font, with some precalculated dimension data +InkHUD::AppletFont InkHUD::Applet::getFont() +{ + return currentFont; +} + +// Set two general-purpose fonts, which are reused by many applets +// Applets are also permitted to use other fonts, if they can justify the flash usage +void InkHUD::Applet::setDefaultFonts(AppletFont large, AppletFont small) +{ + Applet::fontSmall = small; + Applet::fontLarge = large; +} + +// 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; + getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); + + return textWidth; +} + +// Gets rendered width of a string +// Wrappe for getTextBounds +uint16_t InkHUD::Applet::getTextWidth(std::string text) +{ + getFont().applySubstitutions(&text); + + return getTextWidth(text.c_str()); +} + +// Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels +// Roughly comparable to values used by the iOS app; +// I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator +InkHUD::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) +{ + uint8_t score = 0; + + // Give a score for the SNR + if (snr > -17.5) + score += 2; + else if (snr > -26.0) + score += 1; + + // Give a score for the RSSI + if (rssi > -115.0) + score += 3; + else if (rssi > -120.0) + score += 2; + else if (rssi > -126.0) + score += 1; + + // Combine scores, then give a result + if (score >= 5) + return SIGNAL_GOOD; + else if (score >= 4) + return SIGNAL_FAIR; + else if (score > 0) + return SIGNAL_BAD; + else + return SIGNAL_NONE; +} + +// Apply the standard "node id" formatting to a nodenum int: !0123abdc +std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) +{ + // Not found in nodeDB, show a hex nodeid instead + char nodeIdHex[10]; + sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format + return std::string(nodeIdHex); +} + +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 glpyh from custom font (or suitable ASCII addSubstitution?) + getFont().applySubstitutions(&text); + + // Place the AdafruitGFX cursor to suit our "top" coord + setCursor(left, top + getFont().heightAboveCursor()); + + // How wide a space character is + // Used when simulating print, for dimensioning + // Works around issues where getTextDimensions() doesn't account for whitespace + const uint8_t wSp = getFont().widthBetweenWords(); + + // Move through our text, character by character + uint16_t wordStart = 0; + for (uint16_t i = 0; i < text.length(); i++) { + + // Found: end of word (split by spaces or newline) + // Also handles end of string + if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { + // Isolate this word + uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 + std::string word = text.substr(wordStart, wordLength); + wordStart = i + 1; // Next word starts *after* the space + + // If word is terminated by a newline char, don't actually print it. + // We'll manually add a new line later + if (word.back() == '\n') + word.pop_back(); + + // Measure the word, in px + int16_t l, t; + uint16_t w, h; + getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); + + // Word is short + if (w < width) { + // Word fits on current line + if ((l + w + wSp) < left + width) + print(word.c_str()); + + // Word doesn't fit on current line + else { + setCursor(left, getCursorY() + getFont().lineHeight()); // Newline + print(word.c_str()); + } + } + + // Word is really long + // (wider than applet) + else { + // Horribly inefficient: + // Rather than working directly with the glyph sizes, + // we're going to run everything through getTextBounds as a c-string of length 1 + // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, + // which would be a pain to add manually here. + // These super-long strings probably don't come up often so we can maybe tolerate this. + + // Todo: rewrite making use of AdafruitGFX native text wrapping + char cstr[] = {0, 0}; + int16_t l, t; + uint16_t w, h; + for (uint16_t c = 0; c < word.length(); c++) { + // Shove next char into a c string + cstr[0] = word[c]; + getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); + + // Manual newline, if next character will spill beyond screen edge + if ((l + w) > left + width) + setCursor(left, getCursorY() + getFont().lineHeight()); + + // Print next character + print(word[c]); + } + } + } + + // If word was terminated by a newline char, manually add the new line now + if (text[i] == '\n') { + setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline + wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line + } + } +} + +// Simulate running printWrapped, to determine how tall the block of text will be. +// This is a wasteful way of handling things. Maybe some way to optimize in future? +uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) +{ + // Cache the current crop region + int16_t cL = cropLeft; + int16_t cT = cropTop; + uint16_t cW = cropWidth; + uint16_t cH = cropHeight; + + setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels + printWrapped(left, 0, width, text); // Simulate only - no pixels drawn + + // Restore previous crop region + cropLeft = cL; + cropTop = cT; + cropWidth = cW; + cropHeight = cH; + + // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, + // so we need to account for that when determining the height + return (getCursorY() + getFont().heightBelowCursor()); +} + +// Fill a region with sparse diagonal lines, to create a pseudo-translucent fill +void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) +{ + // Cache the currently cropped region + int16_t oldCropL = cropLeft; + int16_t oldCropT = cropTop; + uint16_t oldCropW = cropWidth; + uint16_t oldCropH = cropHeight; + + setCrop(x, y, w, h); + + // Draw lines starting along the top edge, every few px + for (int16_t ix = x; ix < x + w; ix += spacing) { + for (int16_t i = 0; i < w || i < h; i++) { + drawPixel(ix + i, y + i, color); + } + } + + // Draw lines starting along the left edge, every few px + for (int16_t iy = y; iy < y + h; iy += spacing) { + for (int16_t i = 0; i < w || i < h; i++) { + drawPixel(x + i, iy + i, color); + } + } + + // Restore any previous crop + // If none was set, this will clear + cropLeft = oldCropL; + cropTop = oldCropT; + cropWidth = oldCropW; + cropHeight = oldCropH; +} + +// Get a human readable time representation of an epoch time (seconds since 1970) +// If time is invalid, this will be an empty string +std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) +{ +#ifdef BUILD_EPOCH + constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build +#else + constexpr uint32_t validAfterEpoch = 1727740800 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to October 1, 2024 12:00:00 AM GMT +#endif + + uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); + + int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; + int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; + + // Times are invalid: rtc is much older than when code was built + // Don't give any human readable string + if (epochNow <= validAfterEpoch) { + LOG_DEBUG("RTC prior to buildtime"); + return ""; + } + + // Times are invalid: argument time is significantly ahead of RTC + // Don't give any human readable string + if (daysAgo < -2) { + LOG_DEBUG("RTC in future"); + return ""; + } + + // Times are probably invalid: more than 6 months ago + if (daysAgo > 6 * 30) { + LOG_DEBUG("RTC val > 6 months old"); + return ""; + } + + if (daysAgo > 1) + return to_string(daysAgo) + " days ago"; + + else if (hoursAgo > 18) + return "Yesterday"; + + else { + + uint32_t hms = epochSeconds % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m + uint32_t hour = hms / SEC_PER_HOUR; + uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Format the clock string + char clockStr[11]; + sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); + + return clockStr; + } +} + +// If no argument specified, get time string for the current RTC time +std::string InkHUD::Applet::getTimeString() +{ + return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); +} + +// Calculate how many nodes have been seen within our preferred window of activity +// This period is set by user, via the menu +// Todo: optimize to calculate once only per WindowManager::render +uint16_t InkHUD::Applet::getActiveNodeCount() +{ + // Don't even try to count nodes if RTC isn't set + // The last heard values in nodedb will be incomprehensible + if (getRTCQuality() == RTCQualityNone) + return 0; + + uint16_t count = 0; + + // For each node in db + for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Check if heard recently, and not our own node + if (sinceLastSeen(node) < settings.recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) + count++; + } + + return count; +} + +// Get an abbreviated, human readable, distance string +// Honors config.display.units, to offer both metric and imperial +std::string InkHUD::Applet::localizeDistance(uint32_t meters) +{ + constexpr float FEET_PER_METER = 3.28084; + constexpr uint16_t FEET_PER_MILE = 5280; + + // Resulting string + std::string localized; + + // Imeperial + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + uint32_t feet = meters * FEET_PER_METER; + // Distant (miles, rounded) + if (feet > FEET_PER_MILE / 2) { + localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); + localized += "mi"; + } + // Nearby (feet) + else { + localized += to_string(feet); + localized += "ft"; + } + } + + // Metric + else { + // Distant (kilometers, rounded) + if (meters >= 500) { + localized += to_string((uint32_t)roundf(meters / 1000.0)); + localized += "km"; + } + // Nearby (meters) + else { + localized += to_string(meters); + localized += "m"; + } + } + + return localized; +} + +void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) +{ + // How many times to draw along x axis + int16_t xStart; + int16_t xEnd; + switch (thicknessX) { + case 0: + assert(false); + case 1: + xStart = xCenter; + xEnd = xCenter; + break; + case 2: + xStart = xCenter; + xEnd = xCenter + 1; + break; + default: + xStart = xCenter - (thicknessX / 2); + xEnd = xCenter + (thicknessX / 2); + } + + // How many times to draw along Y axis + int16_t yStart; + int16_t yEnd; + switch (thicknessY) { + case 0: + assert(false); + case 1: + yStart = yCenter; + yEnd = yCenter; + break; + case 2: + yStart = yCenter; + yEnd = yCenter + 1; + break; + default: + yStart = yCenter - (thicknessY / 2); + yEnd = yCenter + (thicknessY / 2); + } + + // Print multiple times, overlapping + for (int16_t x = xStart; x <= xEnd; x++) { + for (int16_t y = yStart; y <= yEnd; y++) { + printAt(x, y, text, CENTER, MIDDLE); + } + } +} + +// Allow this applet to suppress notifications +// Asked before a notification is shown via the NotificationApplet +// An applet might want to suppress a notification if the applet itself already displays this info +// Example: AllMessageApplet should not approve notifications for messages, if it is in foreground +bool InkHUD::Applet::approveNotification(InkHUD::Notification &n) +{ + // By default, no objection + return true; +} + +// Draw the standard header, used by most Applets +void InkHUD::Applet::drawHeader(std::string text) +{ + setFont(fontSmall); + + // Y position for divider + // - between header text and messages + constexpr int16_t padDivH = 2; + const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; + + // Print header + printAt(0, padDivH, text); + + // Divider + // - below header text: separates message + // - above header text: separates other applets + for (int16_t x = 0; x < width(); x += 2) { + drawPixel(x, 0, BLACK); + drawPixel(x, headerDivY, BLACK); // Dotted 50% + } +} + +// Get the height of the standard applet header +// This will vary, depending on font +// Applets use this value to avoid drawing overtop the header +uint16_t InkHUD::Applet::getHeaderHeight() +{ + // Y position for divider + // - between header text and messages + constexpr int16_t padDivH = 2; + const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; + + return headerDivY + 1; // "Plus one": height is always one more than Y position +} + +// "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio +uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) +{ + // Determine whether we're limited by width or height + // Makes sure we draw the logo as large as possible, within the specified region, + // while still maintaining correct aspect ratio + if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) + return limitHeight * LOGO_ASPECT_RATIO; + else + return limitWidth; +} + +// "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio +uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) +{ + // Determine whether we're limited by width or height + // Makes sure we draw the logo as large as possible, within the specified region, + // while still maintaining correct aspect ratio + if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) + return limitWidth / LOGO_ASPECT_RATIO; + else + return limitHeight; +} + +// Draw a scalable Meshtastic logo +// Make sure to provide dimensions which have the correct aspect ratio (~2) +// Three paths, drawn thick using quads, with one corner "radiused" +void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height) +{ + struct Point { + int x; + int y; + }; + typedef Point Distance; + + int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. + int16_t logoL = centerX - (width / 2) + (logoTh / 2); + int16_t logoT = centerY - (height / 2) + (logoTh / 2); + int16_t logoW = width - logoTh; + int16_t logoH = height - logoTh; + int16_t logoR = logoL + logoW - 1; + int16_t logoB = logoT + logoH - 1; + + // Points for paths (a, b, and c) + Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; + Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; + Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; + Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; + Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; + Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; + + // Find right-angle to the path + // Used to thicken the single pixel paths + Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; + float angle = tanh((float)deltaA.y / deltaA.x); + + // Distance {at right angle from the paths), which will give corners for our "quads" + // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner + Distance fromPath; + fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; + fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; + + // Make the path thick: path a becomes quad a + Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; + Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; + Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; + Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; + fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, BLACK); + fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, BLACK); + + // Make the path thick: path b becomes quad b + Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; + Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; + Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; + Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; + fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, BLACK); + fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, BLACK); + + // Make the path hick: path c becomes quad c + Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; + Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; + Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; + Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; + fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, BLACK); + fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, BLACK); + + // Radius the intersection of quad b and quad c + // Don't attempt if logo is tiny + if (logoTh > 3) { + // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding + // We get better results just rederiving it + int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); + fillCircle(b2.x, b2.y, capRad, BLACK); + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h new file mode 100644 index 000000000..30c1bdcdc --- /dev/null +++ b/src/graphics/niche/InkHUD/Applet.h @@ -0,0 +1,234 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Base class for InkHUD applets + Must be overriden + + An applet is one "program" which may show info on the display. + + =================================== + Preliminary notes, for the curious + =================================== + + (This info to be streamlined, and moved to a more official documentation) + + User Applets vs System Applets + ------------------------------- + + There are either "User Applets", or "System Applets". + This concept is only for our understanding; as far at the code is concerned, both are just "Applets" + + User applets are the "normal" applets. + User applets are applets like "AllMessageApplet", or "MapApplet". + User applets may be enabled / disabled by user, via the on-screen menu. + Incorporating new UserApplets is easy: just add them during setupNicheGraphics + If a UserApplet is not added during setupNicheGraphics, it will not be built. + The set of available UserApplets is allowed to vary from device to device. + + + Examples of system applets include "NotificationApplet" and "MenuApplet". + For their own reasons, system applets each require some amount of special handling. + + Drawing + -------- + + *All* drawing must be performed by an Applet. + Applets implement the onRender() method, where all drawing takes place. + Applets are told how wide and tall they are, and are expected to draw to suit this size. + When an applet draws, it uses co-ordinates in "Applet Space": between 0 and applet width/height. + + Event-driven rendering + ----------------------- + + Applets don't render unless something on the display needs to change. + An applet is expected to determine for itself when it has new info to display. + It should interact with the firmware via the MeshModule API, via Observables, etc. + Please don't directly add hooks throughout the existing firmware code. + + When an applet decides it would like to update the display, it should call requestUpdate() + The WindowManager will shortly call the onRender() method for all affected applets + + An Applet may be unexpectedly asked to render at any point in time. + + Applets should cache their data, but not their pixel output: they should re-render when onRender runs. + An Applet's dimensions are not know until onRender is called, so pre-rendering of UI elements is prohibited. + + Tiles + ----- + + Applets are assigned to "Tiles". + Assigning an applet to a tile creates a reciprocal link between the two. + When an applet renders, it passes pixels to its tile. + The tile translates these to the correct position, to be placed into the fullscreen framebuffer. + User applets don't get to choose their own tile; the multiplexing is handled by the WindowManager. + System applets might do strange things though. + + Foreground and Background + ------------------------- + + The user can cycle between applets by short-pressing the user button. + Any applets which are currently displayed on the display are "foreground". + When the user button is short pressed, and an applet is hidden, it becomes "background". + + Although the WindowManager will not render background applets, they should still collect data, + so they are ready to display when they are brought to foreground again. + Even if they are in background, Applets should still request updates when an event affects them, + as the user may have given them permission to "autoshow"; bringing themselves foreground automatically + + Applets can implement the onForeground and onBackground methods to handle this change in state. + They can also check their state by calling isForeground() at any time. + + Active and Inactive + ------------------- + + The user can select which applets are available, using the onscreen applet selection menu. + Applets which are enabled in this menu are "active"; otherwise they are "inactive". + + An inactive applet is expected not collect data; not to consume resources. + Applets are activated at boot, or when enabled via the menu. + They are deactivated at shutdown, or when disabled via the menu. + + Applets can implement the onActivation and onDeactivation methods to handle this change in state. + +*/ + +#pragma once + +#include "configuration.h" + +#include + +#include "./AppletFont.h" +#include "./Applets/System/Notification/Notification.h" +#include "./Tile.h" +#include "./Types.h" +#include "./WindowManager.h" +#include "graphics/niche/Drivers/EInk/EInk.h" + +namespace NicheGraphics::InkHUD +{ + +using NicheGraphics::Drivers::EInk; +using std::to_string; + +class Tile; +class WindowManager; + +class Applet : public GFX +{ + public: + Applet(); + + void setTile(Tile *t); // Applets draw via a tile (for multiplexing) + Tile *getTile(); + + void render(); + bool wantsToRender(); // Check whether applet wants to render + bool wantsToAutoshow(); // Check whether applets wants to become foreground, to show new data, if permitted + Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer + void updateDimensions(); // Get current size from tile + void resetDrawingSpace(); // Makes sure every render starts with same parameters + + // Change the applet's state + + void activate(); + void deactivate(); + void bringToForeground(); + void sendToBackground(); + + // Info about applet's state + + bool isActive(); + bool isForeground(); + + // Allow derived applets to handle changes in state + + virtual void onRender() = 0; // All drawing happens here + virtual void onActivate() {} + virtual void onDeactivate() {} + virtual void onForeground() {} + virtual void onBackground() {} + virtual void onShutdown() {} + virtual void onButtonShortPress() {} // For use by System Applets only + virtual void onButtonLongPress() {} // For use by System Applets only + virtual void onLockAvailable() {} // For use by System Applets only + + virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification + + static void setDefaultFonts(AppletFont large, AppletFont small); // Set the general purpose fonts + static uint16_t getHeaderHeight(); // How tall is the "standard" applet header + + const char *name = nullptr; // Shown in applet selection menu + + protected: + // Place a single pixel. All drawing methods output through here + void drawPixel(int16_t x, int16_t y, uint16_t color) override; + + // Tell WindowManager to update display + void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); + + // Ask for applet to be moved to foreground + void requestAutoshow(); + + uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 + uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 + void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region + void resetCrop(); // Removes setCrop() + + void setFont(AppletFont f); + AppletFont getFont(); + + uint16_t getTextWidth(std::string text); + uint16_t getTextWidth(const char *text); + + void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); + + // Print text, with per-word line wrapping + void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); + uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); + + void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines + void drawHeader(std::string text); // Draw the standard applet header + + static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo + uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region + uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region + void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height); // Draw the meshtastic logo + + std::string hexifyNodeNum(NodeNum num); + 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 + + static AppletFont fontSmall, fontLarge; // General purpose fonts, used cross-applet + + private: + Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM + bool active = false; // Has the user enabled this applet (at run-time)? + bool foreground = false; // Is the applet currently drawn on a tile? + + bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing. + bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? + NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = + NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display + + using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly + using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. + + AppletFont currentFont; // As passed to setFont + + // As set by setCrop + int16_t cropLeft; + int16_t cropTop; + uint16_t cropWidth; + uint16_t cropHeight; +}; + +}; // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp new file mode 100644 index 000000000..bee9d33e6 --- /dev/null +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -0,0 +1,208 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./AppletFont.h" + +using namespace NicheGraphics; + +InkHUD::AppletFont::AppletFont() +{ + // Default constructor uses the in-built AdafruitGFX font +} + +InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafruitGFXFont) +{ + // AdafruitGFX fonts are drawn relative to a "cursor line"; + // they print as if the glyphs resting on the line of piece of ruled paper. + // The glyphs also each have a different height. + + // To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text + // We also need to know where that "cursor line" sits inside this "line height"; + // we need this additional info in order to align text by top-left, bottom-right, etc + + // 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. + + // 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 + this->height = max(this->height, glyphHeight); // Store if it's a new max + + // Calculate how far the glyph rises the cursor line + // Store if new max value + // Caution: signed and unsigned types + int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; + if (glyphAscender > 0) + this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); + } + + // Determine how far characters may hang "below the line" + descenderHeight = height - ascenderHeight; + + // Find how far the cursor advances when we "print" a space character + spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; +} + +uint8_t InkHUD::AppletFont::lineHeight() +{ + return this->height; +} + +// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). +// This value is the height of the font, above that imaginary line. +// Used to calculate the true height of the font +uint8_t InkHUD::AppletFont::heightAboveCursor() +{ + return this->ascenderHeight; +} + +// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). +// This value is the height of the font, below that imaginary line. +// Used to calculate the true height of the font +uint8_t InkHUD::AppletFont::heightBelowCursor() +{ + return this->descenderHeight; +} + +// Width of the space character +// Used with Applet::printWrapped +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) +{ + substitutions.push_back({.from = from, .to = to}); +} + +// Run all registered subtitutions on a string +// Used to swap out UTF8 special chars +void InkHUD::AppletFont::applySubstitutions(std::string *text) +{ + // For each substitution + for (Substitution s : substitutions) { + + // Find and replace + // - search for Substitution::from + // - replace with Subsitution::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 + } + } +} + +// Apply a set of substitutions which remap UTF8 for a Windows-1251 font +// Windows-1251 is an 8-bit character encoding, designed to cover languages that 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"); + + addSubstitution("ђ", "\x90"); + addSubstitution("љ", "\x9A"); + addSubstitution("њ", "\x9C"); + addSubstitution("ќ", "\x9D"); + addSubstitution("ћ", "\x9E"); + addSubstitution("џ", "\x9F"); + + addSubstitution("Ў", "\xA1"); + addSubstitution("ў", "\xA2"); + addSubstitution("Ј", "\xA3"); + addSubstitution("Ґ", "\xA5"); + addSubstitution("Ё", "\xA8"); + addSubstitution("Є", "\xAA"); + addSubstitution("Ї", "\xAF"); + + addSubstitution("І", "\xB2"); + addSubstitution("і", "\xB3"); + addSubstitution("ґ", "\xB4"); + addSubstitution("ё", "\xB8"); + addSubstitution("№", "\xB9"); + addSubstitution("є", "\xBA"); + addSubstitution("ј", "\xBC"); + addSubstitution("Ѕ", "\xBD"); + addSubstitution("ѕ", "\xBE"); + addSubstitution("ї", "\xBF"); + + 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"); + + 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"); + + 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"); + + 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"); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h new file mode 100644 index 000000000..89f901c94 --- /dev/null +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -0,0 +1,59 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + 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) + +*/ + +#pragma once + +#include "configuration.h" + +#include + +namespace NicheGraphics::InkHUD +{ + +// An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD +class AppletFont +{ + public: + AppletFont(); + AppletFont(const GFXfont &adafruitGFXFont); + uint8_t lineHeight(); + uint8_t heightAboveCursor(); + uint8_t heightBelowCursor(); + uint8_t widthBetweenWords(); + + 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 + + const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font + + private: + 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; + }; + + // List of all character substitutions to run, prior to printing a string + std::vector substitutions; +}; + +} // namespace NicheGraphics::InkHUD + +#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 new file mode 100644 index 000000000..21f404349 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -0,0 +1,429 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./MapApplet.h" + +using namespace NicheGraphics; + +void InkHUD::MapApplet::onRender() +{ + setFont(fontSmall); + + // Abort if no markers to render + if (!enoughMarkers()) { + printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE); + printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); + return; + } + + // Find center of map + // - latitude and longitude + // - will be placed at X(0.5), Y(0.5) + getMapCenter(&latCenter, &lngCenter); + + // Calculate North+East distance of each node to map center + // - which nodes to use controlled by virtual shouldDrawNode method + calculateAllMarkers(); + + // Set the region shown on the map + // - default: fit all nodes, plus padding + // - maybe overriden by derived applet + getMapSize(&widthMeters, &heightMeters); + + // Set the metersToPx conversion value + calculateMapScale(); + + // Special marker for own node + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && nodeDB->hasValidPosition(ourNode)) + drawLabeledMarker(ourNode); + + // Draw all markers + for (Marker m : markers) { + int16_t x = X(0.5) + (m.eastMeters * metersToPx); + int16_t y = Y(0.5) - (m.northMeters * metersToPx); + + // Cross Size + constexpr uint16_t csMin = 5; + constexpr uint16_t csMax = 12; + + // Too many hops away + if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops + printAt(x, y, "!", CENTER, MIDDLE); + else if (!m.hasHopsAway) // Unknown hops + drawCross(x, y, csMin); + else // The fewer hops, the larger the cross + drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin)); + } +} + +// Find the center point, in the middle of all node positions +// Calculated values are written to the *lat and *long pointer args +// - Finds the "mean lat long" +// - Calculates furthest nodes from "mean lat long" +// - Place map center directly between these furthest nodes + +void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) +{ + // Find mean lat long coords + // ============================ + // - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet + // - averages the x, y and z coords + // - uses tan to find angles for lat / long degrees + // - longitude: triangle formed by x and y (on plane of the equator) + // - latitude: triangle formed by z (north south), + // and the line along plane of equator which stetches from earth's axis to where point xyz intersects planet's surface + + // Working totals, averaged after nodeDB processed + uint32_t positionCount = 0; + float xAvg = 0; + float yAvg = 0; + float zAvg = 0; + + // For each node in db + for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; + + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; + + // Latitude and Longitude of node, in radians + float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; + float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; + + // Convert to cartesian points, with center of earth at 0, 0, 0 + // Exact distance from center is irrelevant, as we're only interested in the vector + float x = cos(latRad) * cos(lngRad); + float y = cos(latRad) * sin(lngRad); + float z = sin(latRad); + + // To find mean values shortly + xAvg += x; + yAvg += y; + zAvg += z; + positionCount++; + } + + // All NodeDB processed, find mean values + xAvg /= positionCount; + yAvg /= positionCount; + zAvg /= positionCount; + + // Longitude from cartesian coords + // (Angle from 3D coords describing a point of globe's surface) + /* + UK + /-------\ + (Top View) /- -\ + /- (You) -\ + /- . -\ + /- . X -\ + Asia - ... - USA + \- Y -/ + \- -/ + \- -/ + \- -/ + \- -----/ + Pacific + + */ + + *lng = atan2(yAvg, xAvg) * RAD_TO_DEG; + + // Latitude from cartesian cooods + // (Angle from 3D coords describing a point on the globe's surface) + // As latitude increases, distance from the Earth's north-south axis out to our surface point decreases. + // Means we need to first find the hypotenuse which becomes base of our triangle in the second step + /* + UK North + /-------\ (Front View) /-------\ + (Top View) /- -\ /- -\ + /- (You) -\ /-(You) -\ + /- /. -\ /- . -\ + /- √X²+Y²/ . X -\ /- Z . -\ + Asia - /... - USA - ..... - + \- Y -/ \- √X²+Y² -/ + \- -/ \- -/ + \- -/ \- -/ + \- -/ \- -/ + \- -----/ \- -----/ + Pacific South + */ + + float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect + *lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG; + + // ---------------------------------------------- + // This has given us the "mean position" + // This will be a position *somewhere* near the center of our nodes. + // What we actually want is to place our center so that our outermost nodes end up on the border of our map. + // The only real use of our "mean position" is to give us a reference frame: + // which direction is east, and which is west. + //------------------------------------------------ + + // Find furthest nodes from "mean lat long" + // ======================================== + + float northernmost = latCenter; + float southernmost = latCenter; + float easternmost = lngCenter; + float westernmost = lngCenter; + + for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; + + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; + + // Check for a new top or bottom latitude + float lat = node->position.latitude_i * 1e-7; + northernmost = max(northernmost, lat); + southernmost = min(southernmost, lat); + + // Longitude is trickier + float lng = node->position.longitude_i * 1e-7; + float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees travelled east from lngCenter to reach node + float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees travelled west from lngCenter to reach node + if (degEastward < degWestward) + easternmost = max(easternmost, lngCenter + degEastward); + else + westernmost = min(westernmost, lngCenter - degWestward); + } + + // Todo: check for issues with map spans >180 deg. MQTT only.. + latCenter = (northernmost + southernmost) / 2; + lngCenter = (westernmost + easternmost) / 2; + + // In case our new center is west of -180, or east of +180, for some reason + lngCenter = fmod(lngCenter, 180); +} + +// Size of map in meters +// Grown to fit the nodes furthest from map center +// Overridable if derived applet wants a custom map size (fixed size?) +void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) +{ + // Reset the value + *widthMeters = 0; + *heightMeters = 0; + + // Find the greatest distance horizontally and vertically from map center + for (Marker m : markers) { + *widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2); + *heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2); + } + + // Add padding + *widthMeters *= 1.1; + *heightMeters *= 1.1; +} + +// Convert and store info we need for drawing a marker +// Lat / long to "meters relative to map center", for position on screen +// Info about hopsAway, for marker size +InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) +{ + assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling. + + // Bearing and distance from map center to node + float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng); + float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians + + // Split into meters north and meters east components (signed) + // - signedness of cos / sin automatically sets negative if south or west + float northMeters = cos(bearingFromCenter) * distanceFromCenter; + float eastMeters = sin(bearingFromCenter) * distanceFromCenter; + + // Store this as a new marker + Marker m; + m.eastMeters = eastMeters; + m.northMeters = northMeters; + m.hasHopsAway = hasHopsAway; + m.hopsAway = hopsAway; + return m; +} + +// Draw a marker on the map for a node, with a shortname label, and backing box +void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) +{ + // Find x and y position based on node's position in nodeDB + assert(nodeDB->hasValidPosition(node)); + Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + node->position.longitude_i * 1e-7, // Long, convered from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + ); + + // Convert to pixel coords + int16_t markerX = X(0.5) + (m.eastMeters * metersToPx); + int16_t markerY = Y(0.5) - (m.northMeters * metersToPx); + + constexpr uint16_t paddingH = 2; + constexpr uint16_t paddingW = 4; + uint16_t paddingInnerW = 2; // Zero'd out if no text + constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) + constexpr uint16_t markerSizeMin = 5; + + int16_t textX; + int16_t textY; + uint16_t textW; + uint16_t textH; + int16_t labelX; + int16_t labelY; + uint16_t labelW; + uint16_t labelH; + uint8_t markerSize; + + bool tooManyHops = node->hops_away > config.lora.hop_limit; + bool isOurNode = node->num == nodeDB->getNodeNum(); + bool unknownHops = !node->has_hops_away && !isOurNode; + + // We will draw a left or right hand variant, to place text towards screen center + // Hopfully avoid text spilling off screen + // Most values are the same, regardless of left-right handedness + + // Pick emblem style + if (tooManyHops) + markerSize = getTextWidth("!"); + else if (unknownHops) + markerSize = markerSizeMin; + else + markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); + + // Common dimensions (left or right variant) + textW = getTextWidth(node->user.short_name); + if (textW == 0) + paddingInnerW = 0; // If no text, no padding for text + textH = fontSmall.lineHeight(); + labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH; + labelY = markerY - (labelH / 2); + textY = markerY; + labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant + + // Left-side variant + if (markerX < width() / 2) { + labelX = markerX - (markerSize / 2) - paddingW; + textX = labelX + paddingW + markerSize + paddingInnerW; + } + + // Right-side variant + else { + labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW; + textX = labelX + paddingW; + } + + // Backing box + fillRect(labelX, labelY, labelW, labelH, WHITE); + drawRect(labelX, labelY, labelW, labelH, BLACK); + + // Short name + printAt(textX, textY, node->user.short_name, LEFT, MIDDLE); + + // If the label is for our own node, + // fade it by overdrawing partially with white + if (node == nodeDB->getMeshNode(nodeDB->getNodeNum())) + hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE); + + // Draw the marker emblem + // - after the fading, because hatching (own node) can align with cross and make it look weird + if (tooManyHops) + printAt(markerX, markerY, "!", CENTER, MIDDLE); + else + drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops +} + +// Check if we actually have enough nodes which would be shown on the map +// Need at least two, to draw a sensible map +bool InkHUD::MapApplet::enoughMarkers() +{ + uint8_t count = 0; + for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Count nodes + if (nodeDB->hasValidPosition(node) && shouldDrawNode(node)) + count++; + + // We need to find two + if (count == 2) + return true; // Two nodes is enough for a sensible map + } + + return false; // No nodes would be drawn (or just the one, uselessly at 0,0) +} + +// Calculate how far north and east of map center each node is +// Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode +void InkHUD::MapApplet::calculateAllMarkers() +{ + // Clear old markers + markers.clear(); + + // For each node in db + for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; + + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; + + // Skip if our own node + // - special handling in render() + if (node->num == nodeDB->getNodeNum()) + continue; + + // Calculate marker and store it + markers.push_back( + calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + node->position.longitude_i * 1e-7, // Long, convered from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + )); + } +} + +// Determine the conversion factor between metres, and pixels on screen +// May be overriden by derived applet, if custom scale required (fixed map size?) +void InkHUD::MapApplet::calculateMapScale() +{ + // Aspect ratio of map and screen + // - larger = wide, smaller = tall + // - used to set scale, so that widest map dimension fits in applet + float mapAspectRatio = (float)widthMeters / heightMeters; + float appletAspectRatio = (float)width() / height(); + + // "Shrink to fit" + // Scale the map so that the largest dimension is fully displayed + // Because aspect ratio will be maintained, the other dimension will appear "padded" + if (mapAspectRatio > appletAspectRatio) + metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width. + else + metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height. +} + +// Draw an x, centered on a specific point +// Most markers will draw with this method +void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) +{ + int16_t x0 = x - (size / 2); + int16_t y0 = y - (size / 2); + int16_t x1 = x0 + size - 1; + int16_t y1 = y0 + size - 1; + drawLine(x0, y0, x1, y1, BLACK); + drawLine(x0, y1, x1, y0, BLACK); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h new file mode 100644 index 000000000..fd5245631 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h @@ -0,0 +1,66 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Base class for Applets which show nodes on a map + +Plots position of for a selection of nodes, with north facing up. +Size of cross represents hops away. +Our own node is identified with a faded label. + +The base applet doesn't handle any events; this is left to the derived applets. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +#include "MeshModule.h" +#include "gps/GeoCoord.h" + +namespace NicheGraphics::InkHUD +{ + +class MapApplet : public Applet +{ + public: + void onRender() override; + + protected: + virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes + virtual void getMapCenter(float *lat, float *lng); + virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters); + + bool enoughMarkers(); // Anything to draw? + void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker + + private: + // Position of markers to be drawn, relative to map center + // HopsAway info used to determine marker size + struct Marker { + float eastMeters = 0; // Meters east of mapCenter. Negative if west. + float northMeters = 0; // Meters north of mapCenter. Negative if south. + bool hasHopsAway = false; + uint8_t hopsAway = 0; + }; + + Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway); + void calculateAllMarkers(); + void calculateMapScale(); // Conversion factor for meters to pixels + void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers + + float metersToPx = 0; // Conversion factor for meters to pixels + float latCenter = 0; // Map center: latitude + float lngCenter = 0; // Map center: longitude + + std::list markers; + uint32_t widthMeters = 0; // Map width: meters + uint32_t heightMeters = 0; // Map height: meters +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp new file mode 100644 index 000000000..5d60e6800 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -0,0 +1,283 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "RTC.h" + +#include "GeoCoord.h" +#include "NodeDB.h" + +#include "./NodeListApplet.h" + +using namespace NicheGraphics; + +InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) +{ + // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule + // For all other packets, we manually reimplement isPromiscuous=false in wantPacket + MeshModule::isPromiscuous = true; +} + +// Do we want to process this packet with handleReceived()? +bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) +{ + // Only interested if: + return isActive() // Applet is active + && !isFromUs(p) // Packet is incoming (not outgoing) + && (isToUs(p) || isBroadcast(p->to) || // Either: intended for us, + p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo + + // Note: special handling of NodeInfo is to match NodeInfoModule + // To match the behavior seen in the client apps: + // - NodeInfoModule's ProtoBufModule base is "promiscuous" + // - All other activity is *not* promiscuous + // To achieve this, our MeshModule *is* promiscious, and we're manually reimplementing non-promiscuous behavior here, + // to match the code in MeshModule::callModules +} + +// MeshModule packets arrive here +// Extract the info and pass it to the derived applet +// Derived applet will store the CardInfo and perform any required sorting of the CardInfo collection +// Derived applet might also need to keep other tallies (active nodes count?) +ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // Abort if applet fully deactivated + // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Assemble info: from this event + CardInfo c; + c.nodeNum = mp.from; + c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); + + // Assemble info: from nodeDB (needed to detect changes) + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (node) { + if (node->has_hops_away) + c.hopsAway = node->hops_away; + + if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { + // Get lat and long as float + // Meshtastic stores these as integers internally + float ourLat = ourNode->position.latitude_i * 1e-7; + float ourLong = ourNode->position.longitude_i * 1e-7; + float theirLat = node->position.latitude_i * 1e-7; + float theirLong = node->position.longitude_i * 1e-7; + + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + } + } + + // Pass to the derived applet + // Derived applet is responsible for requesting update, if justified + // That request will eventually trigger our class' onRender method + handleParsed(c); + + return ProcessMessage::CONTINUE; // Let others look at this message also if they want +} + +// Maximum number of cards we may ever need to render, in our tallest layout config +// May be slightly in excess of the true value: header not accounted for +uint8_t InkHUD::NodeListApplet::maxCards() +{ + // Cache result. Shouldn't change during execution + static uint8_t cards = 0; + + if (!cards) { + const uint16_t height = Tile::maxDisplayDimension(); + + // Use a loop instead of arithmetic, because it's easier for my brain to follow + // Add cards one by one, until the latest card (without margin) extends below screen + + uint16_t y = cardH; // First card: no margin above + cards = 1; + + while (y < height) { + y += cardMarginH; + y += cardH; + cards++; + } + } + + return cards; +} + +// Draw using info which derived applet placed into NodeListApplet::cards for us +void InkHUD::NodeListApplet::onRender() +{ + + // ================================ + // Draw the standard applet header + // ================================ + + drawHeader(getHeaderText()); // Ask derived applet for the title + + // Dimensions of the header + int16_t headerDivY = getHeaderHeight() - 1; + constexpr uint16_t padDivH = 2; + + // ======================== + // Draw the main node list + // ======================== + + // const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards + // const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; + + // Imaginary vertical line dividing left-side and right-side info + // Long-name will crop here + const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); + + // Y value (top) of the current card. Increases as we draw. + uint16_t cardTopY = headerDivY + padDivH; + + // -- Each node in list -- + for (auto card = cards.begin(); card != cards.end(); ++card) { + + // Gather info + // ======================================== + NodeNum &nodeNum = card->nodeNum; + SignalStrength &signal = card->signal; + std::string longName; // handled below + std::string shortName; // handled below + std::string distance; // handled below; + uint8_t &hopsAway = card->hopsAway; + + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); + + // -- Shortname -- + // use "?" if unknown + if (node && node->has_user) + shortName = node->user.short_name; + else + shortName = "?"; + + // -- Longname -- + // use node id if unknown + if (node && node->has_user) + longName = node->user.long_name; // Found in nodeDB + else { + // Not found in nodeDB, show a hex nodeid instead + longName = hexifyNodeNum(nodeNum); + } + + // -- Distance -- + if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN) + distance = localizeDistance(card->distanceMeters); + + // Draw the info + // ==================================== + + // 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); + + // Print the short name + setFont(fontLarge); + printAt(0, lineAY, shortName, LEFT, MIDDLE); + + // Print the distance + setFont(fontSmall); + printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + + // 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); + int16_t signalX = width() - signalW; + drawSignalIndicator(signalX, signalY, signalW, signalH, signal); + } + // Otherwise, print "hops away" info, if available + else if (hopsAway != CardInfo::HOPS_UNKNOWN) { + std::string hopString = to_string(node->hops_away); + hopString += " Hop"; + if (node->hops_away != 1) + hopString += "s"; // Append s for "Hops", rather than "Hop" + + printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); + } + + // Print the long name, cropping to prevent overflow onto the right-side info + setCrop(0, 0, dividerX - 1, height()); + printAt(0, lineBY, longName, LEFT, MIDDLE); + + // GFX effect: "hatch" the right edge of longName area + // If a longName has been cropped, it will appear to fade out, + // creating a soft barrier with the right-side info + const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); + const int16_t hatchWidth = fontSmall.lineHeight(); + hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); + + // Prepare to draw the next card + resetCrop(); + cardTopY += cardH; + + // Once we've run out of screen, stop drawing cards + // Depending on tiles / rotation, this may be before we hit maxCards + if (cardTopY > height()) { + break; + } + } +} + +// Draw element: a "mobile phone" style signal indicator +// We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc +// This prevents issues with premature rounding when rendering tiny elements +void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) +{ + + /* + +-------------------------------------------+ + | | + | | + | barHeightRelative=1.0 + | +--+ ^ | + | gutterW +--+ | | | | + | <--> +--+ | | | | | | + | +--+ | | | | | | | | + | | | | | | | | | | | + | <-> +--+ +--+ +--+ +--+ v | + | paddingW ^ | + | paddingH | | + | v | + +-------------------------------------------+ + */ + + constexpr float paddingW = 0.1; // Either side + constexpr float paddingH = 0.1; // Above and below + constexpr float gutterX = 0.1; // Between bars + + constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the talleest + constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count. + + // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions + float barW = (1.0 - (paddingW + ((barCount - 1) * gutterX) + paddingW)) / barCount; + float barHMax = 1.0 - (paddingH + paddingH); + + // Draw signal bar rectangles, then placeholder lines once strength reached + for (uint8_t i = 0; i < barCount; i++) { + // Co-ords for this specific bar + float barH = barHMax * barHRel[i]; + float barX = paddingW + (i * (gutterX + barW)); + float barY = paddingH + (barHMax - barH); + + // Rasterize to px coords at the last moment + int16_t rX = (x + (w * barX)) + 0.5; + int16_t rY = (y + (h * barY)) + 0.5; + uint16_t rW = (w * barW) + 0.5; + uint16_t rH = (h * barH) + 0.5; + + // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines + if (i <= strength) + drawRect(rX, rY, rW, rH, BLACK); + else { + // Just draw a placeholder line + float lineY = barY + barH; + uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize + drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK); + } + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h new file mode 100644 index 000000000..670dd9e9a --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -0,0 +1,71 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Base class for Applets which display a list of nodes +Used by the "Recents" and "Heard" applets. Possibly more in future? + + +-------------------------------+ + | | | + | SHRT . | | | + | Long name 50km | + | | + | ABCD 2 Hops | + | abcdedfghijk 30km | + | | + +-------------------------------+ + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class NodeListApplet : public Applet, public MeshModule +{ + protected: + // Info used to draw one card to the node list + struct CardInfo { + static constexpr uint8_t HOPS_UNKNOWN = -1; + static constexpr uint32_t DISTANCE_UNKNOWN = -1; + + NodeNum nodeNum = 0; + SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN; + uint32_t distanceMeters = DISTANCE_UNKNOWN; + uint8_t hopsAway = HOPS_UNKNOWN; // Unknown + }; + + public: + NodeListApplet(const char *name); + void onRender() override; + + // MeshModule overrides + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + protected: + virtual void handleParsed(CardInfo c) = 0; // Pass extracted info from a new packet to derived class, for sorting and storage + virtual std::string getHeaderText() = 0; // Title for the applet's header. Todo: get this info another way? + + uint8_t maxCards(); // Calculate the maximum number of cards an applet could ever display + + std::deque cards; // Derived applet places cards here, for this base applet to render + + private: + // UI element: a "mobile phone" style signal indicator + void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength signal); + + // Dimensions for drawing + // Used for render, and also 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 +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp new file mode 100644 index 000000000..17458ab96 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp @@ -0,0 +1,14 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./BasicExampleApplet.h" + +using namespace NicheGraphics; + +// All drawing happens here +// Our basic example doesn't do anything useful. It just passively prints some text. +void InkHUD::BasicExampleApplet::onRender() +{ + print("Hello, World!"); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h new file mode 100644 index 000000000..aed63cdc8 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h @@ -0,0 +1,36 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +A bare-minimum example of an InkHUD applet. +Only prints Hello World. + +In variants//nicheGraphics.h: + + - include this .h file + - add the following line of code: + windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet); + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class BasicExampleApplet : public Applet +{ + public: + // You must have an onRender() method + // All drawing happens here + + void onRender() override; +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp new file mode 100644 index 000000000..e0b2a4238 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp @@ -0,0 +1,54 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./NewMsgExampleApplet.h" + +using namespace NicheGraphics; + +// We configured MeshModule API to call this method when we receive a new text message +ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + + // Abort if applet fully deactivated + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Check that this is an incoming message + // Outgoing messages (sent by us) will also call handleReceived + + if (!isFromUs(&mp)) { + // Store the sender's nodenum + // We need to keep this information, so we can re-use it anytime render() is called + haveMessage = true; + fromWho = mp.from; + + // Tell InkHUD that we have something new to show on the screen + requestUpdate(); + } + + // Tell MeshModule 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; +} + +// All drawing happens here +// We can trigger a render by calling requestUpdate() +// Render might be called by some external source +// We should always be ready to draw +void InkHUD::NewMsgExampleApplet::onRender() +{ + setFont(fontSmall); + + printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) + + int16_t centerX = X(0.5); // Same as width() / 2 + int16_t centerY = Y(0.5); // Same as height() / 2 + + if (haveMessage) { + printAt(centerX, centerY, "New Message", CENTER, BOTTOM); + printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP); + } else { + printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY) + } +} + +#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 new file mode 100644 index 000000000..edfb211d7 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -0,0 +1,61 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +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, +which is a general part of the Meshtastic firmware, and not part of InkHUD. + +In variants//nicheGraphics.h: + + - include this .h file + - add the following line of code: + windowManager->addApplet("New Msg", new InkHUD::NewMsgExampleApplet); + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +#include "mesh/SinglePortModule.h" + +namespace NicheGraphics::InkHUD +{ + +class NewMsgExampleApplet : public Applet, public SinglePortModule +{ + public: + // The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages. + NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} + + // All drawing happens here + void onRender() override; + + // Your applet might also want to use some of these + // Useful for setting up or tidying up + + /* + void onActivate(); // When started + void onDeactivate(); // When stopped + void onForeground(); // When shown by short-press + void onBackground(); // When hidden by short-press + */ + + private: + // Called when we receive new text messages + // Part of the MeshModule API + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + // Store info from handleReceived + bool haveMessage = false; + NodeNum fromWho; +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp new file mode 100644 index 000000000..e4432a7c2 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -0,0 +1,107 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./BatteryIconApplet.h" + +using namespace NicheGraphics; + +void InkHUD::BatteryIconApplet::onActivate() +{ + // Show at boot, if user has previously enabled the feature + if (settings.optionalFeatures.batteryIcon) + bringToForeground(); + + // Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available + // This happens whether or not the battery icon feature is enabled + powerStatusObserver.observe(&powerStatus->onNewStatus); +} + +void InkHUD::BatteryIconApplet::onDeactivate() +{ + // Stop having onPowerStatusUpdate called + powerStatusObserver.unobserve(&powerStatus->onNewStatus); +} + +// We handle power status' even when the feature is disabled, +// so that we have up to date data ready if the feature is enabled later. +// Otherwise could be 30s before new status update, with weird battery value displayed +int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) +{ + // System applets are always active + assert(isActive()); + + // This method should only receive power statuses + // If we get a different type of status, something has gone weird elsewhere + assert(status->getStatusType() == STATUS_TYPE_POWER); + + meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; + + // Get the new state of charge %, and round to the nearest 10% + uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; + + // If rounded value has changed, trigger a display update + // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() + // Don't trigger an update if the feature is disabled + if (this->socRounded != newSocRounded && settings.optionalFeatures.batteryIcon) + requestUpdate(); + + // Store the new value + this->socRounded = newSocRounded; + + return 0; // Tell Observable to continue informing other observers +} + +void InkHUD::BatteryIconApplet::onRender() +{ + // Fill entire tile + // - size of icon controlled by size of tile + int16_t l = 0; + int16_t t = 0; + uint16_t w = width(); + int16_t h = height(); + + // Clear the region beneath the tile + // Most applets are drawing onto an empty frame buffer and don't need to do this + // We do need to do this with the battery though, as it is an "overlay" + fillRect(l, t, w, h, WHITE); + + // Vertical centerline + const int16_t m = t + (h / 2); + + // ===================== + // Draw battery outline + // ===================== + + // Positive terminal "bump" + const int16_t &bumpL = l; + const uint16_t bumpH = h / 2; + const int16_t bumpT = m - (bumpH / 2); + constexpr uint16_t bumpW = 2; + fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); + + // Main body of battery + const int16_t bodyL = bumpL + bumpW; + const int16_t &bodyT = t; + const int16_t &bodyH = h; + const int16_t bodyW = w - bumpW; + drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); + + // Erase join between bump and body + drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE); + + // =================== + // Draw battery level + // =================== + + constexpr int16_t slicePad = 2; + const int16_t sliceL = bodyL + slicePad; + const int16_t sliceT = bodyT + slicePad; + const uint16_t sliceH = bodyH - (slicePad * 2); + uint16_t sliceW = bodyW - (slicePad * 2); + + sliceW = (sliceW * socRounded) / 100; // Apply percentage + + hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); + drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h new file mode 100644 index 000000000..765ca073f --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h @@ -0,0 +1,41 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +This applet floats top-left, giving a graphical representation of battery remaining +It should be optional, enabled by the on-screen menu + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +#include "PowerStatus.h" + +namespace NicheGraphics::InkHUD +{ + +class BatteryIconApplet : public Applet +{ + public: + void onRender() override; + + void onActivate() override; + void onDeactivate() override; + + int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available + + protected: + // Get informed when new information about the battery is available (via onPowerStatusUpdate method) + CallbackObserver powerStatusObserver = + CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); + + uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10% +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp new file mode 100644 index 000000000..cc24417ab --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -0,0 +1,108 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./LogoApplet.h" + +using namespace NicheGraphics; + +InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") +{ + // Don't autostart the runOnce() timer + OSThread::disable(); + + // Grab the WindowManager singleton, for convenience + windowManager = WindowManager::getInstance(); +} + +void InkHUD::LogoApplet::onRender() +{ + // Size of the region which the logo should "scale to fit" + uint16_t logoWLimit = X(0.8); + uint16_t logoHLimit = Y(0.5); + + // Get the max width and height we can manage within the region, while still maintaining aspect ratio + uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); + uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); + + // Where to place the center of the logo + int16_t logoCX = X(0.5); + int16_t logoCY = Y(0.5 - 0.05); + + drawLogo(logoCX, logoCY, logoW, logoH); + + if (!textLeft.empty()) { + setFont(fontSmall); + printAt(0, 0, textLeft, LEFT, TOP); + } + + if (!textRight.empty()) { + setFont(fontSmall); + printAt(X(1), 0, textRight, RIGHT, TOP); + } + + if (!textTitle.empty()) { + int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo + setFont(fontTitle); + printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP); + } +} + +void InkHUD::LogoApplet::onForeground() +{ + // If another applet has locked the display, ask it to exit + Applet *other = windowManager->whoLocked(); + if (other != nullptr) + other->sendToBackground(); + + windowManager->claimFullscreen(this); // Take ownership of fullscreen tile + windowManager->lock(this); // Prevent other applets from requesting updates +} + +void InkHUD::LogoApplet::onBackground() +{ + OSThread::disable(); // Disable auto-dismiss timer, in case applet was dismissed early (sendToBackground from outside class) + + windowManager->releaseFullscreen(); // Relinquish ownership of fullscreen tile + windowManager->unlock(this); // Allow normal user applet update requests to resume + + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + windowManager->forceUpdate(EInk::UpdateTypes::FULL); +} + +int32_t InkHUD::LogoApplet::runOnce() +{ + LOG_DEBUG("Sent to background by timer"); + sendToBackground(); + return OSThread::disable(); +} + +// Begin displaying the screen which is shown at startup +// Suggest EInk::await after calling this method +void InkHUD::LogoApplet::showBootScreen() +{ + OSThread::setIntervalFromNow(8 * 1000UL); + OSThread::enabled = true; + + textLeft = ""; + textRight = ""; + textTitle = xstr(APP_VERSION_SHORT); + fontTitle = fontSmall; + + bringToForeground(); + requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL +} + +// Begin displaying the screen which is shown at shutdown +// Needs EInk::await after calling this method, to ensure display updates before shutdown +void InkHUD::LogoApplet::showShutdownScreen() +{ + textLeft = ""; + textRight = ""; + textTitle = owner.short_name; + fontTitle = fontLarge; + + bringToForeground(); + requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h new file mode 100644 index 000000000..aa1bf8b2c --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -0,0 +1,47 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Shows the Meshtastic logo fullscreen, with accompanying text + Used for boot and shutdown + +*/ + +#pragma once + +#include "configuration.h" + +#include "concurrency/OSThread.h" +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class LogoApplet : public Applet, public concurrency::OSThread +{ + public: + LogoApplet(); + void onRender() override; + void onForeground() override; + void onBackground() override; + + // Note: interacting directly with an applet like this is non-standard + // Only permitted because this is a "system applet", which has special behavior and interacts directly with WindowManager + + void showBootScreen(); + void showShutdownScreen(); + + protected: + int32_t runOnce() override; + + std::string textLeft; + std::string textRight; + std::string textTitle; + AppletFont fontTitle; + + WindowManager *windowManager = nullptr; // For convenience +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h new file mode 100644 index 000000000..6950bb110 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -0,0 +1,38 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Set of end-point actions for the Menu Applet + +Added as menu entries in MenuApplet::showPage +Behaviors assigned in MenuApplet::execute + +*/ + +#pragma once + +#include "configuration.h" + +namespace NicheGraphics::InkHUD +{ + +enum MenuAction { + NO_ACTION, + SEND_NODEINFO, + SEND_POSITION, + SHUTDOWN, + NEXT_TILE, + TOGGLE_APPLET, + ACTIVATE_APPLETS, // Todo: remove? Possible redundant, handled by TOGGLE_APPLET? + TOGGLE_AUTOSHOW_APPLET, + SET_RECENTS, + ROTATE, + LAYOUT, + TOGGLE_BATTERY_ICON, + TOGGLE_NOTIFICATIONS, + TOGGLE_BACKLIGHT, +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp new file mode 100644 index 000000000..d24ae59a5 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -0,0 +1,612 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./MenuApplet.h" + +#include "PowerStatus.h" +#include "RTC.h" + +using namespace NicheGraphics; + +static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes + +// Options for the "Recents" menu +// These are offered to users as possible values for settings.recentlyActiveSeconds +static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; + +InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") +{ + // No timer tasks at boot + OSThread::disable(); +} + +void InkHUD::MenuApplet::onActivate() +{ + // Grab pointers to some singleton components which the menu interacts with + // We could do this every time we needed them, in place, + // but this just makes the code tidier + + this->windowManager = WindowManager::getInstance(); + + // Note: don't get instance if we're not actually using the backlight, + // or else you will unintentionally instantiate it + if (settings.optionalMenuItems.backlight) { + backlight = Drivers::LatchingBacklight::getInstance(); + } +} + +void InkHUD::MenuApplet::onForeground() +{ + // We do need this before we render, but we can optimize by just calculating it once now + systemInfoPanelHeight = getSystemInfoPanelHeight(); + + // Display initial menu page + showPage(MenuPage::ROOT); + + // If device has a backlight which isn't controlled by aux button: + // backlight on always when menu opens. + // Courtesy to T-Echo users who removed the capacitive touch button + if (settings.optionalMenuItems.backlight) { + assert(backlight); + if (!backlight->isOn()) + backlight->peek(); + } + + // Prevent user applets requested update while menu is open + windowManager->lock(this); + + // Begin the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; + + // Upgrade the refresh to FAST, for guaranteed responsiveness + windowManager->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onBackground() +{ + // 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 + if (settings.optionalMenuItems.backlight) { + assert(backlight); + if (!backlight->isLatched()) + backlight->off(); + } + + // Stop the auto-timeout + OSThread::disable(); + + // Resume normal rendering and button behavior of user applets + windowManager->unlock(this); + + // Restore the user applet whose tile we borrowed + if (borrowedTileOwner) + borrowedTileOwner->bringToForeground(); + Tile *t = getTile(); + t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one) + borrowedTileOwner = nullptr; + + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // We're only updating here to ugrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu + windowManager->forceUpdate(EInk::UpdateTypes::FAST); +} + +// Open the menu +// Parameter specifies which user-tile the menu will use +// The user applet originally on this tile will be restored when the menu closes +void InkHUD::MenuApplet::show(Tile *t) +{ + // Remember who *really* owns this tile + borrowedTileOwner = t->getAssignedApplet(); + + // Hide the owner, if it is a valid applet + if (borrowedTileOwner) + borrowedTileOwner->sendToBackground(); + + // Break the owner's link with tile + // Relink it to menu applet + t->assignApplet(this); + + // Show menu + bringToForeground(); +} + +// Auto-exit the menu applet after a period of inactivity +// The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open. +// By exiting the menu, we prevent users mistakenly believing that the data will update. +int32_t InkHUD::MenuApplet::runOnce() +{ + // runOnce's interval is pushed back when a button is pressed + // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC, + // so we close the menu. + showPage(EXIT); + + // Timer should disable after firing + // This is redundant, as onBackground() will also disable + return OSThread::disable(); +} + +// Perform action for a menu item, then change page +// Behaviors for MenuActions are defined here +void InkHUD::MenuApplet::execute(MenuItem item) +{ + // Perform an action + // ------------------ + switch (item.action) { + + // Open a submenu without performing any action + // Also handles exit + case NO_ACTION: + break; + + case NEXT_TILE: + // Note performed manually; + // WindowManager::nextTile is raised by aux button press only, and will interact poorly with the menu + settings.userTiles.focused = (settings.userTiles.focused + 1) % settings.userTiles.count; + windowManager->changeLayout(); + cursor = 0; // No menu item selected, for quick exit after tile swap + cursorShown = false; + break; + + case ROTATE: + settings.rotation = (settings.rotation + 1) % 4; + windowManager->changeLayout(); + // requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL + break; + + case LAYOUT: + // Todo: smarter incrementing of tile count + settings.userTiles.count++; + + if (settings.userTiles.count == 3) // Skip 3 tiles: not done yet + settings.userTiles.count++; + + if (settings.userTiles.count > settings.userTiles.maxCount) // Loop around if tile count now too high + settings.userTiles.count = 1; + + windowManager->changeLayout(); + // requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL + break; + + case TOGGLE_APPLET: + settings.userApplets.active[cursor] = !settings.userApplets.active[cursor]; + windowManager->changeActivatedApplets(); + // requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Select FULL, seeing how this action doesn't auto exit + break; + + case ACTIVATE_APPLETS: + // Todo: remove this action? Already handled by TOGGLE_APPLET? + windowManager->changeActivatedApplets(); + break; + + case TOGGLE_AUTOSHOW_APPLET: + // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() + *items.at(cursor).checkState = !(*items.at(cursor).checkState); + break; + + case TOGGLE_NOTIFICATIONS: + settings.optionalFeatures.notifications = !settings.optionalFeatures.notifications; + break; + + case SET_RECENTS: + // Set value of settings.recentlyActiveSeconds + // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) + assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); + settings.recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes + break; + + case SHUTDOWN: + LOG_INFO("Shutting down from menu"); + power->shutdown(); + // Menu is then sent to background via onShutdown + break; + + case TOGGLE_BATTERY_ICON: + windowManager->toggleBatteryIcon(); + break; + + case TOGGLE_BACKLIGHT: + // Note: backlight is already on in this situation + // We're marking that it should *remain* on once menu closes + assert(backlight); + if (backlight->isLatched()) + backlight->off(); + else + backlight->latch(); + break; + + default: + LOG_WARN("Action not implemented"); + } + + // Move to next page, as defined for the MenuItem + showPage(item.nextPage); +} + +// Display a new page of MenuItems +// May reload same page, or exit menu applet entirely +// Fills the MenuApplet::items vector +void InkHUD::MenuApplet::showPage(MenuPage page) +{ + items.clear(); + + switch (page) { + case ROOT: + // Optional: next applet + 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("Options", MenuPage::OPTIONS)); + // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO + items.push_back(MenuItem("Save & Shutdown", MenuAction::SHUTDOWN)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + 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)); + break; + + case OPTIONS: + // Optional: backlight + if (settings.optionalMenuItems.backlight) { + assert(backlight); + items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label + MenuAction::TOGGLE_BACKLIGHT, // Action + MenuPage::EXIT // Exit once complete + )); + } + + items.push_back(MenuItem("Applets", MenuPage::APPLETS)); + items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); + items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); + if (settings.userTiles.maxCount > 1) + items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); + items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); + items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, + &settings.optionalFeatures.notifications)); + items.push_back( + MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings.optionalFeatures.batteryIcon)); + + // TODO - GPS and Wifi switches + /* + // Optional: has GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) + items.push_back(MenuItem("Enable GPS", MenuPage::EXIT)); // TODO + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + items.push_back(MenuItem("Disable GPS", MenuPage::EXIT)); // TODO + + // Optional: using wifi + if (!config.bluetooth.enabled) + items.push_back(MenuItem("Enable Bluetooth", MenuPage::EXIT)); // TODO: escape hatch if wifi configured wrong + */ + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case APPLETS: + populateAppletPage(); + items.push_back(MenuItem("Exit", MenuAction::ACTIVATE_APPLETS)); + break; + + case AUTOSHOW: + populateAutoshowPage(); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case RECENTS: + populateRecentsPage(); + break; + + case EXIT: + sendToBackground(); // Menu applet dismissed, allow normal behavior to resume + // requestUpdate(Drivers::EInk::UpdateTypes::FULL); + break; + + default: + LOG_WARN("Page not implemented"); + } + + // Reset the cursor, unless reloading same page + // (or now out-of-bounds) + if (page != currentPage || cursor >= items.size()) { + cursor = 0; + + // ROOT menu has special handling: unselected at first, to emphasise the system info panel + if (page == ROOT) + cursorShown = false; + } + + // Remember which page we are on now + currentPage = page; +} + +void InkHUD::MenuApplet::onRender() +{ + if (items.size() == 0) + LOG_ERROR("Empty Menu"); + + // Testing only + setFont(fontSmall); + + // Dimensions for the slots where we will draw menuItems + const float padding = 0.05; + const uint16_t itemH = fontSmall.lineHeight() * 2; + const int16_t itemW = width() - X(padding) - X(padding); + const int16_t itemL = X(padding); + const int16_t itemR = X(1 - padding); + int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu. + + // How many full menuItems will fit on screen + uint8_t slotCount = (height() - itemT) / itemH; + + // System info panel at the top of the menu + // ========================================= + + uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground + const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel + + // System info - top + // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen. + // This is the same behavior we expect from the non-root menus. + // Implementing this with the systemp panel is slightly annoying though, + // and required adding the MenuApplet::getSystemInfoPanelHeight method + int16_t siT; + if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count) + siT = 0; + else + siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH); + + // If showing ROOT menu, + // and the panel isn't yet scrolled off screen top + if (currentPage == ROOT) { + drawSystemInfoPanel(0, siT, width()); // Draw the panel. + itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel + } + + // Draw menu items + // =================== + + // Which item will be drawn to the top-most slot? + // Initially, this is the item 0, but may increase once we begin scrolling + uint8_t firstItem; + if (cursor < slotCount) + firstItem = 0; + else + firstItem = cursor - (slotCount - 1); + + // Which item will be drawn to the bottom-most slot? + // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow + // This may be less than the slot-count, if we are reaching the end of the menuItems + uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1); + + // -- Loop: draw each (visible) menu item -- + for (uint8_t i = firstItem; i <= lastItem; i++) { + // Grab the menuItem + MenuItem item = items.at(i); + + // Center-line for the text + int16_t center = itemT + (itemH / 2); + + if (cursorShown && i == cursor) + drawRect(itemL, itemT, itemW, itemH, BLACK); + printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + + // Testing only: circle instead of check box + if (item.checkState) { + const uint16_t cbWH = fontSmall.lineHeight(); // Checbox: width / height + const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left + const int16_t cbT = center - (cbWH / 2); // Checkbox : top + // Checkbox ticked + if (*(item.checkState)) { + drawRect(cbL, cbT, cbWH, cbWH, BLACK); + // First point of tick: pen down + const int16_t t1Y = center; + const int16_t t1X = cbL + 3; + // Second point of tick: base + const int16_t t2Y = center + (cbWH / 2) - 2; + const int16_t t2X = cbL + (cbWH / 2); + // Third point of tick: end of tail + const int16_t t3Y = center - (cbWH / 2) - 2; + const int16_t t3X = cbL + cbWH + 2; + // Draw twice: faux bold + drawLine(t1X, t1Y, t2X, t2Y, BLACK); + drawLine(t2X, t2Y, t3X, t3Y, BLACK); + drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK); + drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK); + } + // Checkbox ticked + else + drawRect(cbL, cbT, cbWH, cbWH, BLACK); + } + + // Increment the y value (top) as we go + itemT += itemH; + } +} + +void InkHUD::MenuApplet::onButtonShortPress() +{ + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to next entry, then update + if (cursorShown) + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onButtonLongPress() +{ + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + + // If we didn't already request a specialized update, when handling a menu action, + // then perform the usual fast update. + // FAST keeps things responsive: important because we're dealing with user input + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu +void InkHUD::MenuApplet::populateAppletPage() +{ + assert(items.size() == 0); + + for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) { + const char *name = windowManager->getAppletName(i); + bool *isActive = &(settings.userApplets.active[i]); + items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive)); + } +} + +// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data +// We only populate this menu page with applets which are actually active +// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient. +void InkHUD::MenuApplet::populateAutoshowPage() +{ + assert(items.size() == 0); + + for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) { + // Only add a menu item if applet is active + if (settings.userApplets.active[i]) { + const char *name = windowManager->getAppletName(i); + bool *isActive = &(settings.userApplets.autoshow[i]); + items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive)); + } + } +} + +void InkHUD::MenuApplet::populateRecentsPage() +{ + // How many values are shown for use to choose from + constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); + + // Create an entry for each item in RECENTS_OPTIONS_MINUTES array + // (Defined at top of this file) + for (uint8_t i = 0; i < optionCount; i++) { + std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; + items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, 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. +void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight) +{ + // Reset the height + // We'll add to this as we add elements + uint16_t height = 0; + + // Clock (potentially) + // ==================== + std::string clockString = getTimeString(); + if (clockString.length() > 0) { + setFont(fontLarge); + printAt(width / 2, top, clockString, CENTER, TOP); + + height += fontLarge.lineHeight(); + height += fontLarge.lineHeight() * 0.1; // Padding below clock + } + + // Stats + // =================== + + setFont(fontSmall); + + // Position of the label row for the system info + const int16_t labelT = top + height; + height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing + + // Position of the data row for the system info + const int16_t valT = top + height; + height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider) + + // Position of divider between the info panel and the menu entries + const int16_t divY = top + height; + height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) + + // Create a variable number of columns + // Either 3 or 4, depending on whether we have GPS + // Todo + constexpr uint8_t N_COL = 3; + int16_t colL[N_COL]; + int16_t colC[N_COL]; + int16_t colR[N_COL]; + for (uint8_t i = 0; i < N_COL; i++) { + colL[i] = left + ((width / N_COL) * i); + colC[i] = colL[i] + ((width / N_COL) / 2); + colR[i] = colL[i] + (width / N_COL); + } + + // Info blocks, left to right + + // Voltage + float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; + char voltageStr[6]; // "XX.XV" + sprintf(voltageStr, "%.1fV", voltage); + printAt(colC[0], labelT, "Bat", CENTER, TOP); + printAt(colC[0], valT, voltageStr, CENTER, TOP); + + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[0], y, BLACK); + + // Channel Util + char chUtilStr[4]; // "XX%" + sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); + printAt(colC[1], labelT, "Ch", CENTER, TOP); + printAt(colC[1], valT, chUtilStr, CENTER, TOP); + + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[1], y, BLACK); + + // Duty Cycle (AirTimeTx) + char dutyUtilStr[4]; // "XX%" + sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); + printAt(colC[2], labelT, "Duty", CENTER, TOP); + printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); + + /* + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[2], y, BLACK); + + // GPS satellites - todo + printAt(colC[3], labelT, "Sats", CENTER, TOP); + printAt(colC[3], valT, "ToDo", CENTER, TOP); + */ + + // Horizontal divider, at bottom of system info panel + for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item + drawPixel(x, divY, BLACK); + + if (renderedHeight != nullptr) + *renderedHeight = height; +} + +// Get the height of the the panel drawn at the top of the menu +// This is inefficient, as we do actually have to render the panel to determine the height +// It solves a catch-22 situtation, where slotCount needs to know panel height, and panel height needs to know slotCount +uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() +{ + // Render *waay* off screen + uint16_t height = 0; + drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height); + + return height; +} + +#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 new file mode 100644 index 000000000..f2e9b3947 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -0,0 +1,60 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "configuration.h" + +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/InkHUD/Applet.h" +#include "graphics/niche/InkHUD/WindowManager.h" + +#include "./MenuItem.h" +#include "./MenuPage.h" + +#include "concurrency/OSThread.h" + +namespace NicheGraphics::InkHUD +{ + +class Applet; + +class MenuApplet : public Applet, public concurrency::OSThread +{ + public: + MenuApplet(); + void onActivate() override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onRender() override; + + void show(Tile *t); // Open the menu, onto a user tile + + protected: + int32_t runOnce() override; + + void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any + void showPage(MenuPage page); // Load and display a MenuPage + 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 + + MenuPage currentPage; + uint8_t cursor = 0; // Which menu item is currently highlighted + bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) + + uint16_t systemInfoPanelHeight = 0; // Need to know before we render + + std::vector items; // MenuItems for the current page. Filled by ShowPage + + Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu + + WindowManager *windowManager = nullptr; // Convenient access to the InkHUD::WindowManager singleton + Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h new file mode 100644 index 000000000..c74fe3d8a --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h @@ -0,0 +1,47 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +One item of a MenuPage, in InkHUD::MenuApplet + +Added to MenuPages in InkHUD::showPage + +- May open a submenu or exit +- May perform an action +- May toggle a bool value, shown by a checkbox + +*/ + +#pragma once + +#include "configuration.h" + +#include "./MenuAction.h" +#include "./MenuPage.h" + +namespace NicheGraphics::InkHUD +{ + +// One item of a MenuPage +class MenuItem +{ + public: + std::string label; + MenuAction action = NO_ACTION; + MenuPage nextPage = EXIT; + bool *checkState = nullptr; + + // Various constructors, depending on the intended function of the item + + MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {} + MenuItem(const char *label, MenuAction action) : label(label), action(action) {} + MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {} + MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState) + : label(label), action(action), nextPage(nextPage), checkState(checkState) + { + } +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h new file mode 100644 index 000000000..d2314e83b --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -0,0 +1,30 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Sub-menu for InkHUD::MenuApplet +Structure of the menu is defined in InkHUD::showPage + +*/ + +#pragma once + +#include "configuration.h" + +namespace NicheGraphics::InkHUD +{ + +// Sub-menu for MenuApplet +enum MenuPage : uint8_t { + ROOT, // Initial menu page + SEND, + OPTIONS, + APPLETS, + AUTOSHOW, + RECENTS, // Select length of "recentlyActiveSeconds" + EXIT, // Dismiss the menu applet +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h b/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h new file mode 100644 index 000000000..d8c4f8366 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h @@ -0,0 +1,40 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +A notification which might be displayed by the NotificationApplet + +An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification. +An Applet should veto a notification if it is already displaying the same info which the notification would convey. + +*/ + +#pragma once + +#include "configuration.h" + +namespace NicheGraphics::InkHUD +{ + +class Notification +{ + public: + enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type; + + uint32_t timestamp; + + uint8_t getChannel() { return channel; } + uint32_t getSender() { return sender; } + uint8_t getBatteryPercentage() { return batteryPercentage; } + + friend class NotificationApplet; + + protected: + uint8_t channel; + uint32_t sender; + uint8_t batteryPercentage; +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp new file mode 100644 index 000000000..886be84b5 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -0,0 +1,219 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./NotificationApplet.h" + +#include "./Notification.h" + +#include "RTC.h" + +using namespace NicheGraphics; + +void InkHUD::NotificationApplet::onActivate() +{ + textMessageObserver.observe(textMessageModule); +} + +// Note: This applet probably won't ever be deactivated +void InkHUD::NotificationApplet::onDeactivate() +{ + textMessageObserver.unobserve(textMessageModule); +} + +// Collect meta-info about the text message, and ask for approval for the notification +// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render() +int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +{ + // System applets are always active + assert(isActive()); + + // Abort if feature disabled + // This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled + if (!settings.optionalFeatures.notifications) + return 0; + + // Abort if this is an outgoing message + 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; + + Notification n; + n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + + // Gather info: in-channel message + if (isBroadcast(p->to)) { + n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + n.channel = p->channel; + } + + // Gather info: DM + else { + n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT; + n.sender = p->from; + } + + // Check if we should display the notification + // A foreground applet might already be displaying this info + hasNotification = true; + currentNotification = n; + if (isApproved()) { + bringToForeground(); + WindowManager::getInstance()->forceUpdate(); + } else + hasNotification = false; // Clear the pending notification: it was rejected + + // Return zero: no issues here, carry on notifying other observers! + return 0; +} + +void InkHUD::NotificationApplet::onRender() +{ + // Clear the region beneath the tile + // Most applets are drawing onto an empty frame buffer and don't need to do this + // We do need to do this with the battery though, as it is an "overlay" + fillRect(0, 0, width(), height(), WHITE); + + setFont(fontSmall); + + // Padding (horizontal) + const uint16_t padW = 4; + + // Main border + drawRect(0, 0, width(), height(), BLACK); + // drawRect(1, 1, width() - 2, height() - 2, BLACK); + + // Timestamp (potentially) + // ==================== + std::string ts = getTimeString(currentNotification.timestamp); + uint16_t tsW = 0; + int16_t divX = 0; + + // Timestamp available + if (ts.length() > 0) { + tsW = getTextWidth(ts); + divX = padW + tsW + padW; + + hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background + drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text + + setCrop(1, 1, divX - 1, height() - 2); + + // Drop shadow + setTextColor(WHITE); + printThick(padW + (tsW / 2), height() / 2, ts, 4, 4); + + // Bold text + setTextColor(BLACK); + printThick(padW + (tsW / 2), height() / 2, ts, 2, 1); + } + + // Main text + // ===================== + + // Background fill + // - medium dark (1/3) + hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK); + + uint16_t availableWidth = width() - divX - padW; + std::string text = getNotificationText(availableWidth); + + int16_t textM = divX + padW + (getTextWidth(text) / 2); + + // Restrict area for printing + // - don't overlap border, or diveder + setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); + + // Drop shadow + // - thick white text + setTextColor(WHITE); + printThick(textM, height() / 2, text, 4, 4); + + // Main text + // - faux bold: double width + setTextColor(BLACK); + printThick(textM, height() / 2, text, 2, 1); +} + +// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification +// Called internally when we first get a "notifiable event", and then again before render, +// in case autoshow swapped which applet was displayed +bool InkHUD::NotificationApplet::isApproved() +{ + // Instead of an assert + if (!hasNotification) { + LOG_WARN("No notif to approve"); + return false; + } + + return WindowManager::getInstance()->approveNotification(currentNotification); +} + +// Mark that the notification should no-longer be rendered +// In addition to calling thing method, code needs to request a re-render of all applets +void InkHUD::NotificationApplet::dismiss() +{ + sendToBackground(); + hasNotification = false; + // Not requesting update directly from this method, + // as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn +} + +// Get a string for the main body text of a notification +// Formatted to suit screen width +// Takes info from InkHUD::currentNotification +std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) +{ + assert(hasNotification); + + std::string text; + + // Text message + // ============== + + if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, + Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { + + // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently + bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + + // Pick source of message + MessageStore::Message *message = isBroadcast ? &latestMessage.broadcast : &latestMessage.dm; + + // Find info about the sender + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); + + // Leading tag (channel vs. DM) + text += isBroadcast ? "From:" : "DM: "; + + // Sender id + if (node && node->has_user) + text += node->user.short_name; + else + text += hexifyNodeNum(message->sender); + + // Check if text fits + // - use a longer string, if we have the space + if (getTextWidth(text) < widthAvailable * 0.5) { + text.clear(); + + // Leading tag (channel vs. DM) + text += isBroadcast ? "Msg from " : "DM from "; + + // Sender id + if (node && node->has_user) + text += node->user.short_name; + else + text += hexifyNodeNum(message->sender); + + text += ": "; + text += message->text; + } + } + + return text; +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h new file mode 100644 index 000000000..c4d36a4fd --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -0,0 +1,49 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Pop-up notification bar, on screen top edge +Displays information we feel is important, but which is not shown on currently focussed applet(s) +E.g.: messages, while viewing map, etc + +Feature should be optional; enable disable via on-screen menu + +*/ + +#pragma once + +#include "configuration.h" + +#include "concurrency/OSThread.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class NotificationApplet : public Applet +{ + public: + void onRender() override; + void onActivate() override; + void onDeactivate() override; + + int onReceiveTextMessage(const meshtastic_MeshPacket *p); + + bool isApproved(); // Does a foreground applet make notification redundant? + void dismiss(); // Close the Notification Popup + + protected: + // Get notified when a new text message arrives + CallbackObserver textMessageObserver = + CallbackObserver(this, &NotificationApplet::onReceiveTextMessage); + + std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width + + bool hasNotification = false; // Only used for assert. Todo: remove? + Notification currentNotification; // Set when something notification-worthy happens. Used by render() +}; + +} // namespace NicheGraphics::InkHUD + +#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 new file mode 100644 index 000000000..457fa0f3f --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -0,0 +1,96 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./PairingApplet.h" + +using namespace NicheGraphics; + +InkHUD::PairingApplet::PairingApplet() +{ + // Grab the window manager singleton, for convenience + windowManager = WindowManager::getInstance(); +} + +void InkHUD::PairingApplet::onRender() +{ + // Header + setFont(fontLarge); + 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); + 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()); + if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " + name = std::string(getDeviceName()); + if (getTextWidth(name) < width()) // Does it fit? + printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); +} + +void InkHUD::PairingApplet::onActivate() +{ + bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); +} + +void InkHUD::PairingApplet::onDeactivate() +{ + bluetoothStatusObserver.unobserve(&bluetoothStatus->onNewStatus); +} + +void InkHUD::PairingApplet::onForeground() +{ + // If another applet has locked the display, ask it to exit + Applet *other = windowManager->whoLocked(); + if (other != nullptr) + other->sendToBackground(); + + windowManager->claimFullscreen(this); // Take ownership of the fullscreen tile + windowManager->lock(this); // Prevent user applets from requesting update +} +void InkHUD::PairingApplet::onBackground() +{ + windowManager->releaseFullscreen(); // Relinquish ownership of the fullscreen tile + windowManager->unlock(this); // Allow normal user applet update requests to resume + + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + windowManager->forceUpdate(EInk::UpdateTypes::FULL); +} + +int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) +{ + // The standard Meshtastic convention is to pass these "generic" Status objects, + // check their type, and then cast them. + // We'll mimic that behavior, just to keep in line with the other Statuses, + // even though I'm not sure what the original reason for jumping through these extra hoops was. + assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); + meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; + + // When pairing begins + if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { + // Store the passkey for rendering + passkey = bluetoothStatus->getPasskey(); + + // Make sure no other system applets have a lock on the display + // Boot screen, menu, etc + Applet *lockOwner = windowManager->whoLocked(); + if (lockOwner) + lockOwner->sendToBackground(); + + // Show pairing screen + bringToForeground(); + } + + // When pairing ends + // or rather, when something changes, and we shouldn't be showing the pairing screen + else if (isForeground()) + sendToBackground(); + + return 0; // No special result to report back to Observable +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h new file mode 100644 index 000000000..ce420e68b --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h @@ -0,0 +1,43 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Shows the Bluetooth passkey during pairing + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class PairingApplet : public Applet +{ + public: + PairingApplet(); + + void onRender() override; + void onActivate() override; + void onDeactivate() override; + void onForeground() override; + void onBackground() override; + + int onBluetoothStatusUpdate(const meshtastic::Status *status); + + protected: + // Get notified when status of the Bluetooth connection changes + CallbackObserver bluetoothStatusObserver = + CallbackObserver(this, &PairingApplet::onBluetoothStatusUpdate); + + std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros + + WindowManager *windowManager = nullptr; // For convenience. Set in constructor. +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp new file mode 100644 index 000000000..4f66593b9 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp @@ -0,0 +1,21 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./PlaceholderApplet.h" + +using namespace NicheGraphics; + +InkHUD::PlaceholderApplet::PlaceholderApplet() +{ + // Because this applet sometimes gets processed as if it were a bonafide user applet, + // it's probably better that we do give it a human readable name, just in case it comes up later. + // For genuine user applets, this is set by WindowManager::addApplet + Applet::name = "Placeholder"; +} + +void InkHUD::PlaceholderApplet::onRender() +{ + // This placeholder applet fills its area with sparse diagonal lines + hatchRegion(0, 0, width(), height(), 8, BLACK); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h new file mode 100644 index 000000000..e5106105c --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h @@ -0,0 +1,30 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Shown when a tile doesn't have any other valid Applets +Fills the area with diagonal lines + +*/ + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class PlaceholderApplet : public Applet +{ + public: + PlaceholderApplet(); + void onRender() override; + + // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. + // The window manager decides when and where it should be rendered + // It may be drawn to several different tiles during on WindowManager::render call +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp new file mode 100644 index 000000000..e6b5b5dc9 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -0,0 +1,234 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./TipsApplet.h" + +using namespace NicheGraphics; + +InkHUD::TipsApplet::TipsApplet() +{ + // Grab the window manager singleton, for convenience + windowManager = WindowManager::getInstance(); +} + +void InkHUD::TipsApplet::onRender() +{ + switch (tipQueue.front()) { + case Tip::WELCOME: + renderWelcome(); + break; + + case Tip::FINISH_SETUP: { + setFont(fontLarge); + printAt(0, 0, "Tip: Finish Setup"); + + setFont(fontSmall); + int16_t cursorY = fontLarge.lineHeight() * 1.5; + printAt(0, cursorY, "- connect antenna"); + + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- connect a client app"); + + // Only if region not set + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- set region"); + } + + // Only if tz not set + if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- set timezone"); + } + + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "More info at meshtastic.org"); + + setFont(fontSmall); + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::SAFE_SHUTDOWN: { + setFont(fontLarge); + printAt(0, 0, "Tip: Shutdown"); + + setFont(fontSmall); + std::string shutdown; + shutdown += "Before removing power, please shutdown from InkHUD menu, or a client app. \n"; + shutdown += "\n"; + shutdown += "This ensures data is saved."; + printWrapped(0, fontLarge.lineHeight() * 1.5, width(), shutdown); + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + + } break; + + case Tip::CUSTOMIZATION: { + setFont(fontLarge); + printAt(0, 0, "Tip: Customization"); + + setFont(fontSmall); + printWrapped(0, fontLarge.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); + printAt(0, 0, "Tip: Buttons"); + + setFont(fontSmall); + int16_t cursorY = fontLarge.lineHeight() * 1.5; + + printAt(0, cursorY, "User Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- short press: next"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- long press: select / open menu"); + cursorY += fontSmall.lineHeight() * 1.5; + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::ROTATION: { + setFont(fontLarge); + printAt(0, 0, "Tip: Rotation"); + + setFont(fontSmall); + printWrapped(0, fontLarge.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); + + // Revert the "flip screen" setting, preventing this message showing again + config.display.flip_screen = false; + nodeDB->saveToDisk(SEGMENT_DEVICESTATE); + } break; + } +} + +// This tip has its own render method, only because it's a big block of code +// Didn't want to clutter up the switch in onRender too much +void InkHUD::TipsApplet::renderWelcome() +{ + uint16_t padW = X(0.05); + + // Block 1 - logo & title + // ======================== + + // Logo size + uint16_t logoWLimit = X(0.3); + uint16_t logoHLimit = Y(0.3); + uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); + uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); + + // Title size + setFont(fontLarge); + std::string title; + if (width() >= 200) // Future proofing: hide if *tiny* display + title = "meshtastic.org"; + uint16_t titleW = getTextWidth(title); + + // Center the block + // Desired effect: equal margin from display edge for logo left and title right + int16_t block1Y = Y(0.3); + int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); + int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); + int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); + + // Draw block + drawLogo(logoCX, block1Y, logoW, logoH); + printAt(titleCX, block1Y, title, CENTER, MIDDLE); + + // Block 2 - subtitle + // ======================= + setFont(fontSmall); + std::string subtitle = "InkHUD"; + if (width() >= 200) + subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display + printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); + + // Block 3 - press to continue + // ============================ + printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); +} + +// Grab fullscreen tile, and lock the window manager, when applet is shown +void InkHUD::TipsApplet::onForeground() +{ + windowManager->lock(this); + windowManager->claimFullscreen(this); +} + +void InkHUD::TipsApplet::onBackground() +{ + windowManager->releaseFullscreen(); + windowManager->unlock(this); +} + +void InkHUD::TipsApplet::onActivate() +{ + // Decide which tips (if any) should be shown to user after the boot screen + + // Welcome screen + if (settings.tips.firstBoot) + tipQueue.push_back(Tip::WELCOME); + + // Antenna, region, timezone + // Shown at boot if region not yet set + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + tipQueue.push_back(Tip::FINISH_SETUP); + + // Shutdown info + // Shown until user performs one valid shutdown + if (!settings.tips.safeShutdownSeen) + tipQueue.push_back(Tip::SAFE_SHUTDOWN); + + // Using the UI + if (settings.tips.firstBoot) { + tipQueue.push_back(Tip::CUSTOMIZATION); + tipQueue.push_back(Tip::BUTTONS); + } + + // Catch an incorrect attempt at rotating display + if (config.display.flip_screen) + tipQueue.push_back(Tip::ROTATION); + + // Applet will be brought to foreground when boot screen closes, via TipsApplet::onLockAvailable +} + +// While our applet has the window manager locked, we will receive the button input +void InkHUD::TipsApplet::onButtonShortPress() +{ + tipQueue.pop_front(); + + // All tips done + if (tipQueue.empty()) { + // Record that user has now seen the "tutorial" set of tips + // Don't show them on subsequent boots + if (settings.tips.firstBoot) { + settings.tips.firstBoot = false; + saveDataToFlash(); + } + + // Close applet, and full refresh to clean the screen + // Need to force update, because our request would be ignored otherwise, as we are now background + sendToBackground(); + windowManager->forceUpdate(EInk::UpdateTypes::FULL); + } + + // More tips left + else + requestUpdate(); +} + +// If the wm lock has just become availale (rendering, input), and we've still got tips, grab it! +// This situation would arise if bluetooth pairing occurs while TipsApplet was already shown (after pairing) +// Note: this event is only raised when *other* applets unlock the window manager +void InkHUD::TipsApplet::onLockAvailable() +{ + if (!tipQueue.empty()) + bringToForeground(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h new file mode 100644 index 000000000..29bcdfa8b --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -0,0 +1,52 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Shows info on how to use InkHUD + - tutorial at first boot + - additional tips in certain situation (e.g. bad shutdown, region unset) + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class TipsApplet : public Applet +{ + protected: + enum class Tip { + WELCOME, + FINISH_SETUP, + SAFE_SHUTDOWN, + CUSTOMIZATION, + BUTTONS, + ROTATION, + }; + + public: + TipsApplet(); + + void onRender() override; + void onActivate() override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onLockAvailable() override; // Reopen if interrupted by bluetooth pairing + + protected: + void renderWelcome(); // Very first screen of tutorial + + std::deque tipQueue; // List of tips to show, one after another + + WindowManager *windowManager = nullptr; // For convenience. Set in constructor. +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp new file mode 100644 index 000000000..1ae313d8a --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -0,0 +1,133 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./AllMessageApplet.h" + +using namespace NicheGraphics; + +void InkHUD::AllMessageApplet::onActivate() +{ + textMessageObserver.observe(textMessageModule); +} + +void InkHUD::AllMessageApplet::onDeactivate() +{ + textMessageObserver.unobserve(textMessageModule); +} + +// We're not consuming the data passed to this method; +// we're just just using it to trigger a render +int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +{ + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + if (!isActive()) + return 0; + + // Abort if this is an outgoing message + 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 + + // Return zero: no issues here, carry on notifying other observers! + return 0; +} + +void InkHUD::AllMessageApplet::onRender() +{ + setFont(fontSmall); + + // Find newest message, regardless of whether DM or broadcast + MessageStore::Message *message; + if (latestMessage.wasBroadcast) + message = &latestMessage.broadcast; + else + message = &latestMessage.dm; + + // Short circuit: no text message + if (!message->sender) { + printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE); + return; + } + + // =========================== + // Header (sender, timestamp) + // =========================== + + // Y position for divider + // - between header text and messages + + std::string header; + + // RX Time + // - if valid + std::string timeString = getTimeString(message->timestamp); + if (timeString.length() > 0) { + header += timeString; + header += ": "; + } + + // Sender's id + // - shortname, if available, or + // - node id + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); + if (sender && sender->has_user) { + header += sender->user.short_name; + header += " ("; + header += sender->user.long_name; + header += ")"; + } else + header += hexifyNodeNum(message->sender); + + // Draw a "standard" applet header + drawHeader(header); + + // Fade the right edge of the header, if text spills over edge + uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect + uint8_t hF = getHeaderHeight(); // Height of fade effect + if (getCursorX() > width()) + hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); + + // Dimensions of the header + constexpr int16_t padDivH = 2; + const int16_t headerDivY = Applet::getHeaderHeight() - 1; + + // =================== + // Print message text + // =================== + + // Extra gap below the header + int16_t textTop = headerDivY + padDivH; + + // Determine size if printed large + setFont(fontLarge); + uint32_t textHeight = getWrappedTextHeight(0, width(), message->text); + + // If too large, swap to small font + if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned) + setFont(fontSmall); + + // Print text + printWrapped(0, textTop, width(), message->text); +} + +// Don't show notifications for text messages when our applet is displayed +bool InkHUD::AllMessageApplet::approveNotification(Notification &n) +{ + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST) + return false; + + else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) + return false; + + else + return true; +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h new file mode 100644 index 000000000..c74e16196 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h @@ -0,0 +1,49 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Shows the latest incoming text message, as well as sender. +Both broadcast and direct messages will be shown here, from all channels. + +This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages +This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. +This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage + +We do still receive notifications from the text message module though, +to know when a new message has arrived, and trigger the update. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +#include "modules/TextMessageModule.h" + +namespace NicheGraphics::InkHUD +{ + +class Applet; + +class AllMessageApplet : public Applet +{ + public: + void onRender() override; + + void onActivate() override; + void onDeactivate() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); + + bool approveNotification(Notification &n) override; // Which notifications to suppress + + protected: + // Used to register our text message callback + CallbackObserver textMessageObserver = + CallbackObserver(this, &AllMessageApplet::onReceiveTextMessage); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp new file mode 100644 index 000000000..526b86901 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -0,0 +1,126 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./DMApplet.h" + +using namespace NicheGraphics; + +void InkHUD::DMApplet::onActivate() +{ + textMessageObserver.observe(textMessageModule); +} + +void InkHUD::DMApplet::onDeactivate() +{ + textMessageObserver.unobserve(textMessageModule); +} + +// We're not consuming the data passed to this method; +// we're just just using it to trigger a render +int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +{ + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + 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 + requestUpdate(); + + // If this was an incoming message, suggest that our applet becomes foreground, if permitted + if (getFrom(p) != nodeDB->getNodeNum()) + requestAutoshow(); + } + + // Return zero: no issues here, carry on notifying other observers! + return 0; +} + +void InkHUD::DMApplet::onRender() +{ + setFont(fontSmall); + + // Abort if no text message + if (!latestMessage.dm.sender) { + printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE); + return; + } + + // =========================== + // Header (sender, timestamp) + // =========================== + + // Y position for divider + // - between header text and messages + + std::string header; + + // RX Time + // - if valid + std::string timeString = getTimeString(latestMessage.dm.timestamp); + if (timeString.length() > 0) { + header += timeString; + header += ": "; + } + + // Sender's id + // - shortname, if available, or + // - node id + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage.dm.sender); + if (sender && sender->has_user) { + header += sender->user.short_name; + header += " ("; + header += sender->user.long_name; + header += ")"; + } else + header += hexifyNodeNum(latestMessage.dm.sender); + + // Draw a "standard" applet header + drawHeader(header); + + // Fade the right edge of the header, if text spills over edge + uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect + uint8_t hF = getHeaderHeight(); // Height of fade effect + if (getCursorX() > width()) + hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); + + // Dimensions of the header + constexpr int16_t padDivH = 2; + const int16_t headerDivY = Applet::getHeaderHeight() - 1; + + // =================== + // Print message text + // =================== + + // Extra gap below the header + int16_t textTop = headerDivY + padDivH; + + // Determine size if printed large + setFont(fontLarge); + uint32_t textHeight = getWrappedTextHeight(0, width(), latestMessage.dm.text); + + // If too large, swap to small font + if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned) + setFont(fontSmall); + + // Print text + printWrapped(0, textTop, width(), latestMessage.dm.text); +} + +// Don't show notifications for direct messages when our applet is displayed +bool InkHUD::DMApplet::approveNotification(Notification &n) +{ + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) + return false; + + else + return true; +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h new file mode 100644 index 000000000..b3dc36e66 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h @@ -0,0 +1,49 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Shows the latest incoming *Direct Message* (DM), as well as sender. +This compliments the threaded message applets + +This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages +This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. +This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage + +We do still receive notifications from the text message module though, +to know when a new message has arrived, and trigger the update. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +#include "modules/TextMessageModule.h" + +namespace NicheGraphics::InkHUD +{ + +class Applet; + +class DMApplet : public Applet +{ + public: + void onRender() override; + + void onActivate() override; + void onDeactivate() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); + + bool approveNotification(Notification &n) override; // Which notifications to suppress + + protected: + // Used to register our text message callback + CallbackObserver textMessageObserver = + CallbackObserver(this, &DMApplet::onReceiveTextMessage); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp new file mode 100644 index 000000000..ceb9c01fe --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -0,0 +1,123 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "RTC.h" + +#include "gps/GeoCoord.h" + +#include "./HeardApplet.h" + +using namespace NicheGraphics; + +void InkHUD::HeardApplet::onActivate() +{ + // When applet begins, pre-fill with stale info from NodeDB + populateFromNodeDB(); +} + +void InkHUD::HeardApplet::onDeactivate() +{ + // Avoid an unlikely situation where frquent activation / deactivation populated duplicate info from node DB + cards.clear(); +} + +// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo +// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result +void InkHUD::HeardApplet::handleParsed(CardInfo c) +{ + // Grab the previous entry. + // To check if the new data is different enough to justify re-render + // Need to cache now, before we manipulate the deque + CardInfo previous; + if (!cards.empty()) + previous = cards.at(0); + + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = cards.begin(); it != cards.end(); ++it) { + if (it->nodeNum == c.nodeNum) { + cards.erase(it); + break; + } + } + + 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 + + // Our rendered image needs to change if: + if (previous.nodeNum != c.nodeNum // Different node + || previous.signal != c.signal // or different signal strength + || previous.distanceMeters != c.distanceMeters // or different position + || previous.hopsAway != c.hopsAway) // or different hops away + { + requestAutoshow(); + requestUpdate(); + } +} + +// 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. +// 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() +{ + // Fill a collection with pointers to each node in db + std::vector ordered; + for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) { + // Only copy if valid, and not our own node + if (mn->num != 0 && mn->num != nodeDB->getNodeNum()) + ordered.push_back(&*mn); + } + + // Sort the collection by age + std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { + return (top->last_heard > bottom->last_heard); + }); + + // Keep the most recent entries onlyt + // Just enough to fill the screen + if (ordered.size() > maxCards()) + ordered.resize(maxCards()); + + // Create card info for these (stale) node observations + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + for (meshtastic_NodeInfoLite *node : ordered) { + CardInfo c; + c.nodeNum = node->num; + + if (node->has_hops_away) + c.hopsAway = node->hops_away; + + if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { + // Get lat and long as float + // Meshtastic stores these as integers internally + float ourLat = ourNode->position.latitude_i * 1e-7; + float ourLong = ourNode->position.longitude_i * 1e-7; + float theirLat = node->position.latitude_i * 1e-7; + float theirLong = node->position.longitude_i * 1e-7; + + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + } + + // Insert into the card collection (member of base class) + cards.push_back(c); + } +} + +// Text drawn in the usual applet header +// Handled by base class: ChronoListApplet +std::string InkHUD::HeardApplet::getHeaderText() +{ + uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node + + std::string text = "Heard: "; + + // Print node count, if nodeDB not yet nearing full + if (nodeCount < MAX_NUM_NODES) { + text += to_string(nodeCount); // Max nodes + text += " "; + text += (nodeCount == 1) ? "node" : "nodes"; + } + + return text; +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h new file mode 100644 index 000000000..932b5a75e --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h @@ -0,0 +1,35 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Shows a list of all nodes (recently heard or not), sorted by time last heard. +Most of the work is done by the InkHUD::NodeListApplet base class + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" + +namespace NicheGraphics::InkHUD +{ + +class HeardApplet : public NodeListApplet +{ + public: + HeardApplet() : NodeListApplet("HeardApplet") {} + void onActivate() override; + void onDeactivate() override; + + protected: + void handleParsed(CardInfo c) override; // Store new info, and update display if needed + std::string getHeaderText() override; // Set title for this applet + + void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp new file mode 100644 index 000000000..88bed998d --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -0,0 +1,110 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./PositionsApplet.h" + +using namespace NicheGraphics; + +void InkHUD::PositionsApplet::onRender() +{ + // Draw the usual map applet first + MapApplet::onRender(); + + // Draw our latest "node of interest" as a special marker + // ------------------------------------------------------- + // We might be rendering because we got a position packet from them + // We might be rendering because our own position updated + // Either way, we still highlight which node most recently sent us a position packet + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); + if (node && nodeDB->hasValidPosition(node) && enoughMarkers()) + drawLabeledMarker(node); +} + +// Determine if we need to redraw the map, when we receive a new position packet +ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // If applet is not active, we shouldn't be handling any data + // It's good practice for all applets to implement an early return like this + // for PositionsApplet, this is **required** - it's where we're handling active vs deactive + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Try decode a position from the packet + bool hasPosition = false; + float lat; + float lng; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position position = meshtastic_Position_init_default; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { + if (position.has_latitude_i && position.has_longitude_i // Actually has position + && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" + { + hasPosition = true; + lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format + lng = position.longitude_i * 1e-7; + } + } + } + + // Skip if we didn't get a valid position + if (!hasPosition) + return ProcessMessage::CONTINUE; + + bool hasHopsAway = (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start); // From NodeDB::updateFrom + uint8_t hopsAway = mp.hop_start - mp.hop_limit; + + // Determine if the position packet would change anything on-screen + // ----------------------------------------------------------------- + + bool somethingChanged = false; + + // If our own position + if (isFromUs(&mp)) { + // We get frequent position updates from connected phone + // Only update if we're travelled some distance, for rate limiting + // Todo: smarter detection of position changes + if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { + somethingChanged = true; + ourLastLat = lat; + ourLastLng = lng; + } + } + + // If someone else's position + else { + // Check if this position is from someone different than our previous position packet + if (mp.from != lastFrom) { + somethingChanged = true; + lastFrom = mp.from; + lastLat = lat; + lastLng = lng; + lastHopsAway = hopsAway; + } + + // Same sender: check if position changed + // Todo: smarter detection of position changes + else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { + somethingChanged = true; + lastLat = lat; + lastLng = lng; + } + + // Same sender, same position: check if hops changed + // Only pay attention if the hopsAway value is valid + else if (hasHopsAway && (hopsAway != lastHopsAway)) { + somethingChanged = true; + lastHopsAway = hopsAway; + } + } + + // Decision reached + // ----------------- + + if (somethingChanged) { + requestAutoshow(); // Todo: only request this in some situations? + requestUpdate(); + } + + return ProcessMessage::CONTINUE; +} + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h new file mode 100644 index 000000000..5bcec339d --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h @@ -0,0 +1,43 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Plots position of all nodes from DB, with North facing up. +Scaled to fit the most distant node. +Size of cross represents hops away. +The node which has most recently sent a position will be labeled. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h" + +#include "SinglePortModule.h" + +namespace NicheGraphics::InkHUD +{ + +class PositionsApplet : public MapApplet, public SinglePortModule +{ + public: + PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} + void onRender() override; + + protected: + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + NodeNum lastFrom; // Sender of most recent (non-local) position packet + float lastLat; + float lastLng; + float lastHopsAway; + + float ourLastLat; // Info about the most recent (non-local) position packet + float ourLastLng; // Info about most recent *local* position +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp new file mode 100644 index 000000000..54e67efef --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp @@ -0,0 +1,150 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./RecentsListApplet.h" + +#include "RTC.h" + +using namespace NicheGraphics; + +InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") +{ + // No scheduled tasks initially + OSThread::disable(); +} + +void InkHUD::RecentsListApplet::onActivate() +{ + // When the applet is activated, begin scheduled purging of any nodes which are no longer "active" + OSThread::enabled = true; + OSThread::setIntervalFromNow(60 * 1000UL); // Every minute +} + +void InkHUD::RecentsListApplet::onDeactivate() +{ + // Halt scheduled purging + OSThread::disable(); +} + +int32_t InkHUD::RecentsListApplet::runOnce() +{ + prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently + return OSThread::interval; +} + +// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo +// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result +// We also need to record the current time against the nodenum, so we know when it becomes inactive +void InkHUD::RecentsListApplet::handleParsed(CardInfo c) +{ + // Grab the previous entry. + // To check if the new data is different enough to justify re-render + // Need to cache now, before we manipulate the deque + CardInfo previous; + if (!cards.empty()) + previous = cards.at(0); + + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = cards.begin(); it != cards.end(); ++it) { + if (it->nodeNum == c.nodeNum) { + cards.erase(it); + break; + } + } + + 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 + + // Record the time of this observation + // Used to count active nodes, and to know when to prune inactive nodes + seenNow(c.nodeNum); + + // Our rendered image needs to change if: + if (previous.nodeNum != c.nodeNum // Different node + || previous.signal != c.signal // or different signal strength + || previous.distanceMeters != c.distanceMeters // or different position + || previous.hopsAway != c.hopsAway) // or different hops away + { + prune(); // Take the opportunity now to remove inactive nodes + requestAutoshow(); + requestUpdate(); + } +} + +// Record the time (millis, right now) that we hear a node +// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly +void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) +{ + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = ages.begin(); it != ages.end(); ++it) { + if (it->nodeNum == nodeNum) { + ages.erase(it); + break; + } + } + + Age a; + a.nodeNum = nodeNum; + a.seenAtMs = millis(); + + ages.push_front(a); +} + +// Remove Card and Age info for any nodes which are now inactive +// Determined by when a node was last heard, in our internal record (not from nodeDB) +void InkHUD::RecentsListApplet::prune() +{ + // Iterate age records from newest to oldest + for (uint16_t i = 0; i < ages.size(); i++) { + // Found the first record which is too old + if (!isActive(ages.at(i).seenAtMs)) { + // Drop this item, and all others behind it + ages.resize(i); + cards.resize(i); + + // Request an update, if pruning did modify our data + // Required if pruning was scheduled. Redundent if pruning was prior to rendering. + requestAutoshow(); + requestUpdate(); + + break; + } + } + + // Push next scheduled pruning back + // Pruning may be called from by handleParsed, immediately prior to rendering + // In that case, we can slightly delay our scheduled pruning + OSThread::setIntervalFromNow(60 * 1000UL); +} + +// Is a timestamp old enough that it would make a node inactive, and in need of purging? +bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) +{ + uint32_t now = millis(); + uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe + + return (secsAgo < settings.recentlyActiveSeconds); +} + +// Text to be shown at top of applet +// ChronoListApplet base class allows us to set this dynamically +// Might want to adjust depending on node count, RTC status, etc +std::string InkHUD::RecentsListApplet::getHeaderText() +{ + std::string text; + + // Print the length of our "Recents" time-window + text += "Last "; + text += to_string(settings.recentlyActiveSeconds / 60); + text += " mins"; + + // Print the node count + const uint16_t nodeCount = ages.size(); + text += ": "; + text += to_string(nodeCount); + text += " "; + text += (nodeCount == 1) ? "node" : "nodes"; + + return text; +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h new file mode 100644 index 000000000..74f5f3e57 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h @@ -0,0 +1,52 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Shows a list of nodes which have been recently active +The length of this "recently active" window is configurable using the onscreen menu + +Most of the work is done by the shared InkHUD::NodeListApplet base class + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" + +namespace NicheGraphics::InkHUD +{ + +class RecentsListApplet : public NodeListApplet, public concurrency::OSThread +{ + protected: + // Used internally to count the number of active nodes + // We count for ourselves, instead of using the value provided by NodeDB, + // as the values occasionally differ, due to the timing of our Applet's purge method + struct Age { + uint32_t nodeNum; + uint32_t seenAtMs; + }; + + public: + RecentsListApplet(); + void onActivate() override; + void onDeactivate() override; + + protected: + int32_t runOnce() override; + + void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed + std::string getHeaderText() override; // Set title for this applet + + void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count + void prune(); // Remove cards for nodes which we haven't seen recently + bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it? + + std::deque ages; // Information about when we last heard nodes. Independent of NodeDB +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp new file mode 100644 index 000000000..d81dd020c --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -0,0 +1,270 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./ThreadedMessageApplet.h" + +#include "RTC.h" +#include "mesh/NodeDB.h" + +using namespace NicheGraphics; + +// Hard limits on how much message data to write to flash +// Avoid filling the storage if something goes wrong +// Normal usage should be well below this size +constexpr uint8_t MAX_MESSAGES_SAVED = 10; +constexpr uint32_t MAX_MESSAGE_SIZE = 250; + +InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex) +{ + // Create the message store + // Will shortly attempt to load messages from RAM, if applet is active + // Label (filename in flash) is set from channel index + store = new MessageStore("ch" + to_string(channelIndex)); +} + +void InkHUD::ThreadedMessageApplet::onRender() +{ + setFont(fontSmall); + + // ============= + // Draw a header + // ============= + + // Header text + std::string headerText; + headerText += "Channel "; + headerText += to_string(channelIndex); + headerText += ": "; + if (channels.isDefaultChannel(channelIndex)) + headerText += "Public"; + else + headerText += channels.getByIndex(channelIndex).settings.name; + + // Draw a "standard" applet header + drawHeader(headerText); + + // Y position for divider + const int16_t dividerY = Applet::getHeaderHeight() - 1; + + // ================== + // Draw each message + // ================== + + // Restrict drawing area + // - don't overdraw the header + // - small gap below divider + setCrop(0, dividerY + 2, width(), height() - (dividerY + 2)); + + // Set padding + // - separates text from the vertical line which marks its edge + constexpr uint16_t padW = 2; + constexpr int16_t msgL = padW; + const int16_t msgR = (width() - 1) - padW; + const uint16_t msgW = (msgR - msgL) + 1; + + int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. + uint8_t i = 0; // Index of stored message + + // Loop over messages + // - until no messages left, or + // - until no part of message fits on screen + while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { + + // Grab data for message + MessageStore::Message &m = store->messages.at(i); + bool outgoing = (m.sender == 0); + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); + + // 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); + int16_t bodyT = msgB - bodyH; + + // Print message + // - if incoming + if (!outgoing) + printWrapped(msgL, bodyT, msgW, m.text); + + // 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 + } + + // Move cursor up + // - above message text + msgB -= bodyH; + msgB -= getFont().lineHeight() * 0.2; // Padding between message and header + + // Compose info string + // - 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) + info += "Me"; + else + info += hexifyNodeNum(m.sender); + + std::string timeString = getTimeString(m.timestamp); + if (timeString.length() > 0) { + info += " - "; + info += timeString; + } + + // Print the info string + // - Faux bold: printed twice, shifted horizontally by one px + printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); + printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); + + // Underline the info string + const int16_t divY = msgB; + int16_t divL; + int16_t divR; + if (!outgoing) { + // Left side - incoming + divL = msgL; + divR = getTextWidth(info) + getFont().lineHeight() / 2; + } else { + // Right side - outgoing + divR = msgR; + divL = divR - getTextWidth(info) - getFont().lineHeight() / 2; + } + for (int16_t x = divL; x <= divR; x += 2) + drawPixel(x, divY, BLACK); + + // Move cursor up: above info string + msgB -= fontSmall.lineHeight(); + + // Vertical line alongside message + for (int16_t y = msgB; y < dotsB; y += 1) + drawPixel(outgoing ? width() - 1 : 0, y, BLACK); + + // Move cursor up: padding before next message + msgB -= fontSmall.lineHeight() * 0.5; + + i++; + } // End of loop: drawing each message + + // Fade effect: + // Area immediately below the divider. Overdraw with sparse white lines. + // Make text appear to pass behind the header + hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); + + // If we've run out of screen to draw messages, we can drop any leftover data from the queue + // Those messages have been pushed off the screen-top by newer ones + while (i < store->messages.size()) + store->messages.pop_back(); +} + +// Code which runs when the applet begins running +// This might happen at boot, or if user enables the applet at run-time, via the menu +void InkHUD::ThreadedMessageApplet::onActivate() +{ + loadMessagesFromFlash(); + textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage +} + +// Code which runs when the applet stop running +// This might be happen at shutdown, or if user disables the applet at run-time +void InkHUD::ThreadedMessageApplet::onDeactivate() +{ + textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage +} + +// 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) +{ + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + if (!isActive()) + return 0; + + // Abort if wrong channel + if (p->channel != this->channelIndex) + return 0; + + // 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; + + // 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]); + + // 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()) + requestAutoshow(); + + // Redraw the applet, perhaps. + requestUpdate(); // Want to update display, if applet is foreground + + return 0; +} + +// Don't show notifications for text messages broadcast to our channel, when the applet is displayed +bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) +{ + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex) + return false; + + // None of our business. Allow the notification. + else + return true; +} + +// Save several recent messages to flash +// Stores the contents of ThreadedMessageApplet::messages +// Just enough messages to fill the display +// Messages are packed "back-to-back", to minimize blocks of flash used +void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() +{ + // Create a label (will become the filename in flash) + std::string label = "ch" + to_string(channelIndex); + + store->saveToFlash(); +} + +// Load recent messages to flash +// Fills ThreadedMessageApplet::messages with previous messages +// Just enough messages have been stored to cover the display +void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() +{ + // Create a label (will become the filename in flash) + std::string label = "ch" + to_string(channelIndex); + + store->loadFromFlash(); +} + +// Code to run when device is shutting down +// This is in addition to any onDeactivate() code, which will also run +// Todo: implement before a reboot also +void InkHUD::ThreadedMessageApplet::onShutdown() +{ + // Save our current set of messages to flash, provided the applet isn't disabled + if (isActive()) + saveMessagesToFlash(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h new file mode 100644 index 000000000..5bb8bf96e --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -0,0 +1,63 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Displays a thread-view of incoming and outgoing message for a specific channel + +The channel for this applet is set in the constructor, +when the applet is added to WindowManager in the setupNicheGraphics method. + +Several messages are saved to flash at shutdown, to preseve applet between reboots. +This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer. +If the amount of flash usage is unacceptable, we could keep these in RAM only. + +Multiple instances of this channel may be used. This must be done at buildtime. +Suggest a max of two channel, to minimize fs usage? + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" +#include "graphics/niche/InkHUD/MessageStore.h" + +#include "modules/TextMessageModule.h" + +namespace NicheGraphics::InkHUD +{ + +class Applet; + +class ThreadedMessageApplet : public Applet +{ + public: + ThreadedMessageApplet(uint8_t channelIndex); + ThreadedMessageApplet() = delete; + + void onRender() override; + + void onActivate() override; + void onDeactivate() override; + void onShutdown() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); + + 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(); + + MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown + uint8_t channelIndex = 0; +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp new file mode 100644 index 000000000..ac6fe1b35 --- /dev/null +++ b/src/graphics/niche/InkHUD/MessageStore.cpp @@ -0,0 +1,139 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./MessageStore.h" + +#include "SafeFile.h" + +using namespace NicheGraphics; + +// Hard limits on how much message data to write to flash +// Avoid filling the storage if something goes wrong +// Normal usage should be well below this size +constexpr uint8_t MAX_MESSAGES_SAVED = 10; +constexpr uint32_t MAX_MESSAGE_SIZE = 250; + +InkHUD::MessageStore::MessageStore(std::string label) +{ + filename = ""; + filename += "/NicheGraphics"; + filename += "/"; + filename += label; + filename += ".msgs"; +} + +// Write the contents of the MessageStore::messages object to flash +void InkHUD::MessageStore::saveToFlash() +{ + assert(!filename.empty()); + +#ifdef FSCom + // Make the directory, if doesn't already exist + // This is the same directory accessed by NicheGraphics::FlashData + FSCom.mkdir("/NicheGraphics"); + + // Open or create the file + // No "full atomic": don't save then rename + auto f = SafeFile(filename.c_str(), false); + + LOG_INFO("Saving messages in %s", filename.c_str()); + + // 1st byte: how many messages will be written to store + f.write(messages.size()); + + // For each message + for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { + Message &m = messages.at(i); + f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes + f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes + f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte + f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length + f.write('\0'); // Append null term + LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); + } + + bool writeSucceeded = f.close(); + + if (!writeSucceeded) { + LOG_ERROR("Can't write data!"); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented\n"); +#endif +} + +// Attempt to load the previous contents of the MessageStore:message deque from flash. +// Filename is controlled by the "label" parameter +void InkHUD::MessageStore::loadFromFlash() +{ + // Hopefully redundant. Initial intention is to only load / save once per boot. + messages.clear(); + +#ifdef FSCom + + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_WARN("'%s' not found. Using default values", filename.c_str()); + return; + } + + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_INFO("'%s' not found.", filename.c_str()); + return; + } + + // Open the file + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + + if (f.size() == 0) { + LOG_INFO("%s is empty", filename.c_str()); + f.close(); + return; + } + + // If opened, start reading + if (f) { + LOG_INFO("Loading threaded messages '%s'", filename.c_str()); + + // First byte: how many messages are in the flash store + uint8_t flashMessageCount = 0; + f.readBytes((char *)&flashMessageCount, 1); + LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); + + // For each message + for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { + Message m; + + // Read meta data (fixed width) + f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); + f.readBytes((char *)&m.sender, sizeof(m.sender)); + f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); + + // Read characters until we find a null term + char c; + while (m.text.size() < MAX_MESSAGE_SIZE) { + f.readBytes(&c, 1); + if (c != '\0') + m.text += c; + else + break; + } + + // Store in RAM + messages.push_back(m); + + LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); + } + + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename.c_str()); + } +#else + LOG_ERROR("Filesystem not implemented"); + state = LoadFileState::NO_FILESYSTEM; +#endif + return; +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h new file mode 100644 index 000000000..3cf7d0f68 --- /dev/null +++ b/src/graphics/niche/InkHUD/MessageStore.h @@ -0,0 +1,47 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +We hold a few recent messages, for features like the threaded message applet. +This class contains a struct for storing those messages, +and methods for serializing them to flash. + +*/ + +#pragma once + +#include "configuration.h" + +#include + +#include "mesh/MeshTypes.h" + +namespace NicheGraphics::InkHUD +{ + +class MessageStore +{ + public: + // A stored message + struct Message { + uint32_t timestamp; // Epoch seconds + NodeNum sender = 0; + uint8_t channelIndex; + std::string text; + }; + + MessageStore() = delete; + MessageStore(std::string label); // Label determines filename in flash + + void saveToFlash(); + void loadFromFlash(); + + std::deque messages; // Interact with this object! + + private: + std::string filename; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Persistence.cpp b/src/graphics/niche/InkHUD/Persistence.cpp new file mode 100644 index 000000000..6e8ac0458 --- /dev/null +++ b/src/graphics/niche/InkHUD/Persistence.cpp @@ -0,0 +1,59 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./Persistence.h" + +using namespace NicheGraphics; + +// Load settings and latestMessage data +void InkHUD::loadDataFromFlash() +{ + // Load the InkHUD settings from flash, and check version number + // We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data + InkHUD::Settings loadedSettings; + bool loadSucceeded = FlashData::load(&loadedSettings, "settings"); + if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0) + settings = loadedSettings; // Version matched, replace the defaults with the loaded values + else + LOG_WARN("Settings version changed. Using defaults"); + + // Load previous "latestMessages" data from flash + MessageStore store("latest"); + store.loadFromFlash(); + + // Place into latestMessage struct, for convenient access + // Number of strings loaded determines whether last message was broadcast or dm + if (store.messages.size() == 1) { + latestMessage.dm = store.messages.at(0); + latestMessage.wasBroadcast = false; + } else if (store.messages.size() == 2) { + latestMessage.dm = store.messages.at(0); + latestMessage.broadcast = store.messages.at(1); + latestMessage.wasBroadcast = true; + } +} + +// Save settings and latestMessage data +void InkHUD::saveDataToFlash() +{ + // Save the InkHUD settings to flash + FlashData::save(&settings, "settings"); + + // Number of strings saved determines whether last message was broadcast or dm + MessageStore store("latest"); + store.messages.push_back(latestMessage.dm); + if (latestMessage.wasBroadcast) + store.messages.push_back(latestMessage.broadcast); + store.saveToFlash(); +} + +// Holds InkHUD settings while running +// Saved back to Flash at shutdown +// Accessed by including persistence.h +InkHUD::Settings InkHUD::settings; + +// Holds copies of the most recent broadcast and DM messages while running +// Saved to Flash at shutdown +// Accessed by including persistence.h +InkHUD::LatestMessage InkHUD::latestMessage; + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h new file mode 100644 index 000000000..e2daa02d9 --- /dev/null +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -0,0 +1,123 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +A quick and dirty alternative to storing "device only" settings using the protobufs +Convenient during development. +Potentially a polite option, to avoid polluting the generated code with values for obscure use cases like this. + +The save / load mechanism is a shared NicheGraphics feature. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/FlashData.h" +#include "graphics/niche/InkHUD/MessageStore.h" + +namespace NicheGraphics::InkHUD +{ + +constexpr uint8_t MAX_TILES_GLOBAL = 4; +constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16; + +// Used to invalidate old settings, if needed +// Version 0 is reserved for testing, and will always load defaults +constexpr uint32_t SETTINGS_VERSION = 2; + +struct Settings { + struct Meta { + // Used to invalidate old savefiles, if we make breaking changes + uint32_t version = SETTINGS_VERSION; + } meta; + + struct UserTiles { + // How many tiles are shown + uint8_t count = 1; + + // Maximum amount of tiles for this display + uint8_t maxCount = 4; + + // Which tile is focused (responding to user button input) + uint8_t focused = 0; + + // Which applet is displayed on which tile + // Index of array: which tile, as indexed in WindowManager::tiles + // Value of array: which applet, as indexed in WindowManager::activeApplets + uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3}; + } userTiles; + + struct UserApplets { + // Which applets are running (either displayed, or in the background) + // Index of array: which applet, as indexed in WindowManager::applets + // Initial value is set by the "activeByDefault" parameter of WindowManager::addApplet, in setupNicheGraphics() + bool active[MAX_USERAPPLETS_GLOBAL]; + + // Which user applets should be automatically shown when they have important data to show + // If none set, foreground applets should remain foreground without manual user input + // If multiple applets request this at once, + // priority is the order which they were passed to WindowManager::addApplets, in setupNicheGraphics() + bool autoshow[MAX_USERAPPLETS_GLOBAL]{false}; + } userApplets; + + // Features which the use can enable / disable via the on-screen menu + struct OptionalFeatures { + bool notifications = true; + bool batteryIcon = false; + } optionalFeatures; + + // Some menu items may not be required, based on device / configuration + // We can enable them only when needed, to de-clutter the menu + struct OptionalMenuItems { + // If aux button is used to swap between tiles, we have to need for this menu item + bool nextTile = true; + + // Used if backlight present, and not controlled by AUX button + // If this item is added to menu: backlight is always active when menu is open + // The added menu items then allows the user to "Keep Backlight On", globally. + bool backlight = false; + } optionalMenuItems; + + // Allows tips to be run once only + struct Tips { + // Enables the longer "tutorial" shown only on first boot + // Once tutorial has been completed, it is no longer shown + bool firstBoot = true; + + // User is advised to shutdown before removing device power + // Once user executes a shutdown (either via menu or client app), + // this tip is no longer shown + bool safeShutdownSeen = false; + } tips; + + // Rotation of the display + // Multiples of 90 degrees clockwise + // Most commonly: rotation is 0 when flex connector is oriented below display + uint8_t rotation = 1; + + // How long do we consider another node to be "active"? + // Used when applets want to filter for "active nodes" only + uint32_t recentlyActiveSeconds = 2 * 60; +}; + +// Most recently received text message +// Value is updated by InkHUD::WindowManager, as a courtesty to applets +// Note: different from devicestate.rx_text_message, +// which may contain an *outgoing message* to broadcast +struct LatestMessage { + MessageStore::Message broadcast; // Most recent message received broadcast + MessageStore::Message dm; // Most recent received DM + bool wasBroadcast; // True if most recent broadcast is newer than most recent dm +}; + +extern Settings settings; +extern LatestMessage latestMessage; + +void loadDataFromFlash(); +void saveDataToFlash(); + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini new file mode 100644 index 000000000..7eb1d34e9 --- /dev/null +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -0,0 +1,10 @@ +[inkhud] +board_level = extra +build_src_filter = +<../variants/$PIOENV> ; Include nicheGraphics.h +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 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/README.md b/src/graphics/niche/InkHUD/README.md new file mode 100644 index 000000000..8d788ffa8 --- /dev/null +++ b/src/graphics/niche/InkHUD/README.md @@ -0,0 +1,12 @@ +# InkHUD + +A heads-up-display for E-Ink devices, intended to supplement a connected phone / client. Implemented as a "NicheGraphics" UI. + +Supported devices (as of 1st Feb. 2025): + +- Heltec Vision Master E213 +- Heltec Vision Master E290 +- Heltec Wireless Paper V1.1 +- LILYGO T-Echo + +More to follow diff --git a/src/graphics/niche/InkHUD/Tile.cpp b/src/graphics/niche/InkHUD/Tile.cpp new file mode 100644 index 000000000..e65835801 --- /dev/null +++ b/src/graphics/niche/InkHUD/Tile.cpp @@ -0,0 +1,237 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./Tile.h" + +#include "concurrency/Periodic.h" + +using namespace NicheGraphics; + +// Static members of Tile class (for linking) +InkHUD::Tile *InkHUD::Tile::highlightTarget; +bool InkHUD::Tile::highlightShown; + +// For dismissing the highlight indicator, after a few seconds +// Highlighting is used to inform user of which tile is now focused +static concurrency::Periodic *taskHighlight; +static int32_t runtaskHighlight() +{ + LOG_DEBUG("Dismissing Highlight"); + InkHUD::Tile::highlightShown = false; + InkHUD::Tile::highlightTarget = nullptr; + InkHUD::WindowManager::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting + return taskHighlight->disable(); +} +static void inittaskHighlight() +{ + static bool doneOnce = false; + if (!doneOnce) { + taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight); + taskHighlight->disable(); + doneOnce = true; + } +} + +InkHUD::Tile::Tile() +{ + // For convenince + windowManager = InkHUD::WindowManager::getInstance(); + + inittaskHighlight(); + Tile::highlightTarget = nullptr; + Tile::highlightShown = false; +} + +// Set the region of the tile automatically, based on the user's chosen layout +// This method places tiles which will host user applets +// The WindowManager multiplexes the applets to these tiles automatically +void InkHUD::Tile::placeUserTile(uint8_t userTileCount, uint8_t tileIndex) +{ + uint16_t displayWidth = windowManager->getWidth(); + uint16_t displayHeight = windowManager->getHeight(); + + bool landscape = displayWidth > displayHeight; + + // Check for any stray tiles + if (tileIndex > (userTileCount - 1)) { + // Dummy values to prevent rendering + LOG_WARN("Tile index out of bounds"); + left = -2; + top = -2; + width = 1; + height = 1; + return; + } + + // Todo: special handling for the notification area + // Todo: special handling for 3 tile layout + + // Gap between tiles + const uint16_t spacing = 4; + + switch (userTileCount) { + // One tile only + case 1: + left = 0; + top = 0; + width = displayWidth; + height = displayHeight; + break; + + // Two tiles + case 2: + if (landscape) { + // Side by side + left = ((displayWidth / 2) + (spacing / 2)) * tileIndex; + top = 0; + width = (displayWidth / 2) - (spacing / 2); + height = displayHeight; + } else { + // Above and below + left = 0; + top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex); + width = displayWidth; + height = (displayHeight / 2) - (spacing / 2); + } + break; + + // Four tiles + case 4: + width = (displayWidth / 2) - (spacing / 2); + height = (displayHeight / 2) - (spacing / 2); + switch (tileIndex) { + case 0: + left = 0; + top = 0; + break; + case 1: + left = 0 + (width - 1) + spacing; + top = 0; + break; + case 2: + left = 0; + top = 0 + (height - 1) + spacing; + break; + case 3: + left = 0 + (width - 1) + spacing; + top = 0 + (height - 1) + spacing; + break; + } + break; + + default: + LOG_ERROR("Unsupported tile layout"); + assert(0); + } + + assert(width > 0 && height > 0); + + this->left = left; + this->top = top; + this->width = width; + this->height = height; +} + +// Manually set the region for a tile +// This is only done for tiles which will host certain "System Applets", which have unique position / sizes: +// Things like the NotificationApplet, BatteryIconApplet, etc +void InkHUD::Tile::placeSystemTile(int16_t left, int16_t top, uint16_t width, uint16_t height) +{ + assert(width > 0 && height > 0); + + this->left = left; + this->top = top; + this->width = width; + this->height = height; +} + +// Place an applet onto a tile +// Creates a reciprocal link between applet and tile +// The tile should always know which applet is displayed +// The applet should always know which tile it is display on +// This is enforced with asserts +// Assigning a new applet will break a previous link +// Link may also be broken by assigning a nullptr +void InkHUD::Tile::assignApplet(Applet *a) +{ + // Break the link between old applet and this tile + if (assignedApplet) + assignedApplet->setTile(nullptr); + + // Store the new applet + assignedApplet = a; + + // Create the reciprocal link between the new applet and this tile + if (a) + a->setTile(this); +} + +// Get pointer to whichever applet is displayed on this tile +InkHUD::Applet *InkHUD::Tile::getAssignedApplet() +{ + return assignedApplet; +} + +// Receive drawing output from the assigned applet, +// and translate it from "applet-space" coordinates, to it's true location. +// The final "rotation" step is performed by the windowManager +void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) +{ + // Move pixels from applet-space to tile-space + x += left; + y += top; + + // Crop to tile borders + if (x >= left && x < (left + width) && y >= top && y < (top + height)) { + // Pass to the window manager + windowManager->handleTilePixel(x, y, c); + } +} + +// Called by Applet base class, when learning of its dimensions +uint16_t InkHUD::Tile::getWidth() +{ + return width; +} + +// Called by Applet base class, when learning of its dimensions +uint16_t InkHUD::Tile::getHeight() +{ + return height; +} + +// Longest edge of the display, in pixels +// Maximum possible size of any tile's width / height +// Used by some components to allocate resources for the "worst possible situtation" +// "Sizing the cathedral for christmas eve" +uint16_t InkHUD::Tile::maxDisplayDimension() +{ + WindowManager *wm = WindowManager::getInstance(); + return max(wm->getHeight(), wm->getWidth()); +} + +// Ask for this tile to be highlighted +// Used to indicate which tile is now indicated after focus changes +// Only used for aux button focus changes, not changes via menu +void InkHUD::Tile::requestHighlight() +{ + Tile::highlightTarget = this; + Tile::highlightShown = false; + windowManager->forceUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first +void InkHUD::Tile::startHighlightTimeout() +{ + taskHighlight->setIntervalFromNow(5 * 1000UL); + taskHighlight->enabled = true; +} + +// Stop the timer which would automatically dismiss the highlighting +// Called if the tile organically renders before the timer is up +void InkHUD::Tile::cancelHighlightTimeout() +{ + if (taskHighlight->enabled) + taskHighlight->disable(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Tile.h b/src/graphics/niche/InkHUD/Tile.h new file mode 100644 index 000000000..e41536e53 --- /dev/null +++ b/src/graphics/niche/InkHUD/Tile.h @@ -0,0 +1,62 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Class which represents a region of the display area + Applets are assigned to a tile + Tile controls the Applet's dimensions + Tile receives pixel output from the applet, and translates it to the correct display region + +*/ + +#pragma once + +#include "configuration.h" + +#include "./Applet.h" +#include "./Types.h" +#include "./WindowManager.h" + +#include + +namespace NicheGraphics::InkHUD +{ + +class Applet; +class WindowManager; + +class Tile +{ + public: + Tile(); + void placeUserTile(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout + void placeSystemTile(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually + void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet + uint16_t getWidth(); // Used to set the assigned applet's width before render + uint16_t getHeight(); // Used to set the assigned applet's height before render + static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter + + void assignApplet(Applet *a); // Place an applet onto a tile + Applet *getAssignedApplet(); // Applet which is on a tile + + void requestHighlight(); // Ask for this tile to be highlighted + static void startHighlightTimeout(); // Start the auto-dismissal timer + static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed + + static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?) + static bool highlightShown; // Is the tile highlighted yet? Controlls highlight vs dismiss + + protected: + int16_t left; + int16_t top; + uint16_t width; + uint16_t height; + + Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile + + WindowManager *windowManager; // Convenient access to the WindowManager singleton +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Types.h b/src/graphics/niche/InkHUD/Types.h new file mode 100644 index 000000000..f4ab9ed4e --- /dev/null +++ b/src/graphics/niche/InkHUD/Types.h @@ -0,0 +1,62 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Custom data types for InkHUD + +Only "general purpose" data-types should be defined here. +If your applet has its own structs or enums, which won't be useful to other applets, +please define them inside (or in the same folder as) your applet. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/Drivers/EInk/EInk.h" + +namespace NicheGraphics::InkHUD +{ + +// Color, understood by display controller IC (as bit values) +// Also suitable for use as AdafruitGFX colors +enum Color : uint8_t { + BLACK = 0, + WHITE = 1, +}; + +// Info contained within AppletFont +struct FontDimensions { + uint8_t height; + uint8_t ascenderHeight; + uint8_t descenderHeight; +}; + +// Which edge Applet::printAt will place on the X parameter +enum HorizontalAlignment : uint8_t { + LEFT, + RIGHT, + CENTER, +}; + +// Which edge Applet::printAt will place on the Y parameter +enum VerticalAlignment : uint8_t { + TOP, + MIDDLE, + BOTTOM, +}; + +// An easy-to-understand intepretation of SNR and RSSI +// Calculate with Applet::getSignalStringth +enum SignalStrength : int8_t { + SIGNAL_UNKNOWN = -1, + SIGNAL_NONE, + SIGNAL_BAD, + SIGNAL_FAIR, + SIGNAL_GOOD, +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/UpdateMediator.cpp b/src/graphics/niche/InkHUD/UpdateMediator.cpp new file mode 100644 index 000000000..16fc21cef --- /dev/null +++ b/src/graphics/niche/InkHUD/UpdateMediator.cpp @@ -0,0 +1,151 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./UpdateMediator.h" + +#include "./WindowManager.h" + +using namespace NicheGraphics; + +static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; +static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; + +InkHUD::UpdateMediator::UpdateMediator() : concurrency::OSThread("Mediator") +{ + // Timer disabled by default + OSThread::disable(); +} + +// Ask which type of update operation we should perform +// Even if we explicitly want a FAST or FULL update, we should pass it through this method, +// as it allows UpdateMediator to count the refreshes. +// Internal "maintenance" refreshes are not passed through evaluate, however. +Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::evaluate(Drivers::EInk::UpdateTypes requested) +{ + LOG_DEBUG("FULL-update debt:%f", debt); + + // For conveninece + typedef Drivers::EInk::UpdateTypes UpdateTypes; + + // Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress) + // This maintenance behavior will also halt itself when the timer next fires, + // but that could be an hour away, so we can stop it early here and free up resources + if (OSThread::enabled && debt == 0.0) + endMaintenance(); + + // Explicitly requested FULL + if (requested == UpdateTypes::FULL) { + LOG_DEBUG("Explicit FULL"); + debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt + return UpdateTypes::FULL; + } + + // Explicitly requested FAST + if (requested == UpdateTypes::FAST) { + LOG_DEBUG("Explicit FAST"); + // Add to the FULL refresh debt + if (debt < 1.0) + debt += 1.0 / fastPerFull; + else + debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes + + // If *significant debt*, begin occasionally refreshing *unprovoked* + // This maintenance behavior is only triggered here, during periods of user interaction + if (debt >= 2.0) + beginMaintenance(); + + return UpdateTypes::FAST; // Give them what the asked for + } + + // Handling UpdateTypes::UNSPECIFIED + // ----------------------------------- + // In this case, the UI doesn't care which refresh we use + + // Not much debt: suggest FAST + if (debt < 1.0) { + LOG_DEBUG("UNSPECIFIED: using FAST"); + debt += 1.0 / fastPerFull; + return UpdateTypes::FAST; + } + + // In debt: suggest FULL + else { + LOG_DEBUG("UNSPECIFIED: using FULL"); + debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt + + // When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so) + // If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh + // We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically + if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) { + LOG_DEBUG("Initial maintenance skipped"); + OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow + } + + return UpdateTypes::FULL; + } +} + +// Determine which of two update types is more important to honor +// Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness +// Explicit FULL is more important than explicint FAST - prioritize image quality: explicit FULL is rare +Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) +{ + switch (type1) { + case Drivers::EInk::UpdateTypes::UNSPECIFIED: + return type2; + + case Drivers::EInk::UpdateTypes::FAST: + return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST; + + case Drivers::EInk::UpdateTypes::FULL: + return type1; + } + + return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only +} + +// We're using the timer to perform "maintenance" +// If signifcant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked. +// This prevents gradual build-up of debt, +// in case we don't have enough UNSPECIFIED refreshes to pay the debt back organically. +// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration +// Subsequent refreshes take place *much* less frequently. +// Hopefully an applet will want to render before this, meaning we can cancel the maintenance. +int32_t InkHUD::UpdateMediator::runOnce() +{ + if (debt > 0.0) { + LOG_DEBUG("debt=%f: performing maintenance", debt); + + // Ask WindowManager to redraw everything, purely for the refresh + // Todo: optimize? Could update without re-rendering + WindowManager::getInstance()->forceUpdate(EInk::UpdateTypes::FULL); + + // Record that we have paid back (some of) the FULL refresh debt + debt = max(debt - 1.0, 0.0); + + // Next maintenance refresh scheduled - long wait (an hour?) + return MAINTENANCE_MS; + } + + else + return endMaintenance(); +} + +// Begin periodically refreshing the display, to repay FULL-refresh debt +// We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED +// After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently +// This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable +void InkHUD::UpdateMediator::beginMaintenance() +{ + LOG_DEBUG("Maintenance enabled"); + OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL); + OSThread::enabled = true; +} + +// FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates +int32_t InkHUD::UpdateMediator::endMaintenance() +{ + LOG_DEBUG("Maintenance disabled"); + return OSThread::disable(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/UpdateMediator.h b/src/graphics/niche/InkHUD/UpdateMediator.h new file mode 100644 index 000000000..e4c7c6786 --- /dev/null +++ b/src/graphics/niche/InkHUD/UpdateMediator.h @@ -0,0 +1,45 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Responsible for display health +- counts number of FULL vs FAST refresh +- suggests whether to use FAST or FULL, when not explicitly specified +- periodically requests update unprovoked, if required for display health + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/Drivers/EInk/EInk.h" + +namespace NicheGraphics::InkHUD +{ + +class UpdateMediator : protected concurrency::OSThread +{ + public: + UpdateMediator(); + + // Tell the mediator what we want, get told what we can have + Drivers::EInk::UpdateTypes evaluate(Drivers::EInk::UpdateTypes requested); + + // Determine which of two update types is more important to honor + Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2); + + uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes + float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull? + + private: + int32_t runOnce() override; + void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health + int32_t endMaintenance(); // End unprovoked refreshing: debt paid + + float debt = 0.0; // How many full refreshes are due +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp new file mode 100644 index 000000000..f987a3646 --- /dev/null +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -0,0 +1,1128 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./WindowManager.h" + +#include "RTC.h" +#include "mesh/NodeDB.h" + +// System applets +// Must be defined in .cpp to prevent a circular dependency with Applet base class +#include "./Applets/System/BatteryIcon/BatteryIconApplet.h" +#include "./Applets/System/Logo/LogoApplet.h" +#include "./Applets/System/Menu/MenuApplet.h" +#include "./Applets/System/Notification/NotificationApplet.h" +#include "./Applets/System/Pairing/PairingApplet.h" +#include "./Applets/System/Placeholder/PlaceholderApplet.h" +#include "./Applets/System/Tips/TipsApplet.h" + +using namespace NicheGraphics; + +InkHUD::WindowManager::WindowManager() : concurrency::OSThread("InkHUD WM") +{ + // Nothing for the timer to do just yet + OSThread::disable(); +} + +// Get or create the WindowManager singleton +InkHUD::WindowManager *InkHUD::WindowManager::getInstance() +{ + // Create the singleton instance of our class, if not yet done + static InkHUD::WindowManager *instance = new InkHUD::WindowManager(); + return instance; +} + +// Connect the driver, which is created independently is setupNicheGraphics() +void InkHUD::WindowManager::setDriver(Drivers::EInk *driver) +{ + // Make sure not already set + if (this->driver) { + LOG_ERROR("Driver already set"); + delay(2000); // Wait for native serial.. + assert(false); + } + + // Store the driver which was created in setupNicheGraphics() + this->driver = driver; + + // Determine the dimensions of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + imageBufferWidth = ((driver->width - 1) / 8) + 1; + imageBufferHeight = driver->height; + + // Allocate the image buffer + imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight]; +} + +// Sets the ideal ratio of FAST updates to FULL updates +// We want as many FAST updates as possible, without causing gradual degradation of the display +// If explicitly requested, number of FAST updates may exceed fastPerFull value. +// In this case, the stressMultiplier is applied, causing the "FULL update debt" to increase by more than normal +// The stressMultplier helps the display recover from particularly taxing periods of use +// (Default arguments of 5,2 are very conservative values) +void InkHUD::WindowManager::setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0) +{ + mediator.fastPerFull = fastPerFull; + mediator.stressMultiplier = stressMultiplier; +} + +// Register a user applet with the WindowManager +// This is called in setupNicheGraphics() +// This should be the only time that specific user applets are mentioned in the code +// If a user applet is not added with this method, its code should not be built +void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) +{ + userApplets.push_back(a); + + // If requested, mark in settings that this applet should be active by default + // This means that it will be available for the user to cycle to with short-press of the button + // This is the default state only: user can activate or deactive applets through the menu. + // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present + if (defaultActive) + settings.userApplets.active[userApplets.size() - 1] = true; + + // If requested, mark in settings that this applet should "autoshow" by default + // This means that the applet will be automatically brought to foreground when it has new data to show + // This is the default state only: user can select which applets have this behavior through the menu + // User's selection is stored in settings, and will be honored instead of these defaults, if present + if (defaultAutoshow) + settings.userApplets.autoshow[userApplets.size() - 1] = true; + + // If specified, mark this as the default applet for a given tile index + // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile + if (onTile != (uint8_t)-1) + settings.userTiles.displayedUserApplet[onTile] = userApplets.size() - 1; + + // The label that will be show in the applet selection menu, on the device + a->name = name; +} + +// Perform initial setup, and begin responding to incoming events +// First task once init is to show the boot screen +void InkHUD::WindowManager::begin() +{ + // Make sure we have set a driver + if (!this->driver) { + LOG_ERROR("Driver not set"); + delay(2000); // Wait for native serial.. + assert(false); + } + + loadDataFromFlash(); + + createSystemApplets(); + createSystemTiles(); + placeSystemTiles(); + assignSystemAppletsToTiles(); + + createUserApplets(); + createUserTiles(); + placeUserTiles(); + assignUserAppletsToTiles(); + refocusTile(); + + logoApplet->showBootScreen(); + forceUpdate(Drivers::EInk::FULL, false); // Update now, and wait here until complete + + deepSleepObserver.observe(¬ifyDeepSleep); + rebootObserver.observe(¬ifyReboot); + textMessageObserver.observe(textMessageModule); +#ifdef ARCH_ESP32 + lightSleepObserver.observe(¬ifyLightSleep); +#endif +} + +// Set-up special "system applets" +// These handle things like bootscreen, pop-up notifications etc +// They are processed separately from the user applets, because they might need to do "weird things" +// They also won't be activated or deactivated +void InkHUD::WindowManager::createSystemApplets() +{ + logoApplet = new LogoApplet; + pairingApplet = new PairingApplet; + tipsApplet = new TipsApplet; + notificationApplet = new NotificationApplet; + batteryIconApplet = new BatteryIconApplet; + menuApplet = new MenuApplet; + placeholderApplet = new PlaceholderApplet; + + // System applets are always active + logoApplet->activate(); + pairingApplet->activate(); + tipsApplet->activate(); + notificationApplet->activate(); + batteryIconApplet->activate(); + menuApplet->activate(); + placeholderApplet->activate(); + + // Add to the systemApplets vector + // Although system applets often need special handling, sometimes we can process them en-masse with this vector + // e.g. rendering, raising events + // Order of these entries determines Z-Index when rendering + systemApplets.push_back(logoApplet); + systemApplets.push_back(pairingApplet); + systemApplets.push_back(tipsApplet); + systemApplets.push_back(batteryIconApplet); + systemApplets.push_back(menuApplet); + systemApplets.push_back(notificationApplet); + // Note: placeholder applet is technically a system applet, but it renders in WindowManager::renderPlaceholders +} + +void InkHUD::WindowManager::createSystemTiles() +{ + fullscreenTile = new Tile; + notificationTile = new Tile; + batteryIconTile = new Tile; +} + +void InkHUD::WindowManager::placeSystemTiles() +{ + fullscreenTile->placeSystemTile(0, 0, getWidth(), getHeight()); + notificationTile->placeSystemTile(0, 0, getWidth(), 20); // Testing only: constant value + + // Todo: appropriate sizing for the battery icon + const uint16_t batteryIconHeight = Applet::getHeaderHeight() - (2 * 2); + uint16_t batteryIconWidth = batteryIconHeight * 1.8; + + batteryIconTile->placeSystemTile(getWidth() - batteryIconWidth, 2, batteryIconWidth, batteryIconHeight); +} + +// Assign a system applet to the fullscreen tile +// Rendering of user tiles is suspended when the fullscreen tile is occupied +void InkHUD::WindowManager::claimFullscreen(InkHUD::Applet *a) +{ + // Make sure that only system applets use the fullscreen tile + bool isSystemApplet = false; + for (Applet *sa : systemApplets) { + if (sa == a) { + isSystemApplet = true; + break; + } + } + assert(isSystemApplet); + + fullscreenTile->assignApplet(a); +} + +// Clear the fullscreen tile, unlinking whichever system applet is assigned +// This allows the normal rendering of user tiles to resume +void InkHUD::WindowManager::releaseFullscreen() +{ + // Make sure the applet is ready to release the tile + assert(!fullscreenTile->getAssignedApplet()->isForeground()); + + // Break the link between the applet and the fullscreen tile + fullscreenTile->assignApplet(nullptr); +} + +// Some system applets can be assigned to a tile at boot +// These are applets which do have their own tile, and whose assignment never changes +// Applets which: +// - share the fullscreen tile (e.g. logoApplet, pairingApplet), +// - render on user tiles (e.g. menuApplet, placeholderApplet), +// are assigned to the tile only when needed +void InkHUD::WindowManager::assignSystemAppletsToTiles() +{ + notificationTile->assignApplet(notificationApplet); + batteryIconTile->assignApplet(batteryIconApplet); +} + +// Activate or deactivate user applets, to match settings +// Called at boot, or after run-time config changes via menu +// Note: this method does not instantiate the applets; +// this is done in setupNicheGraphics, with WindowManager::addApplet +void InkHUD::WindowManager::createUserApplets() +{ + // Deactivate and remove any no-longer-needed applets + for (uint8_t i = 0; i < userApplets.size(); i++) { + Applet *a = userApplets.at(i); + + // If the applet is active, but settings say it shouldn't be: + // - run applet's custom deactivation code + // - mark applet as inactive (internally) + if (a->isActive() && !settings.userApplets.active[i]) + a->deactivate(); + } + + // Activate and add any new applets + for (uint8_t i = 0; i < userApplets.size() && i < MAX_USERAPPLETS_GLOBAL; i++) { + + // If not activated, but it now should be: + // - run applet's custom activation code + // - mark applet as active (internally) + if (!userApplets.at(i)->isActive() && settings.userApplets.active[i]) + userApplets.at(i)->activate(); + } +} + +void InkHUD::WindowManager::createUserTiles() +{ + // Delete any tiles which currently exist + for (Tile *t : userTiles) + delete t; + userTiles.clear(); + + // Create new tiles + for (uint8_t i = 0; i < settings.userTiles.count; i++) { + Tile *t = new Tile; + userTiles.push_back(t); + } +} + +void InkHUD::WindowManager::placeUserTiles() +{ + // Calculate the display region occupied by each tile + // This determines how pixels are translated from applet-space to windowmanager-space + for (uint8_t i = 0; i < userTiles.size(); i++) + userTiles.at(i)->placeUserTile(settings.userTiles.count, i); +} + +void InkHUD::WindowManager::assignUserAppletsToTiles() +{ + // Set "assignedApplet" property + // Which applet should be initially shown on a tile? + // This is preserved between reboots, but the value needs validating at startup + for (uint8_t i = 0; i < userTiles.size(); i++) { + Tile *t = userTiles.at(i); + + // Check whether tile can display the previously shown applet again + uint8_t oldIndex = settings.userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets + bool canRestore = true; + if (oldIndex > userApplets.size() - 1) // Check if old index is now out of bounds + canRestore = false; + else if (!settings.userApplets.active[oldIndex]) // Check that old applet is still activated + canRestore = false; + else { // Check that the old applet isn't now shown already on a different tile + for (uint8_t i2 = 0; i2 < i; i2++) { + if (settings.userTiles.displayedUserApplet[i2] == oldIndex) { + canRestore = false; + break; + } + } + } + + // Restore previously shown applet if possible, + // otherwise assign nullptr, which will render specially using placeholderApplet + if (canRestore) { + Applet *a = userApplets.at(oldIndex); + t->assignApplet(a); + a->bringToForeground(); + } else { + t->assignApplet(nullptr); + settings.userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet + } + } +} + +void InkHUD::WindowManager::refocusTile() +{ + // Validate "focused tile" setting + // - info: focused tile responds to button presses: applet cycling, menu, etc + // - if number of tiles changed, might now be out of index + if (settings.userTiles.focused >= userTiles.size()) + settings.userTiles.focused = 0; + + // Give "focused tile" a valid applet + // - scan for another valid applet, which we can addSubstitution + // - reason: nextApplet() won't cycle if no applet is assigned + Tile *focusedTile = userTiles.at(settings.userTiles.focused); + if (!focusedTile->getAssignedApplet()) { + // Search for available applets + for (uint8_t i = 0; i < userApplets.size(); i++) { + Applet *a = userApplets.at(i); + if (a->isActive() && !a->isForeground()) { + // Found a suitable applet + // Assign it to the focused tile + focusedTile->assignApplet(a); + a->bringToForeground(); + settings.userTiles.displayedUserApplet[settings.userTiles.focused] = i; // Record change: persist after reboot + break; + } + } + } +} + +// Callback for deepSleepObserver +// Returns 0 to signal that we agree to sleep now +int InkHUD::WindowManager::beforeDeepSleep(void *unused) +{ + // Notify all applets that we're shutting down + for (Applet *ua : userApplets) { + ua->onDeactivate(); + ua->onShutdown(); + } + for (Applet *sa : userApplets) { + // Note: no onDeactivate. System applets are always active. + sa->onShutdown(); + } + + // User has successfull executed a safe shutdown + // We don't need to nag at boot anymore + settings.tips.safeShutdownSeen = true; + + saveDataToFlash(); + + // Display the shutdown screen, and wait here until the update is complete + logoApplet->showShutdownScreen(); + forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + + return 0; // We agree: deep sleep now +} + +// Callback for rebootObserver +// Same as shutdown, without drawing the logoApplet +// Makes sure we don't lose message history / InkHUD config +int InkHUD::WindowManager::beforeReboot(void *unused) +{ + + // Notify all applets that we're "shutting down" + // They don't need to know that it's really a reboot + for (Applet *a : userApplets) { + a->onDeactivate(); + a->onShutdown(); + } + for (Applet *sa : userApplets) { + // Note: no onDeactivate. System applets are always active. + sa->onShutdown(); + } + + saveDataToFlash(); + + return 0; // No special status to report. Ignored anyway by this Observable +} + +#ifdef ARCH_ESP32 +// Callback for lightSleepObserver +// Make sure the display is not partway through an update when we begin light sleep +// This is because some displays require active input from us to terminate the update process, and protect the panel hardware +int InkHUD::WindowManager::beforeLightSleep(void *unused) +{ + if (driver->busy()) { + LOG_INFO("Waiting for display"); + driver->await(); // Wait here for update to complete + } + + return 0; // No special status to report. Ignored anyway by this Observable +} +#endif + +// Callback when a new text message is received +// Caches the most recently received message, for use by applets +// Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc. +// Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message +int InkHUD::WindowManager::onReceiveTextMessage(const meshtastic_MeshPacket *packet) +{ + // Short circuit: don't store outgoing messages + if (getFrom(packet) == nodeDB->getNodeNum()) + return 0; + + // Short circuit: don't store "emoji reactions" + // Possibly some implemetation 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 + latestMessage.wasBroadcast = isBroadcast(packet->to); + + // Pick the appropriate variable to store the message in + MessageStore::Message *storedMessage = latestMessage.wasBroadcast ? &latestMessage.broadcast : &latestMessage.dm; + + // Store nodenum of the sender + // Applets can use this to fetch user data from nodedb, if they want + storedMessage->sender = packet->from; + + // Store the time (epoch seconds) when message received + storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + + // Store the channel + // - (potentially) used to determine whether notification shows + // - (potentially) used to determine which applet to focus + storedMessage->channelIndex = packet->channel; + + // Store the text + // Need to specify manually how many bytes, because source not null-terminated + storedMessage->text = + std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); + + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) +} + +// Triggered by an input source when a short-press fires +// The input source is a separate component; not part of InkHUD +// It is connected in setupNicheGraphics() +void InkHUD::WindowManager::handleButtonShort() +{ + // If notification is open: close it + if (notificationApplet->isForeground()) { + notificationApplet->dismiss(); + forceUpdate(EInk::UpdateTypes::FULL); // Redraw everything, to clear the notification + } + + // If window manager is locked: lock owner handles button + else if (lockOwner) + lockOwner->onButtonShortPress(); + + // Normally: next applet + else + nextApplet(); +} + +// Triggered by an input source when a long-press fires +// The input source is a separate component; not part of InkHUD +// It is connected in setupNicheGraphics() +// Note: input source should raise this while button still held +void InkHUD::WindowManager::handleButtonLong() +{ + if (lockOwner) + lockOwner->onButtonLongPress(); + + else + menuApplet->show(userTiles.at(settings.userTiles.focused)); +} + +// On the currently focussed tile: cycle to the next available user applet +// Applets available for this must be activated, and not already displayed on another tile +void InkHUD::WindowManager::nextApplet() +{ + Tile *t = userTiles.at(settings.userTiles.focused); + + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; + + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < userApplets.size(); i++) { + if (userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; + } + } + + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); + + // Iterate forward through the WindowManager::applets, looking for the next valid applet + Applet *nextValidApplet = nullptr; + // for (uint8_t i = (appletIndex + 1) % applets.size(); i != appletIndex; i = (i + 1) % applets.size()) { + for (uint8_t i = 1; i < userApplets.size(); i++) { + uint8_t newAppletIndex = (appletIndex + i) % userApplets.size(); + Applet *a = userApplets.at(newAppletIndex); + + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + nextValidApplet = a; + settings.userTiles.displayedUserApplet[settings.userTiles.focused] = + newAppletIndex; // Remember this setting between boots! + break; + } + } + + // Confirm that we found another applet + if (!nextValidApplet) + return; + + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(nextValidApplet); + nextValidApplet->bringToForeground(); + forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST +} + +// Focus on a different tile +// The "focused tile" is the one which cycles applets on user button press, +// and the one where the menu will be displayed +// Note: this method is only used by an aux button +// The menuApplet manually performs a subset of these actions, to avoid disturbing the stale image on adjacent tiles +void InkHUD::WindowManager::nextTile() +{ + // Close the menu applet if open + // We done *really* want to do this, but it simplifies handling *a lot* + if (menuApplet->isForeground()) + menuApplet->sendToBackground(); + + // Seems like some system applet other than menu is open. Pairing? Booting? + if (!canRequestUpdate()) + return; + + // Swap to next tile + settings.userTiles.focused = (settings.userTiles.focused + 1) % settings.userTiles.count; + + // Make sure that we don't get stuck on the placeholder tile + // changeLayout reassigns applets to tiles + changeLayout(); + + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + userTiles.at(settings.userTiles.focused)->requestHighlight(); +} + +// Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time +// Call after changing settings.tiles.count +void InkHUD::WindowManager::changeLayout() +{ + // Recreate tiles + // - correct number created, from settings.userTiles.count + // - set dimension and position of tiles, according to layout + createUserTiles(); + placeUserTiles(); + placeSystemTiles(); + + // Handle fewer tiles + // - background any applets which have lost their tile + findOrphanApplets(); + + // Handle more tiles + // - create extra applets + // - assign them to the new extra tiles + createUserApplets(); + assignUserAppletsToTiles(); + + // Focus a valid tile + // - info: focused tile is the one which cycles applets when user button pressed + // - may now be out of bounds if tile count has decreased + refocusTile(); + + // Restore menu + // - its tile was just destroyed and recreated (createUserTiles) + // - its assignment was cleared (assignUserAppletsToTiles) + if (menuApplet->isForeground()) { + Tile *ft = userTiles.at(settings.userTiles.focused); + menuApplet->show(ft); + } + + // Force-render + // - redraw all applets + forceUpdate(EInk::UpdateTypes::FAST); +} + +// Perform necessary reconfiguration when user activates or deactivates applets at run-time +// Call after changing settings.userApplets.active +void InkHUD::WindowManager::changeActivatedApplets() +{ + assert(menuApplet->isForeground()); + + // Activate or deactivate applets + // - to match value of settings.userApplets.active + createUserApplets(); + + // Assign the placeholder applet + // - if applet was foreground on a tile when deactivated, swap it with a placeholder + // - placeholder applet may be assigned to multiple tiles, if needed + assignUserAppletsToTiles(); + + // Ensure focused tile has a valid applet + // - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder + // - reason: nextApplet() won't cycle applets if placeholder is shown + refocusTile(); + + // Restore menu + // - its assignment was cleared (assignUserAppletsToTiles) + if (menuApplet->isForeground()) { + Tile *ft = userTiles.at(settings.userTiles.focused); + menuApplet->show(ft); + } + + // Force-render + // - redraw all applets + forceUpdate(EInk::UpdateTypes::FAST); +} + +// Change whether the battery icon is displayed (top left corner) +// Don't toggle the OptionalFeatures value before calling this, our method handles it internally +void InkHUD::WindowManager::toggleBatteryIcon() +{ + assert(batteryIconApplet->isActive()); + settings.optionalFeatures.batteryIcon = !settings.optionalFeatures.batteryIcon; // Preserve the change between boots + + // Show or hide the applet + if (settings.optionalFeatures.batteryIcon) + batteryIconApplet->bringToForeground(); + else + batteryIconApplet->sendToBackground(); + + // Force-render + // - redraw all applets + forceUpdate(EInk::UpdateTypes::FAST); +} + +// Allow applets to suppress notifications +// Applets will be asked whether they approve, before a notification is shown via the NotificationApplet +// An applet might want to suppress a notification if the applet itself already displays this info +// Example: AllMessageApplet should not approve notifications for messages, if it is in foreground +bool InkHUD::WindowManager::approveNotification(InkHUD::Notification &n) +{ + // Ask all currently displayed applets + for (Tile *ut : userTiles) { + Applet *ua = ut->getAssignedApplet(); + if (ua && !ua->approveNotification(n)) + return false; + } + + // Nobody objected + return true; +} + +// Set a flag, which will be picked up by runOnce, ASAP. +// Quite likely, multiple applets will all want to respond to one event (Observable, etc) +// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce +void InkHUD::WindowManager::requestUpdate() +{ + requestingUpdate = true; + + // We will run the thread as soon as we loop(), + // after all Applets have had a chance to observe whatever event set this off + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; +} + +// requestUpdate will not actually update if no requests were made by applets which are actually visible +// This can occur, because applets requestUpdate even from the background, +// in case the user's autoshow settings permit them to be moved to foreground. +// Sometimes, however, we will want to trigger a display update manually, in the absense of any sort of applet event +// Display health, for example. +// In these situations, we use forceUpdate +void InkHUD::WindowManager::forceUpdate(EInk::UpdateTypes type, bool async) +{ + requestingUpdate = true; + forcingUpdate = true; + forcedUpdateType = type; + + // Normally, we need to start the timer, in case the display is busy and we briefly defer the update + if (async) { + // We will run the thread as soon as we loop(), + // after all Applets have had a chance to observe whatever event set this off + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; + } + + // If the update is *not* asynchronous, we begin the render process directly here + // so that it can block code flow while running + else + render(false); +} + +// Receives rendered image data from an Applet, via a tile +// When applets render, they output pixel data relative to their own left / top edges +// They pass this pixel data to tile, which offsets the pixels, making them relative to the display left / top edges +// That data is then passed to this method, which applies any rotation, then places the pixels into the image buffer +// That image buffer is the fully-formatted data handed off to the driver +void InkHUD::WindowManager::handleTilePixel(int16_t x, int16_t y, Color c) +{ + rotatePixelCoords(&x, &y); + setBufferPixel(x, y, c); +} + +// Width of the display, relative to rotation +uint16_t InkHUD::WindowManager::getWidth() +{ + if (settings.rotation % 2) + return driver->height; + else + return driver->width; +} + +// Height of the display, relative to rotation +uint16_t InkHUD::WindowManager::getHeight() +{ + if (settings.rotation % 2) + return driver->width; + else + return driver->height; +} + +// How many user applets have been built? Includes applets which have been inactivated by user config +uint8_t InkHUD::WindowManager::getAppletCount() +{ + return userApplets.size(); +} + +// A tidy title for applets: used on-display in some situations +// Index is the order in the WindowManager::userApplets vector +// This is the same order that applets were added in setupNicheGraphics +const char *InkHUD::WindowManager::getAppletName(uint8_t index) +{ + return userApplets.at(index)->name; +} + +// Allows a system applet to prevent other applets from temporarily requesting updates +// All user applets will honor this. Some system applets might not, although they probably should +// WindowManager::forceUpdate will ignore this lock +void InkHUD::WindowManager::lock(Applet *owner) +{ + // Only one system applet may lock render at once + assert(!lockOwner); + + // Only system applets may lock rendering + for (Applet *a : userApplets) + assert(owner != a); + + lockOwner = owner; +} + +// Remove a lock placed by a system applet, which prevents other applets from rendering +void InkHUD::WindowManager::unlock(Applet *owner) +{ + assert(lockOwner = owner); + lockOwner = nullptr; + + // Raise this as an event (system applets only) + // - in case applet waiting for lock + // - in case applet relinquished its lock earlier, and wants it back + for (Applet *sa : systemApplets) { + // Don't raise event for the applet which is calling unlock + // - avoid loop of unlock->lock (some implementations of Applet::onLockAvailable) + if (sa != owner) + sa->onLockAvailable(); + } +} + +// Is an applet blocked from requesting update by a current lock? +// Applets are allowed to request updates if there is no lock, or if they are the owner of the lock +// If a == nullptr, checks permission "for everyone and anyone" +bool InkHUD::WindowManager::canRequestUpdate(Applet *a) +{ + if (!lockOwner) + return true; + else if (lockOwner == a) + return true; + else + return false; +} + +// Get the applet which is currently locking rendering +// We might be able to convince it release its lock, if we want it instead +InkHUD::Applet *InkHUD::WindowManager::whoLocked() +{ + return WindowManager::lockOwner; +} + +// Runs at regular intervals +// WindowManager's uses of this include: +// - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render +// - queuing another render: while one is already is progress +int32_t InkHUD::WindowManager::runOnce() +{ + // If an applet asked to render, and hardware is able, lets try now + if (requestingUpdate && !driver->busy()) { + render(); + } + + // If our render() call failed, try again shortly + // otherwise, stop our thread until next update due + if (requestingUpdate) + return 250UL; + else + return OSThread::disable(); +} + +// Some applets may be permitted to bring themselved to foreground, to show new data +// User selects which applets have this permission via on-screen menu +// Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics +// We will only autoshow one applet +void InkHUD::WindowManager::autoshow() +{ + for (uint8_t i = 0; i < userApplets.size(); i++) { + Applet *a = userApplets.at(i); + if (a->wantsToAutoshow() // Applet wants to become foreground + && !a->isForeground() // Not yet foreground + && settings.userApplets.autoshow[i] // User permits this applet to autoshow + && canRequestUpdate()) // Updates not currently blocked by system applet + { + Tile *t = userTiles.at(settings.userTiles.focused); // Get focused tile + t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile + t->assignApplet(a); // Assign our new applet to tile + a->bringToForeground(); // Foreground our new applet + + // Check if autoshown applet shows the same information as notification intended to + // In this case, we can dismiss the notification before it is shown + // Note: we are re-running the approval process. This normally occurs when the notification is initially triggered. + if (notificationApplet->isForeground() && !notificationApplet->isApproved()) + notificationApplet->dismiss(); + + break; // One autoshow only! Avoid conflicts + } + } +} + +// Check whether an update is justified +// We usually require that a foreground applet requested the update, +// but forceUpdate call will bypass these checks. +// Abstraction for WindowManager::render only +bool InkHUD::WindowManager::shouldUpdate() +{ + bool should = false; + + // via forceUpdate + should |= forcingUpdate; + + // via user applet + for (Tile *ut : userTiles) { + Applet *ua = ut->getAssignedApplet(); + if (ua // Tile has valid applet + && ua->wantsToRender() // This applet requested display update + && ua->isForeground() // This applet is currently shown + && canRequestUpdate()) // Requests are not currently locked + { + should = true; + break; + } + } + + // via system applet + for (Applet *sa : systemApplets) { + if (sa->wantsToRender() // This applet requested + && sa->isForeground() // This applet is currently shown + && canRequestUpdate(sa)) // Requests are not currently locked, or this applet owns the lock + { + should = true; + break; + } + } + + return should; +} + +// Determine which type of E-Ink update the display will perform, to change the image. +// Considers the needs of the various applets, then weighs against display health. +// An update type specified by forceUpdate will be granted with no further questioning. +// Abstraction for WindowManager::render only +Drivers::EInk::UpdateTypes InkHUD::WindowManager::selectUpdateType() +{ + // Ask applets which update type they would prefer + // Some update types take priority over others + EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED; + if (forcingUpdate) { + // Update type was manually specified via forceUpdate + type = forcedUpdateType; + } else { + // User applets + for (Tile *ut : userTiles) { + Applet *ua = ut->getAssignedApplet(); + if (ua && ua->isForeground() && canRequestUpdate()) + type = mediator.prioritize(type, ua->wantsUpdateType()); + } + // System Applets + for (Applet *sa : systemApplets) { + if (sa->isForeground() && canRequestUpdate(sa)) + type = mediator.prioritize(type, sa->wantsUpdateType()); + } + } + + // Tell the mediator what update type the applets deciced on, + // find out what update type the mediator will actually allow us to have + type = mediator.evaluate(type); + + return type; +} + +// Run the drawing operations of any user applets which are currently displayed +// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver +// Abstraction for WindowManager::render only +void InkHUD::WindowManager::renderUserApplets() +{ + // Don't render any user applets if the screen is covered by a system applet using the fullscreen tile + if (fullscreenTile->getAssignedApplet()) + return; + + // For each tile + for (Tile *ut : userTiles) { + Applet *ua = ut->getAssignedApplet(); // Get the applet on the tile + + // Don't render if tile has no applet. Handled in renderPlaceholders + if (!ua) + continue; + + // Don't render the menu applet, Handled by renderSystemApplets + if (ua == menuApplet) + continue; + + uint32_t start = millis(); + ua->render(); // Draw! + uint32_t stop = millis(); + LOG_DEBUG("%s took %dms to render", ua->name, stop - start); + } +} + +// Run the drawing operations of any system applets which are currently displayed +// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver +// Abstraction for WindowManager::render only +void InkHUD::WindowManager::renderSystemApplets() +{ + // Each system applet + for (Applet *sa : systemApplets) { + // Skip if not shown + if (!sa->isForeground()) + continue; + + // Don't draw the battery overtop the menu + // Todo: smarter way to handle this + if (sa == batteryIconApplet && menuApplet->isForeground()) + continue; + + // Skip applet if fullscreen tile is in use, but not used by this applet + // Applet is "obscured" + if (fullscreenTile->getAssignedApplet() && fullscreenTile->getAssignedApplet() != sa) + continue; + + // uint32_t start = millis(); // Debugging only: runtime + sa->render(); // Draw! + // uint32_t stop = millis(); // Debugging only: runtime + // LOG_DEBUG("%s (system) took %dms to render", (sa->name == nullptr) ? "Unnamed" : sa->name, stop - start); + } +} + +// In some situations (e.g. layout or applet selection changes), +// a user tile can end up without an assigned applet. +// In this case, we will fill the empty space with diagonal lines. +void InkHUD::WindowManager::renderPlaceholders() +{ + // Don't draw if obscured by the fullscreen tile + if (fullscreenTile->getAssignedApplet()) + return; + + for (Tile *ut : userTiles) { + // If no applet assigned + if (!ut->getAssignedApplet()) { + ut->assignApplet(placeholderApplet); + placeholderApplet->render(); + ut->assignApplet(nullptr); + } + } +} + +// Make an attempt to gather image data from some / all applets, and update the display +// Might not be possible right now, if update already is progress. +void InkHUD::WindowManager::render(bool async) +{ + // Make sure the display is ready for a new update + if (async) { + // Previous update still running, Will try again shortly, via runOnce() + if (driver->busy()) + return; + } else { + // Wait here for previous update to complete + driver->await(); + } + + // (Potentially) change applet to display new info, + // then check if this newly displayed applet makes a pending notification redundant + autoshow(); + + // If an update is justified. + // We don't know this until after autoshow has run, as new applets may now be in foreground + if (shouldUpdate()) { + + // Decide which technique the display will use to change image + EInk::UpdateTypes updateType = selectUpdateType(); + + // Render the new image + clearBuffer(); + renderUserApplets(); + renderSystemApplets(); + renderPlaceholders(); + + // Tell display to begin process of drawing new image + LOG_INFO("Updating display"); + driver->update(imageBuffer, updateType); + + // If not async, wait here until the update is complete + if (!async) + driver->await(); + } else + LOG_DEBUG("Not updating display"); + + // Our part is done now. + // If update is async, the display hardware is still performing the update process, + // but that's all handled by NicheGraphics::Drivers::EInk + + // Tidy up, ready for a new request + requestingUpdate = false; + forcingUpdate = false; + forcedUpdateType = EInk::UpdateTypes::UNSPECIFIED; +} + +// Set a ready-to-draw pixel into the image buffer +// All rotations / translations have already taken place: this buffer data is formatted ready for the driver +void InkHUD::WindowManager::setBufferPixel(int16_t x, int16_t y, Color c) +{ + uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte + uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte. + + bitWrite(imageBuffer[byteNum], bitNum, c); +} + +// Applies the system-wide rotation to pixel positions +// This step is applied to image data which has already been translated by a Tile object +// This is the final step before the pixel is placed into the image buffer +// No return: values of the *x and *y parameters are modified by the method +void InkHUD::WindowManager::rotatePixelCoords(int16_t *x, int16_t *y) +{ + // Apply a global rotation to pixel locations + int16_t x1 = 0; + int16_t y1 = 0; + switch (settings.rotation) { + case 0: + x1 = *x; + y1 = *y; + break; + case 1: + x1 = (driver->width - 1) - *y; + y1 = *x; + break; + case 2: + x1 = (driver->width - 1) - *x; + y1 = (driver->height - 1) - *y; + break; + case 3: + x1 = *y; + y1 = (driver->height - 1) - *x; + break; + } + *x = x1; + *y = y1; +} + +// Manually fill the image buffer with WHITE +// Clears any old drawing +void InkHUD::WindowManager::clearBuffer() +{ + memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); +} + +// Seach for any applets which believe they are foreground, but no longer have a valid tile +// Tidies up after layout changes at runtime +void InkHUD::WindowManager::findOrphanApplets() +{ + for (uint8_t ia = 0; ia < userApplets.size(); ia++) { + Applet *a = userApplets.at(ia); + + // Applet doesn't believe it is displayed: not orphaned + if (!a->isForeground()) + continue; + + // Check each tile, to see if anyone claims this applet + bool foundOwner = false; + for (uint8_t it = 0; it < userTiles.size(); it++) { + Tile *t = userTiles.at(it); + // A tile claims this applet: not orphaned + if (t->getAssignedApplet() == a) { + foundOwner = true; + break; + } + } + + // Orphan found + // Tell the applet that no tile is currently displaying it + // This allows the focussed tile to cycle to this applet again by pressing user button + if (!foundOwner) + a->sendToBackground(); + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h new file mode 100644 index 000000000..f701233e2 --- /dev/null +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -0,0 +1,177 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Singleton class, which manages the broadest InkHUD behaviors + + Tasks include: + - containing instances of Tiles and Applets + - co-ordinating display updates + - interacting with other NicheGraphics componets, such as the driver, and input sources + - handling system-wide events (e.g. shutdown) + +*/ + +#pragma once + +#include "configuration.h" + +#include + +#include "main.h" +#include "modules/TextMessageModule.h" +#include "power.h" +#include "sleep.h" + +#include "./Applet.h" +#include "./Applets/System/Notification/Notification.h" +#include "./Persistence.h" +#include "./Tile.h" +#include "./Types.h" +#include "./UpdateMediator.h" +#include "graphics/niche/Drivers/EInk/EInk.h" + +namespace NicheGraphics::InkHUD +{ + +class Applet; +class Tile; + +class LogoApplet; +class MenuApplet; +class NotificationApplet; + +class WindowManager : protected concurrency::OSThread +{ + public: + static WindowManager *getInstance(); // Get or create singleton instance + + void setDriver(NicheGraphics::Drivers::EInk *driver); // Assign a driver class + void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); // How many FAST updates before FULL + void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, + uint8_t onTile = -1); // Select which applets are used with InkHUD + void begin(); // Start running the window manager (provisioning done) + + void createSystemApplets(); // Instantiate and activate system applets + void createSystemTiles(); // Instantiate tiles which host system applets + void assignSystemAppletsToTiles(); + void placeSystemTiles(); // Set position and size + void claimFullscreen(Applet *sa); // Assign a system applet to the fullscreen tile + void releaseFullscreen(); // Remove any system applet from the fullscreen tile + + void createUserApplets(); // Activate user's selected applets + void createUserTiles(); // Instantiate enough tiles for user's selected layout + void assignUserAppletsToTiles(); + void placeUserTiles(); // Automatically place tiles, according to user's layout + void refocusTile(); // Ensure focused tile has a valid applet + + 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 +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); // Prepare for light sleep +#endif + + void handleButtonShort(); // User button: short press + void handleButtonLong(); // User button: long press + + void nextApplet(); // Cycle through user applets + void nextTile(); // Focus the next tile (when showing multiple applets at once) + + void changeLayout(); // Change tile layout or count + void changeActivatedApplets(); // Change which applets are activated + void toggleBatteryIcon(); // Change whether the battery icon is shown + bool approveNotification(Notification &n); // Ask applets if a notification is worth showing + + void handleTilePixel(int16_t x, int16_t y, Color c); // Apply rotation, then store the pixel in framebuffer + void requestUpdate(); // Update display, if a foreground applet has info it wants to show + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, + bool async = true); // Update display, regardless of whether any applets requested this + + uint16_t getWidth(); // Display width, relative to rotation + uint16_t getHeight(); // Display height, relative to rotation + uint8_t getAppletCount(); // How many user applets are available, including inactivated + const char *getAppletName(uint8_t index); // By order in userApplets + + void lock(Applet *owner); // Allows system applets to prevent other applets triggering a refresh + void unlock(Applet *owner); // Allows normal updating of user applets to continue + bool canRequestUpdate(Applet *a = nullptr); // Checks if allowed to request an update (not locked by other applet) + Applet *whoLocked(); // Find which applet is blocking update requests, if any + + protected: + WindowManager(); // Private constructor for singleton + + int32_t runOnce() override; + + void clearBuffer(); // Empty the framebuffer + void autoshow(); // Show a different applet, to display new info + bool shouldUpdate(); // Check if reason to change display image + Drivers::EInk::UpdateTypes selectUpdateType(); // Determine how the display hardware will perform the image update + void renderUserApplets(); // Draw all currently displayed user applets to the frame buffer + void renderSystemApplets(); // Draw all currently displayed system applets to the frame buffer + void renderPlaceholders(); // Draw diagonal lines on user tiles which have no assigned applet + void render(bool async = true); // Attempt to update the display + + void setBufferPixel(int16_t x, int16_t y, Color c); // Place pixels into the frame buffer. All translation / rotation done. + void rotatePixelCoords(int16_t *x, int16_t *y); // Apply the display rotation + + void findOrphanApplets(); // Find any applets left-behind when layout changes + + // Get notified when the system is shutting down + CallbackObserver deepSleepObserver = + CallbackObserver(this, &WindowManager::beforeDeepSleep); + + // Get notified when the system is rebooting + CallbackObserver rebootObserver = + CallbackObserver(this, &WindowManager::beforeReboot); + + // Cache *incoming* text messages, for use by applets + CallbackObserver textMessageObserver = + CallbackObserver(this, &WindowManager::onReceiveTextMessage); + +#ifdef ARCH_ESP32 + // Get notified when the system is entering light sleep + CallbackObserver lightSleepObserver = + CallbackObserver(this, &WindowManager::beforeLightSleep); +#endif + + NicheGraphics::Drivers::EInk *driver = nullptr; + uint8_t *imageBuffer; // Fed into driver + uint16_t imageBufferHeight; + uint16_t imageBufferWidth; + uint32_t imageBufferSize; // Bytes + + // Encapsulates decision making about E-Ink update types + // Responsible for display health + UpdateMediator mediator; + + // User Applets + std::vector userApplets; + std::vector userTiles; + + // System Applets + std::vector systemApplets; + Tile *fullscreenTile = nullptr; + Tile *notificationTile = nullptr; + Tile *batteryIconTile = nullptr; + LogoApplet *logoApplet; + Applet *pairingApplet; + Applet *tipsApplet; + NotificationApplet *notificationApplet; + Applet *batteryIconApplet; + MenuApplet *menuApplet; + Applet *placeholderApplet; + + // requestUpdate + bool requestingUpdate = false; // WindowManager::render run pending + + // forceUpdate + bool forcingUpdate = false; // WindowManager::render run pending, guaranteed no skip of update + Drivers::EInk::UpdateTypes forcedUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // guaranteed update using this type + + Applet *lockOwner = nullptr; // Which system applet (if any) is preventing other applets from requesting update +}; + +}; // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Inputs/README.md b/src/graphics/niche/Inputs/README.md new file mode 100644 index 000000000..767352881 --- /dev/null +++ b/src/graphics/niche/Inputs/README.md @@ -0,0 +1,7 @@ +# NiceGraphics - Inputs + +General purpose input sources, for use with NicheGraphics UIs. + +By remaining independent, we can have tailored input sources with further complicating the code in ButtonThread and the canned messages module. + +Depending on its role, a NicheGraphics UI may or may not want to make use of the existing input broker. diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp new file mode 100644 index 000000000..e478364cc --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -0,0 +1,272 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./TwoButton.h" + +#include "PowerFSM.h" +#include "sleep.h" + +using namespace NicheGraphics::Inputs; + +TwoButton::TwoButton() : concurrency::OSThread("TwoButton") +{ + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); + +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif +} + +// Get access to (or create) the singleton instance of this class +// Accessible inside the ISRs, even though we maybe shouldn't +TwoButton *TwoButton::getInstance() +{ + // Instantiate the class the first time this method is called + static TwoButton *const singletonInstance = new TwoButton; + + return singletonInstance; +} + +// Begin receiving button input +// We probably need to do this after sleep, as well as at boot +void TwoButton::start() +{ + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); + + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); +} + +// Stop receiving button input, and run custom sleep code +// Called before device sleeps. This might be power-off, or just ESP32 light sleep +// Some devices will want to attach interrupts here, for the user button to wake from sleep +void TwoButton::stop() +{ + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); + + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].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) +{ + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; + buttons[whichButton].mode = internalPullup ? INPUT_PULLUP : INPUT; // fix me + + pinMode(buttons[whichButton].pin, buttons[whichButton].mode); +} + +void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) +{ + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; +} + +// Set what should happen when a button becomes pressed +// Use this to implement a "while held" behavior +void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) +{ + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; +} + +// Set what should happen when a button becomes unpressed +// Use this to implement a "While held" behavior +void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) +{ + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; +} + +// Set what should happen when a "short press" event has occurred +void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) +{ + assert(whichButton < 2); + buttons[whichButton].onShortPress = onShortPress; +} + +// Set what should happen when a "long press" event has fired +// Note: this will occur while the button is still held +void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) +{ + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; +} + +// Handle the start of a press to the primary button +// Wakes our button thread +void TwoButton::isrPrimary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButton *b = TwoButton::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the secondary button +// Wakes our button thread +void TwoButton::isrSecondary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButton *b = TwoButton::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Concise method to start our button thread +// Follows an ISR, listening for button release +void TwoButton::startThread() +{ + if (!OSThread::enabled) { + OSThread::setInterval(50); + OSThread::enabled = true; + } +} + +// Concise method to stop our button thread +// Called when we no longer need to poll for button release +void TwoButton::stopThread() +{ + if (OSThread::enabled) { + OSThread::disable(); + } + + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; +} + +// Our button thread +// Started by an IRQ, on either button +// Polls for button releases +// Stops when both buttons released +int32_t TwoButton::runOnce() +{ + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); + + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; + + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Inform that press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Inform that press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) + buttons[i].onShortPress(); + } + + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Raise a long press event, once + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); + } + } + break; + } + + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Possible release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // If both buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); + + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; +} + +#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 TwoButton::beforeLightSleep(void *unused) +{ + stop(); + 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 TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + start(); + + // 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) + isrPrimary(); + + return 0; // Indicates success +} + +#endif + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Inputs/TwoButton.h b/src/graphics/niche/Inputs/TwoButton.h new file mode 100644 index 000000000..1e1576256 --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButton.h @@ -0,0 +1,103 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics input source + +Short and Long press for up to two buttons +Interrupt driven + +*/ + +#pragma once + +#include "configuration.h" + +#include "assert.h" +#include "functional" + +#ifdef ARCH_ESP32 +#include "esp_sleep.h" // For light-sleep handling +#endif + +#include "Observer.h" + +namespace NicheGraphics::Inputs +{ + +class TwoButton : protected concurrency::OSThread +{ + public: + typedef std::function Callback; + + 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 setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + + private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; + + // Contains info about a specific button + // (Array of this struct below) + class Button + { + 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 + 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 + volatile uint32_t irqAtMillis; // millis() when button went down + + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onShortPress = noop; + std::function onLongPress = noop; + }; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &TwoButton::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButton::afterLightSleep); +#endif + + int32_t runOnce() override; // Timer method. Polls for button release + + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release + + static void isrPrimary(); // Detect start of press + static void isrSecondary(); // Detect start of press (optional aux button) + + TwoButton(); // Constructor made private: force use of Button::instance() + + // Info about both buttons + Button buttons[2]; +}; + +}; // namespace NicheGraphics::Inputs + +#endif \ No newline at end of file diff --git a/src/graphics/niche/README.md b/src/graphics/niche/README.md new file mode 100644 index 000000000..e87464abc --- /dev/null +++ b/src/graphics/niche/README.md @@ -0,0 +1,15 @@ +# NicheGraphics + +A pattern / collection of resources for creating custom UIs, to target small groups of devices which have specific design requirements. + +For an example, see the `heltec-vision-master-e290-inkhud` platformio env. + +- platformio.ini + + - suppress default Meshtastic components (Screen, ButtonThread, etc) + - define `MESHTASTIC_INCLUDE_NICHE_GRAPHICS` + - (possibly) Edit `build_src_filter` to include our new nicheGraphics.h file + +- nicheGraphics.h + - `#include` all necessary components + - perform all setup and config inside a `setupNicheGraphics()` method diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp new file mode 100644 index 000000000..c31659c62 --- /dev/null +++ b/src/graphics/tftSetup.cpp @@ -0,0 +1,126 @@ +#if HAS_TFT + +#include "SPILock.h" +#include "sleep.h" + +#include "api/PacketAPI.h" +#include "comms/PacketClient.h" +#include "comms/PacketServer.h" +#include "graphics/DeviceScreen.h" +#include "graphics/driver/DisplayDriverConfig.h" + +#ifdef ARCH_PORTDUINO +#include "PortduinoGlue.h" +#endif + +DeviceScreen *deviceScreen = nullptr; + +#ifdef ARCH_ESP32 +// Get notified when the system is entering light sleep +CallbackObserver tftSleepObserver = + CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); +CallbackObserver endSleepObserver = + CallbackObserver(deviceScreen, &DeviceScreen::wakeUp); +#endif + +void tft_task_handler(void *param = nullptr) +{ + while (true) { + if (deviceScreen) { + spiLock->lock(); + deviceScreen->task_handler(); + spiLock->unlock(); + deviceScreen->sleep(); + } + } +} + +void tftSetup(void) +{ +#ifndef ARCH_PORTDUINO + deviceScreen = &DeviceScreen::create(); + PacketAPI::create(PacketServer::init()); + deviceScreen->init(new PacketClient); +#else + if (settingsMap[displayPanel] != no_screen) { + DisplayDriverConfig displayConfig; + static char *panels[] = {"NOSCREEN", "X11", "ST7789", "ST7735", "ST7735S", "ST7796", + "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; + static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; +#ifdef USE_X11 + if (settingsMap[displayPanel] == x11) { + if (settingsMap[displayWidth] && settingsMap[displayHeight]) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth], + (uint16_t)settingsMap[displayHeight]); + else + displayConfig.device(DisplayDriverConfig::device_t::X11); + } else +#endif + { + displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) + .panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]], + .panel_width = (uint16_t)settingsMap[displayWidth], + .panel_height = (uint16_t)settingsMap[displayHeight], + .rotation = (bool)settingsMap[displayRotate], + .pin_cs = (int16_t)settingsMap[displayCS], + .pin_rst = (int16_t)settingsMap[displayReset], + .offset_x = (uint16_t)settingsMap[displayOffsetX], + .offset_y = (uint16_t)settingsMap[displayOffsetY], + .offset_rotation = (uint8_t)settingsMap[displayOffsetRotate], + .invert = settingsMap[displayInvert] ? true : false, + .rgb_order = (bool)settingsMap[displayRGBOrder], + .dlen_16bit = settingsMap[displayPanel] == ili9486 || + settingsMap[displayPanel] == ili9488}) + .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency], + .freq_read = 16000000, + .spi{.pin_dc = (int8_t)settingsMap[displayDC], + .use_lock = true, + .spi_host = (uint16_t)settingsMap[displayspidev]}}) + .input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice], + .pointerDevice = settingsStrings[pointerDevice]}) + .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight], + .pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel], + .invert = (bool)settingsMap[displayBacklightInvert]}); + if (settingsMap[touchscreenI2CAddr] == -1) { + displayConfig.touch( + DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]], + .freq = (uint32_t)settingsMap[touchscreenBusFrequency], + .pin_int = (int16_t)settingsMap[touchscreenIRQ], + .offset_rotation = (uint8_t)settingsMap[touchscreenRotate], + .spi{ + .spi_host = (int8_t)settingsMap[touchscreenspidev], + }, + .pin_cs = (int16_t)settingsMap[touchscreenCS]}); + } else { + displayConfig.touch(DisplayDriverConfig::touch_config_t{ + .type = touch[settingsMap[touchscreenModule]], + .freq = (uint32_t)settingsMap[touchscreenBusFrequency], + .x_min = 0, + .x_max = + (int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) - + 1), + .y_min = 0, + .y_max = + (int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) - + 1), + .pin_int = (int16_t)settingsMap[touchscreenIRQ], + .offset_rotation = (uint8_t)settingsMap[touchscreenRotate], + .i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}}); + } + } + deviceScreen = &DeviceScreen::create(&displayConfig); + PacketAPI::create(PacketServer::init()); + deviceScreen->init(new PacketClient); + } else { + LOG_INFO("Running without TFT display!"); + } +#endif + +#ifdef ARCH_ESP32 + tftSleepObserver.observe(¬ifyLightSleep); + endSleepObserver.observe(¬ifyLightSleepEnd); + xTaskCreatePinnedToCore(tft_task_handler, "tft", 8192, NULL, 1, NULL, 0); +#endif +} + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 807252510..f13941d96 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,9 +115,27 @@ AccelerometerThread *accelerometerThread = nullptr; AudioThread *audioThread = nullptr; #endif +#if HAS_TFT +extern void tftSetup(void); +#endif + +#ifdef HAS_UDP_MULTICAST +#include "mesh/udp/UdpMulticastThread.h" +UdpMulticastThread *udpThread = nullptr; +#endif + +#if defined(TCXO_OPTIONAL) +float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down. +#endif + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +void setupNicheGraphics(); +#include "nicheGraphics.h" +#endif + using namespace concurrency; -volatile static const char slipstreamTZString[] = USERPREFS_TZ_STRING; +volatile static const char slipstreamTZString[] = {USERPREFS_TZ_STRING}; // We always create a screen object, but we only init it if we find the hardware graphics::Screen *screen = nullptr; @@ -131,6 +149,9 @@ meshtastic::GPSStatus *gpsStatus = new meshtastic::GPSStatus(); // Global Node status meshtastic::NodeStatus *nodeStatus = new meshtastic::NodeStatus(); +// Global Bluetooth status +meshtastic::BluetoothStatus *bluetoothStatus = new meshtastic::BluetoothStatus(); + // Scan for I2C Devices /// The I2C address of our display (if found) @@ -251,6 +272,15 @@ void setup() // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) pinMode(KB_POWERON, OUTPUT); digitalWrite(KB_POWERON, HIGH); + // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus + // We need to initialize all CS pins in advance otherwise there will be SPI communication issues + // e.g. when detecting the SD card + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); delay(100); #endif @@ -428,6 +458,10 @@ void setup() digitalWrite(AQ_SET_PIN, HIGH); #endif +#if HAS_TFT + tftSetup(); +#endif + // Currently only the tbeam has a PMU // PMU initialization needs to be placed before i2c scanning power = new Power(); @@ -646,9 +680,9 @@ void setup() // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; - // If we're taking on the repeater role, use flood router and turn off 3V3_S rail because peripherals are not needed + // If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { - router = new FloodingRouter(); + router = new NextHopRouter(); #ifdef PIN_3V3_EN digitalWrite(PIN_3V3_EN, LOW); #endif @@ -733,8 +767,9 @@ void setup() #endif // Initialize the screen first so we can show the logo while we start up everything else. +#if HAS_SCREEN screen = new graphics::Screen(screen_found, screen_model, screen_geometry); - +#endif // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string @@ -783,12 +818,22 @@ void setup() LOG_DEBUG("Start audio thread"); audioThread = new AudioThread(); #endif + +#ifdef HAS_UDP_MULTICAST + LOG_DEBUG("Start multicast thread"); + udpThread = new UdpMulticastThread(); +#endif service = new MeshService(); service->init(); // Now that the mesh service is created, create any modules setupModules(); +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + // After modules are setup, so we can observe modules + setupNicheGraphics(); +#endif + #ifdef LED_PIN // Turn LED off after boot, if heartbeat by config if (config.device.led_heartbeat_disabled) @@ -933,6 +978,7 @@ void setup() if (!sxIf->init()) { LOG_WARN("No SX1262 radio"); delete sxIf; + rIf = NULL; } else { LOG_INFO("SX1262 init success"); rIf = sxIf; @@ -949,6 +995,7 @@ void setup() if (!sxIf->init()) { LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); delete sxIf; + rIf = NULL; } else { LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); rIf = sxIf; @@ -1144,7 +1191,15 @@ void setup() // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS powerFSMthread = new PowerFSMThread(); + +#if !HAS_TFT setCPUFast(false); // 80MHz is fine for our slow peripherals +#endif + +#ifdef ARDUINO_ARCH_ESP32 + LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); + LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); +#endif } #endif uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) @@ -1241,4 +1296,5 @@ void loop() mainDelay.delay(delayMsec); } } -#endif + +#endif \ No newline at end of file diff --git a/src/main.h b/src/main.h index 252f0e3ee..a7835a3d6 100644 --- a/src/main.h +++ b/src/main.h @@ -1,5 +1,6 @@ #pragma once +#include "BluetoothStatus.h" #include "GPSStatus.h" #include "NodeStatus.h" #include "PowerStatus.h" @@ -49,6 +50,11 @@ extern Adafruit_DRV2605 drv; extern AudioThread *audioThread; #endif +#ifdef HAS_UDP_MULTICAST +#include "mesh/udp/UdpMulticastThread.h" +extern UdpMulticastThread *udpThread; +#endif + // Global Screen singleton. extern graphics::Screen *screen; diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4bc91ce4e..f1d4926db 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -93,6 +93,35 @@ void Channels::initDefaultLoraConfig() #endif } +bool Channels::ensureLicensedOperation() +{ + if (!owner.is_licensed) { + return false; + } + bool hasEncryptionOrAdmin = false; + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + auto channel = channels.getByIndex(i); + if (!channel.has_settings) { + continue; + } + auto &channelSettings = channel.settings; + if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) { + channel.role = meshtastic_Channel_Role_DISABLED; + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + hasEncryptionOrAdmin = true; + channels.setChannel(channel); + + } else if (channelSettings.psk.size > 0) { + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + hasEncryptionOrAdmin = true; + channels.setChannel(channel); + } + } + return hasEncryptionOrAdmin; +} + /** * Write a default channel to the specified channel index */ @@ -119,7 +148,7 @@ void Channels::initDefaultChannel(ChannelIndex chIndex) channelSettings.psk.size = sizeof(defaultpsk0); #endif #ifdef USERPREFS_CHANNEL_0_NAME - strcpy(channelSettings.name, USERPREFS_CHANNEL_0_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_0_NAME); #endif #ifdef USERPREFS_CHANNEL_0_PRECISION channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_0_PRECISION; @@ -138,7 +167,7 @@ void Channels::initDefaultChannel(ChannelIndex chIndex) channelSettings.psk.size = sizeof(defaultpsk1); #endif #ifdef USERPREFS_CHANNEL_1_NAME - strcpy(channelSettings.name, USERPREFS_CHANNEL_1_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_1_NAME); #endif #ifdef USERPREFS_CHANNEL_1_PRECISION channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_1_PRECISION; @@ -157,7 +186,7 @@ void Channels::initDefaultChannel(ChannelIndex chIndex) channelSettings.psk.size = sizeof(defaultpsk2); #endif #ifdef USERPREFS_CHANNEL_2_NAME - strcpy(channelSettings.name, USERPREFS_CHANNEL_2_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_2_NAME); #endif #ifdef USERPREFS_CHANNEL_2_PRECISION channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_2_PRECISION; diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index b0c9b3d07..7873a306a 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -92,6 +92,8 @@ class Channels // Returns true if any of our channels have enabled MQTT uplink or downlink bool anyMqttEnabled(); + bool ensureLicensedOperation(); + private: /** Given a channel index, change to use the crypto key specified by that index * diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index f94540905..142ada806 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -13,7 +13,8 @@ FloodingRouter::FloodingRouter() {} ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p) { // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) - wasSeenRecently(p); // FIXME, move this to a sniffSent method + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us + wasSeenRecently(p); // FIXME, move this to a sniffSent method return Router::send(p); } @@ -23,26 +24,17 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) if (wasSeenRecently(p)) { // Note: this will also add a recent packet record printPacket("Ignore dupe incoming msg", p); rxDupe++; - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! - if (Router::cancelSending(p->from, p->id)) - txRelayCanceled++; - } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { - iface->clampToLateRebroadcastWindow(getFrom(p), p->id); - } /* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, e.g., when - the ACK got lost, we will handle the packet again to make sure it gets an ACK to its packet. */ + the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */ bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; if (isRepeated) { LOG_DEBUG("Repeated reliable tx"); - if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) { - // FIXME - channel index should be used, but the packet is still encrypted here - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, 0, 0); - } + // Check if it's still in the Tx queue, if not, we have to relay it again + if (!findInTxQueue(p->from, p->id)) + perhapsRebroadcast(p); + } else { + perhapsCancelDupe(p); } return true; @@ -51,13 +43,27 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) return Router::shouldFilterReceived(p); } +void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) +{ + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! + if (Router::cancelSending(p->from, p->id)) + txRelayCanceled++; + } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } +} + bool FloodingRouter::isRebroadcaster() { return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE && config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; } -bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) +void FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) { if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) { if (p->id != 0) { @@ -72,13 +78,12 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) tosend->hop_limit = 2; } #endif + tosend->next_hop = NO_NEXT_HOP_PREFERENCE; // this should already be the case, but just in case LOG_INFO("Rebroadcast received floodmsg"); // Note: we are careful to resend using the original senders node id // We are careful not to call our hooked version of send() - because we don't want to check this again Router::send(tosend); - - return true; } else { LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); } @@ -86,13 +91,12 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) LOG_DEBUG("Ignore 0 id broadcast"); } } - - return false; } void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { - bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0); + bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && + (p->decoded.request_id != 0 || p->decoded.reply_id != 0); if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) { // do not flood direct message that is ACKed or replied to LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast"); diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 52614f391..36c6ad8aa 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -1,6 +1,5 @@ #pragma once -#include "PacketHistory.h" #include "Router.h" /** @@ -26,14 +25,11 @@ Any entries in recentBroadcasts that are older than X seconds (longer than the max time a flood can take) will be discarded. */ -class FloodingRouter : public Router, protected PacketHistory +class FloodingRouter : public Router { private: - bool isRebroadcaster(); - - /** Check if we should rebroadcast this packet, and do so if needed - * @return true if rebroadcasted */ - bool perhapsRebroadcast(const meshtastic_MeshPacket *p); + /* Check if we should rebroadcast this packet, and do so if needed */ + void perhapsRebroadcast(const meshtastic_MeshPacket *p); public: /** @@ -62,4 +58,10 @@ class FloodingRouter : public Router, protected PacketHistory * Look for broadcasts we need to rebroadcast */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + + /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ + void perhapsCancelDupe(const meshtastic_MeshPacket *p); + + // Return true if we are a rebroadcaster + bool isRebroadcaster(); }; \ No newline at end of file diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index a3402381f..bc1f242d4 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -265,10 +265,17 @@ template void LR11x0Interface::startReceive() template bool LR11x0Interface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; setStandby(); - result = lora.scanChannel(); + result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) return true; diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index 7dd84639d..0c312fd1e 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -117,6 +117,19 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t return NULL; } +/* Attempt to find a packet from this queue. Return true if it was found. */ +bool MeshPacketQueue::find(NodeNum from, PacketId id) +{ + for (auto it = queue.begin(); it != queue.end(); it++) { + auto p = (*it); + if (getFrom(p) == from && p->id == id) { + return true; + } + } + + return false; +} + /** * Attempt to find a lower-priority packet in the queue and replace it with the provided one. * @return True if the replacement succeeded, false otherwise diff --git a/src/mesh/MeshPacketQueue.h b/src/mesh/MeshPacketQueue.h index b41a214b9..6b2c3998a 100644 --- a/src/mesh/MeshPacketQueue.h +++ b/src/mesh/MeshPacketQueue.h @@ -37,4 +37,7 @@ class MeshPacketQueue /** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true); + + /* Attempt to find a packet from this queue. Return true if it was found. */ + bool find(NodeNum from, PacketId id); }; \ No newline at end of file diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 773ab7053..0ef21d4ca 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -173,7 +173,9 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) return; } #endif - p.from = 0; // We don't let phones assign nodenums to their sent messages + p.from = 0; // We don't let clients assign nodenums to their sent messages + p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages + p.relay_node = NO_RELAY_NODE; // We don't let clients assign relay_node to their sent messages if (p.id == 0) p.id = generatePacketId(); // If the phone didn't supply one, then pick one diff --git a/src/mesh/MeshTypes.h b/src/mesh/MeshTypes.h index 1d6bd342d..680926d3c 100644 --- a/src/mesh/MeshTypes.h +++ b/src/mesh/MeshTypes.h @@ -40,6 +40,11 @@ enum RxSource { /// We normally just use max 3 hops for sending reliable messages #define HOP_RELIABLE 3 +// For old firmware or when falling back to flooding, there is no next-hop preference +#define NO_NEXT_HOP_PREFERENCE 0 +// For old firmware there is no relay node set +#define NO_RELAY_NODE 0 + typedef int ErrorCode; /// Alloc and free packets to our global, ISR safe pool diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp new file mode 100644 index 000000000..f21974a2e --- /dev/null +++ b/src/mesh/NextHopRouter.cpp @@ -0,0 +1,272 @@ +#include "NextHopRouter.h" + +NextHopRouter::NextHopRouter() {} + +PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions) +{ + packet = p; + this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send +} + +/** + * Send a packet + */ +ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) +{ + // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us + wasSeenRecently(p); // FIXME, move this to a sniffSent method + + p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop + LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); + + // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is + // not 0 or want_ack is set, start retransmissions + if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) + startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet + + return Router::send(p); +} + +bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) +{ + bool wasFallback = false; + bool weWereNextHop = false; + if (wasSeenRecently(p, true, &wasFallback, &weWereNextHop)) { // Note: this will also add a recent packet record + printPacket("Ignore dupe incoming msg", p); + rxDupe++; + stopRetransmission(p->from, p->id); + + // If it was a fallback to flooding, try to relay again + if (wasFallback) { + LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node); + // Check if it's still in the Tx queue, if not, we have to relay it again + if (!findInTxQueue(p->from, p->id)) + perhapsRelay(p); + } else { + bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; + // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again + if (isRepeated) { + if (!findInTxQueue(p->from, p->id) && !perhapsRelay(p) && isToUs(p) && p->want_ack) + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } else if (!weWereNextHop) { + perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay + } + } + return true; + } + + return Router::shouldFilterReceived(p); +} + +void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) +{ + NodeNum ourNodeNum = getNodeNum(); + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum); + bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && + (p->decoded.request_id != 0 || p->decoded.reply_id != 0); + if (isAckorReply) { + // Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from" is + // not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the destination + if (p->from != 0) { + meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); + if (origTx) { + // Either relayer of ACK was also a relayer of the packet, or we were the relayer and the ACK came directly from + // the destination + if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) || + (wasRelayer(ourRelayID, p->decoded.request_id, p->to) && p->hop_start != 0 && p->hop_start == p->hop_limit)) { + if (origTx->next_hop != p->relay_node) { // Not already set + LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node); + origTx->next_hop = p->relay_node; + } + } + } + } + if (!isToUs(p)) { + Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM + // stop retransmission for the original packet + stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id + } + } + + perhapsRelay(p); + + // handle the packet as normal + Router::sniffReceived(p, c); +} + +/* Check if we should be relaying this packet if so, do so. */ +bool NextHopRouter::perhapsRelay(const meshtastic_MeshPacket *p) +{ + if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { + if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { + if (isRebroadcaster()) { + meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it + LOG_INFO("Relaying received message coming from %x", p->relay_node); + + tosend->hop_limit--; // bump down the hop count + NextHopRouter::send(tosend); + + return true; + } else { + LOG_DEBUG("Not rebroadcasting: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); + } + } + } + + return false; +} + +/** + * Get the next hop for a destination, given the relay node + * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) + */ +uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) +{ + // When we're a repeater router->sniffReceived will call NextHopRouter directly without checking for broadcast + if (isBroadcast(to)) + return NO_NEXT_HOP_PREFERENCE; + + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); + if (node && node->next_hop) { + // We are careful not to return the relay node as the next hop + if (node->next_hop != relay_node) { + // LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop); + return node->next_hop; + } else + LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); + } + return NO_NEXT_HOP_PREFERENCE; +} + +PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) +{ + auto old = pending.find(key); // If we have an old record, someone messed up because id got reused + if (old != pending.end()) { + return &old->second; + } else + return NULL; +} + +/** + * Stop any retransmissions we are doing of the specified node/packet ID pair + */ +bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) +{ + auto key = GlobalPacketId(from, id); + return stopRetransmission(key); +} + +bool NextHopRouter::stopRetransmission(GlobalPacketId key) +{ + auto old = findPendingPacket(key); + if (old) { + auto p = old->packet; + /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue + to avoid canceling a transmission if it was ACKed super fast via MQTT */ + if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { + // remove the 'original' (identified by originator and packet->id) from the txqueue and free it + cancelSending(getFrom(p), p->id); + // now free the pooled copy for retransmission too + packetPool.release(p); + } + auto numErased = pending.erase(key); + assert(numErased == 1); + return true; + } else + return false; +} + +/** + * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. + */ +PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx) +{ + auto id = GlobalPacketId(p); + auto rec = PendingPacket(p, numReTx); + + stopRetransmission(getFrom(p), p->id); + + setNextTx(&rec); + pending[id] = rec; + + return &pending[id]; +} + +/** + * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) + */ +int32_t NextHopRouter::doRetransmissions() +{ + uint32_t now = millis(); + int32_t d = INT32_MAX; + + // FIXME, we should use a better datastructure rather than walking through this map. + // for(auto el: pending) { + for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { + ++nextIt; // we use this odd pattern because we might be deleting it... + auto &p = it->second; + + bool stillValid = true; // assume we'll keep this record around + + // FIXME, handle 51 day rolloever here!!! + if (p.nextTxMsec <= now) { + if (p.numRetransmissions == 0) { + if (isFromUs(p.packet)) { + LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, + p.packet->id); + sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); + } + // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived + stopRetransmission(it->first); + stillValid = false; // just deleted it + } else { + LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, + p.packet->id, p.numRetransmissions); + + if (!isBroadcast(p.packet->to)) { + if (p.numRetransmissions == 1) { + // Last retransmission, reset next_hop (fallback to FloodingRouter) + p.packet->next_hop = NO_NEXT_HOP_PREFERENCE; + // Also reset it in the nodeDB + meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); + if (sentTo) { + LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to); + sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; + } + FloodingRouter::send(packetPool.allocCopy(*p.packet)); + } else { + NextHopRouter::send(packetPool.allocCopy(*p.packet)); + } + } else { + // Note: we call the superclass version because we don't want to have our version of send() add a new + // retransmission record + FloodingRouter::send(packetPool.allocCopy(*p.packet)); + } + + // Queue again + --p.numRetransmissions; + setNextTx(&p); + } + } + + if (stillValid) { + // Update our desired sleep delay + int32_t t = p.nextTxMsec - now; + + d = min(t, d); + } + } + + return d; +} + +void NextHopRouter::setNextTx(PendingPacket *pending) +{ + assert(iface); + auto d = iface->getRetransmissionMsec(pending->packet); + pending->nextTxMsec = millis() + d; + LOG_DEBUG("Setting next retransmission in %u msecs: ", d); + printPacket("", pending->packet); + setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time +} \ No newline at end of file diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h new file mode 100644 index 000000000..6c2764aff --- /dev/null +++ b/src/mesh/NextHopRouter.h @@ -0,0 +1,151 @@ +#pragma once + +#include "FloodingRouter.h" +#include + +/** + * An identifier for a globally unique message - a pair of the sending nodenum and the packet id assigned + * to that message + */ +struct GlobalPacketId { + NodeNum node; + PacketId id; + + bool operator==(const GlobalPacketId &p) const { return node == p.node && id == p.id; } + + explicit GlobalPacketId(const meshtastic_MeshPacket *p) + { + node = getFrom(p); + id = p->id; + } + + GlobalPacketId(NodeNum _from, PacketId _id) + { + node = _from; + id = _id; + } +}; + +/** + * A packet queued for retransmission + */ +struct PendingPacket { + meshtastic_MeshPacket *packet; + + /** The next time we should try to retransmit this packet */ + uint32_t nextTxMsec = 0; + + /** Starts at NUM_RETRANSMISSIONS -1 and counts down. Once zero it will be removed from the list */ + uint8_t numRetransmissions = 0; + + PendingPacket() {} + explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions); +}; + +class GlobalPacketIdHashFunction +{ + public: + size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } +}; + +/* + Router for direct messages, which only relays if it is the next hop for a packet. The next hop is set by the current + relayer of a packet, which bases this on information from a previous successful delivery to the destination via flooding. + Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered back to us via a node + that also relayed the original packet, we use that node as next hop for the destination from then on. This makes sure that only + when there’s a two-way connection, we assign a next hop. Both the ReliableRouter and NextHopRouter will do retransmissions (the + NextHopRouter only 1 time). For the final retry, if no one actually relayed the packet, it will reset the next hop in order to + fall back to the FloodingRouter again. Note that thus also intermediate hops will do a single retransmission if the intended + next-hop didn’t relay, in order to fix changes in the middle of the route. +*/ +class NextHopRouter : public FloodingRouter +{ + public: + /** + * Constructor + * + */ + NextHopRouter(); + + /** + * Send a packet + * @return an error code + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; + + /** Do our retransmission handling */ + virtual int32_t runOnce() override + { + // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation + doRetransmissions(); + + int32_t r = FloodingRouter::runOnce(); + + // Also after calling runOnce there might be new packets to retransmit + auto d = doRetransmissions(); + return min(d, r); + } + + // The number of retransmissions intermediate nodes will do (actually 1 less than this) + constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2; + // The number of retransmissions the original sender will do + constexpr static uint8_t NUM_RELIABLE_RETX = 3; + + protected: + /** + * Pending retransmissions + */ + std::unordered_map pending; + + /** + * Should this incoming filter be dropped? + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + + /** + * Look for packets we need to relay + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + + /** + * Try to find the pending packet record for this ID (or NULL if not found) + */ + PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } + PendingPacket *findPendingPacket(GlobalPacketId p); + + /** + * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. + */ + PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); + + /** + * Stop any retransmissions we are doing of the specified node/packet ID pair + * + * @return true if we found and removed a transmission with this ID + */ + bool stopRetransmission(NodeNum from, PacketId id); + bool stopRetransmission(GlobalPacketId p); + + /** + * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) + * + * @return the number of msecs until our next retransmission or MAXINT if none scheduled + */ + int32_t doRetransmissions(); + + void setNextTx(PendingPacket *pending); + + private: + /** + * Get the next hop for a destination, given the relay node + * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) + */ + uint8_t getNextHop(NodeNum to, uint8_t relay_node); + + /** Check if we should be relaying this packet if so, do so. + * @return true if we did relay */ + bool perhapsRelay(const meshtastic_MeshPacket *p); +}; \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 9caa03928..62ab675bc 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -56,6 +56,7 @@ NodeDB *nodeDB = nullptr; // we have plenty of ram so statically alloc this tempbuf (for now) EXT_RAM_BSS_ATTR meshtastic_DeviceState devicestate; meshtastic_MyNodeInfo &myNodeInfo = devicestate.my_node; +meshtastic_NodeDatabase nodeDatabase; meshtastic_LocalConfig config; meshtastic_DeviceUIConfig uiconfig{.screen_brightness = 153, .screen_timeout = 30}; meshtastic_LocalModuleConfig moduleConfig; @@ -143,7 +144,7 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_ #endif -bool meshtastic_DeviceState_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) +bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) { if (ostream) { std::vector const *vec = (std::vector *)field->pData; @@ -192,6 +193,7 @@ NodeDB::NodeDB() cleanupMeshDB(); uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); + uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)); uint32_t configCRC = crc32Buffer(&config, sizeof(config)); uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); @@ -246,15 +248,15 @@ NodeDB::NodeDB() // Ensure macaddr is set to our macaddr as it will be copied in our info below memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); - // Include our owner in the node db under our nodenum - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); if (!config.has_security) { config.has_security = true; + config.security = meshtastic_Config_SecurityConfig_init_default; config.security.serial_enabled = config.device.serial_enabled; config.security.is_managed = config.device.is_managed; } #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (!owner.is_licensed) { bool keygenSuccess = false; if (config.security.private_key.size == 32) { @@ -281,10 +283,18 @@ NodeDB::NodeDB() crypto->setDHPrivateKey(config.security.private_key.bytes); } #endif - + // Include our owner in the node db under our nodenum + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); info->user = TypeConversions::ConvertToUserLite(owner); info->has_user = true; + // If node database has not been saved for the first time, save it now +#ifdef FSCom + if (!FSCom.exists(nodeDatabaseFileName)) { + saveNodeDatabaseToDisk(); + } +#endif + #ifdef ARCH_ESP32 Preferences preferences; preferences.begin("meshtastic", false); @@ -296,6 +306,9 @@ NodeDB::NodeDB() resetRadioConfig(); // If bogus settings got saved, then fix them // nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes); + // Uncomment below to always enable UDP broadcasts + // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; + // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value // of 30 minutes or more if (channels.isDefaultChannel(channels.getPrimaryIndex())) { @@ -315,8 +328,15 @@ NodeDB::NodeDB() moduleConfig.neighbor_info.update_interval = Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs); + // Don't let licensed users to rebroadcast encrypted packets + if (owner.is_licensed) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + } + if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) saveWhat |= SEGMENT_DEVICESTATE; + if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) + saveWhat |= SEGMENT_NODEDATABASE; if (configCRC != crc32Buffer(&config, sizeof(config))) saveWhat |= SEGMENT_CONFIG; if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile))) @@ -380,11 +400,13 @@ bool isBroadcast(uint32_t dest) return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; } -bool NodeDB::resetRadioConfig(bool factory_reset) +bool NodeDB::resetRadioConfig(bool factory_reset, bool is_fresh_install) { bool didFactoryReset = false; - radioGeneration++; + if (is_fresh_install) { + radioGeneration++; + } if (factory_reset) { didFactoryReset = factoryReset(); @@ -407,13 +429,6 @@ bool NodeDB::resetRadioConfig(bool factory_reset) rebootAtMsec = millis() + (5 * 1000); } -#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3)) && HAS_TFT - // as long as PhoneAPI shares BT and TFT app switch BT off - config.bluetooth.enabled = false; - if (moduleConfig.external_notification.nag_timeout == 60) - moduleConfig.external_notification.nag_timeout = 0; -#endif - return didFactoryReset; } @@ -431,6 +446,7 @@ bool NodeDB::factoryReset(bool eraseBleBonds) #endif spiLock->unlock(); // second, install default state (this will deal with the duplicate mac address issue) + installDefaultNodeDatabase(); installDefaultDeviceState(); installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds installDefaultModuleConfig(); @@ -455,6 +471,15 @@ bool NodeDB::factoryReset(bool eraseBleBonds) return true; } +void NodeDB::installDefaultNodeDatabase() +{ + LOG_DEBUG("Install default NodeDatabase"); + nodeDatabase.version = DEVICESTATE_CUR_VER; + nodeDatabase.nodes = std::vector(MAX_NUM_NODES); + numMeshNodes = 0; + meshNodes = &nodeDatabase.nodes; +} + void NodeDB::installDefaultConfig(bool preserveKey = false) { uint8_t private_key_temp[32]; @@ -555,18 +580,30 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #else config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; #endif +#ifdef USERPREFS_CONFIG_SMART_POSITION_ENABLED + config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED; +#else config.position.position_broadcast_smart_enabled = true; +#endif config.position.broadcast_smart_minimum_distance = 100; config.position.broadcast_smart_minimum_interval_secs = 30; if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; config.security.serial_enabled = true; config.security.admin_channel_enabled = false; - resetRadioConfig(); + resetRadioConfig(false, true); // This also triggers NodeInfo/Position requests since we're fresh strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); - // FIXME: Default to bluetooth capability of platform as default + +#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR)) && \ + HAS_TFT + // switch BT off by default; use TFT programming mode or hotkey to enable + config.bluetooth.enabled = false; +#else + // default to bluetooth capability of platform as default config.bluetooth.enabled = true; +#endif config.bluetooth.fixed_pin = defaultBLEPin; + #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ defined(HX8357_CS) || defined(USE_ST7789) bool hasScreen = true; @@ -582,9 +619,12 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) hasScreen = true; else hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; +#elif MESHTASTIC_INCLUDE_NICHE_GRAPHICS // See "src/graphics/niche" + bool hasScreen = true; // Use random pin for Bluetooth pairing #else bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #endif + #ifdef USERPREFS_FIXED_BLUETOOTH config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH; config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; @@ -608,18 +648,22 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.display.screen_on_secs = 30; config.display.wake_on_tap_or_motion = true; #endif -#ifdef HELTEC_VISION_MASTER_E290 - // Orient so that LoRa antenna faces up - config.display.flip_screen = true; -#endif initConfigIntervals(); } void NodeDB::initConfigIntervals() { +#ifdef USERPREFS_CONFIG_GPS_UPDATE_INTERVAL + config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL; +#else config.position.gps_update_interval = default_gps_update_interval; +#endif +#ifdef USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL + config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL; +#else config.position.position_broadcast_secs = default_broadcast_interval_secs; +#endif config.power.ls_secs = default_ls_secs; config.power.min_wake_secs = default_min_wake_secs; @@ -669,8 +713,13 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.use_i2s_as_buzzer = true; moduleConfig.external_notification.alert_message_buzzer = true; +#if HAS_TFT + if (moduleConfig.external_notification.nag_timeout == 60) + moduleConfig.external_notification.nag_timeout = 0; +#else moduleConfig.external_notification.nag_timeout = 60; #endif +#endif #ifdef NANO_G2_ULTRA moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.alert_message = true; @@ -782,9 +831,10 @@ void NodeDB::resetNodes() if (!config.position.fixed_position) clearLocalPosition(); numMeshNodes = 1; - std::fill(devicestate.node_db_lite.begin() + 1, devicestate.node_db_lite.end(), meshtastic_NodeInfoLite()); + std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); devicestate.has_rx_text_message = false; devicestate.has_rx_waypoint = false; + saveNodeDatabaseToDisk(); saveDeviceStateToDisk(); if (neighborInfoModule && moduleConfig.neighbor_info.enabled) neighborInfoModule->resetNeighbors(); @@ -800,10 +850,10 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum) removed++; } numMeshNodes -= removed; - std::fill(devicestate.node_db_lite.begin() + numMeshNodes, devicestate.node_db_lite.begin() + numMeshNodes + 1, + std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, meshtastic_NodeInfoLite()); LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); - saveDeviceStateToDisk(); + saveNodeDatabaseToDisk(); } void NodeDB::clearLocalPosition() @@ -832,7 +882,7 @@ void NodeDB::cleanupMeshDB() } } numMeshNodes -= removed; - std::fill(devicestate.node_db_lite.begin() + numMeshNodes, devicestate.node_db_lite.begin() + numMeshNodes + removed, + std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, meshtastic_NodeInfoLite()); LOG_DEBUG("cleanupMeshDB purged %d entries", removed); } @@ -842,13 +892,9 @@ void NodeDB::installDefaultDeviceState() LOG_INFO("Install default DeviceState"); // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); - numMeshNodes = 0; - meshNodes = &devicestate.node_db_lite; - // init our devicestate with valid flags so protobuf writing/reading will work devicestate.has_my_node = true; devicestate.has_owner = true; - // devicestate.node_db_lite_count = 0; devicestate.version = DEVICESTATE_CUR_VER; devicestate.receive_queue_count = 0; // Not yet implemented FIXME devicestate.has_rx_waypoint = false; @@ -859,12 +905,12 @@ void NodeDB::installDefaultDeviceState() // Set default owner name pickNewNodeNum(); // based on macaddr now #ifdef USERPREFS_CONFIG_OWNER_LONG_NAME - snprintf(owner.long_name, sizeof(owner.long_name), USERPREFS_CONFIG_OWNER_LONG_NAME); + snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME); #else snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff); #endif #ifdef USERPREFS_CONFIG_OWNER_SHORT_NAME - snprintf(owner.short_name, sizeof(owner.short_name), USERPREFS_CONFIG_OWNER_SHORT_NAME); + snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME); #else snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff); #endif @@ -902,12 +948,6 @@ void NodeDB::pickNewNodeNum() myNodeInfo.my_node_num = nodeNum; } -static const char *prefFileName = "/prefs/db.proto"; -static const char *configFileName = "/prefs/config.proto"; -static const char *uiconfigFileName = "/prefs/uiconfig.proto"; -static const char *moduleConfigFileName = "/prefs/module.proto"; -static const char *channelFileName = "/prefs/channels.proto"; - /** Load a protobuf from a file, return LoadFileResult */ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct) @@ -943,20 +983,58 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t void NodeDB::loadFromDisk() { - devicestate.version = - 0; // Mark the current device state as completely unusable, so that if we fail reading the entire file from + // Mark the current device state as completely unusable, so that if we fail reading the entire file from // disk we will still factoryReset to restore things. + devicestate.version = 0; + + meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero; #ifdef ARCH_ESP32 spiLock->lock(); + // If the legacy deviceState exists, start over with a factory reset if (FSCom.exists("/static/static")) rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release spiLock->unlock(); #endif +#ifdef FSCom + spiLock->lock(); + if (FSCom.exists(legacyPrefFileName)) { + spiLock->unlock(); + LOG_WARN("Legacy prefs version found, factory resetting"); + if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, + &config) == LoadFileResult::LOAD_SUCCESS && + config.has_security && config.security.private_key.size > 0) { + LOG_DEBUG("Saving backup of security config and keys"); + backupSecurity = config.security; + } + spiLock->lock(); + rmDir("/prefs"); + spiLock->unlock(); + } else { + spiLock->unlock(); + } + +#endif + auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase), + &meshtastic_NodeDatabase_msg, &nodeDatabase); + if (nodeDatabase.version < DEVICESTATE_MIN_VER) { + LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); + installDefaultNodeDatabase(); + } else { + meshNodes = &nodeDatabase.nodes; + numMeshNodes = nodeDatabase.nodes.size(); + LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); + } + + if (numMeshNodes > MAX_NUM_NODES) { + LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); + numMeshNodes = MAX_NUM_NODES; + } + meshNodes->resize(MAX_NUM_NODES); // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM - auto state = loadProto(prefFileName, sizeof(meshtastic_DeviceState) + MAX_NUM_NODES_FS * meshtastic_NodeInfoLite_size, - sizeof(meshtastic_DeviceState), &meshtastic_DeviceState_msg, &devicestate); + state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), + &meshtastic_DeviceState_msg, &devicestate); // See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786 // It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most of our @@ -970,15 +1048,8 @@ void NodeDB::loadFromDisk() LOG_WARN("Devicestate %d is old, discard", devicestate.version); installDefaultDeviceState(); } else { - LOG_INFO("Loaded saved devicestate version %d, with nodecount: %d", devicestate.version, devicestate.node_db_lite.size()); - meshNodes = &devicestate.node_db_lite; - numMeshNodes = devicestate.node_db_lite.size(); + LOG_INFO("Loaded saved devicestate version %d", devicestate.version); } - if (numMeshNodes > MAX_NUM_NODES) { - LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); - numMeshNodes = MAX_NUM_NODES; - } - meshNodes->resize(MAX_NUM_NODES); state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config); @@ -992,6 +1063,11 @@ void NodeDB::loadFromDisk() LOG_INFO("Loaded saved config version %d", config.version); } } + if (backupSecurity.private_key.size > 0) { + LOG_DEBUG("Restoring backup of security config"); + config.security = backupSecurity; + saveToDisk(SEGMENT_CONFIG); + } // Make sure we load hard coded admin keys even when the configuration file has none. // Initialize admin_key_count to zero @@ -1144,15 +1220,24 @@ bool NodeDB::saveDeviceStateToDisk() #endif // Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB // Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this - size_t deviceStateSize; - pb_get_encoded_size(&deviceStateSize, meshtastic_DeviceState_fields, &devicestate); - return saveProto(prefFileName, deviceStateSize, &meshtastic_DeviceState_msg, &devicestate, false); + return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true); +} + +bool NodeDB::saveNodeDatabaseToDisk() +{ +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + size_t nodeDatabaseSize; + pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); + return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); } bool NodeDB::saveToDiskNoRetry(int saveWhat) { bool success = true; - #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1197,11 +1282,16 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) success &= saveDeviceStateToDisk(); } + if (saveWhat & SEGMENT_NODEDATABASE) { + success &= saveNodeDatabaseToDisk(); + } + return success; } bool NodeDB::saveToDisk(int saveWhat) { + LOG_DEBUG("Save to disk %d", saveWhat); bool success = saveToDiskNoRetry(saveWhat); if (!success) { @@ -1382,8 +1472,9 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde // We just changed something about a User, // store our DB unless we just did so less than a minute ago + if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) { - saveToDisk(SEGMENT_DEVICESTATE); + saveToDisk(SEGMENT_NODEDATABASE); lastNodeDbSave = millis(); } else { LOG_DEBUG("Defer NodeDB saveToDisk for now"); @@ -1397,6 +1488,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { + // if (mp.from == getNodeNum()) { + // LOG_DEBUG("Ignore update from self"); + // return; + // } if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); @@ -1506,6 +1601,17 @@ bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); } +/// If we have a node / user and they report is_licensed = true +/// we consider them licensed +UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) +{ + meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); + if (!info || !info->has_user) { + return UserLicenseStatus::NotKnown; + } + return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; +} + /// Record an error that should be reported via analytics void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) { @@ -1528,4 +1634,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co LOG_ERROR("A critical failure occurred, portduino is exiting"); exit(2); #endif -} +} \ No newline at end of file diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index d244a94ba..25f1e9083 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "MeshTypes.h" @@ -12,6 +13,10 @@ #include "mesh-pb-constants.h" #include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#endif + /* DeviceState versions used to be defined in the .proto file but really only this function cares. So changed to a #define here. @@ -21,11 +26,13 @@ DeviceState versions used to be defined in the .proto file but really only this #define SEGMENT_MODULECONFIG 2 #define SEGMENT_DEVICESTATE 4 #define SEGMENT_CHANNELS 8 +#define SEGMENT_NODEDATABASE 16 -#define DEVICESTATE_CUR_VER 23 -#define DEVICESTATE_MIN_VER 22 +#define DEVICESTATE_CUR_VER 24 +#define DEVICESTATE_MIN_VER 24 extern meshtastic_DeviceState devicestate; +extern meshtastic_NodeDatabase nodeDatabase; extern meshtastic_ChannelFile channelFile; extern meshtastic_MyNodeInfo &myNodeInfo; extern meshtastic_LocalConfig config; @@ -34,6 +41,14 @@ extern meshtastic_LocalModuleConfig moduleConfig; extern meshtastic_User &owner; extern meshtastic_Position localPosition; +static constexpr const char *deviceStateFileName = "/prefs/device.proto"; +static constexpr const char *legacyPrefFileName = "/prefs/db.proto"; +static constexpr const char *nodeDatabaseFileName = "/prefs/nodes.proto"; +static constexpr const char *configFileName = "/prefs/config.proto"; +static constexpr const char *uiconfigFileName = "/prefs/uiconfig.proto"; +static constexpr const char *moduleConfigFileName = "/prefs/module.proto"; +static constexpr const char *channelFileName = "/prefs/channels.proto"; + /// Given a node, return how many seconds in the past (vs now) that we last heard from it uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n); @@ -53,6 +68,8 @@ enum LoadFileResult { OTHER_FAILURE = 5 }; +enum UserLicenseStatus { NotKnown, NotLicensed, Licensed }; + class NodeDB { // NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt @@ -75,15 +92,18 @@ class NodeDB /// write to flash /// @return true if the save was successful - bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | + SEGMENT_NODEDATABASE); /** Reinit radio config if needed, because either: * a) sometimes a buggy android app might send us bogus settings or * b) the client set factory_reset * + * @param factory_reset if true, reset all settings to factory defaults + * @param is_fresh_install set to true after a fresh install, to trigger NodeInfo/Position requests * @return true if the config was completely reset, in that case, we should send it back to the client */ - bool resetRadioConfig(bool factory_reset = false); + bool resetRadioConfig(bool factory_reset = false, bool is_fresh_install = false); /// given a subpacket sniffed from the network, update our DB state /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw @@ -104,6 +124,9 @@ class NodeDB /// @return our node number NodeNum getNodeNum() { return myNodeInfo.my_node_num; } + // @return last byte of a NodeNum, 0xFF if it ended at 0x00 + uint8_t getLastByteOfNodeNum(NodeNum num) { return (uint8_t)((num & 0xFF) ? (num & 0xFF) : 0xFF); } + /// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay for use // bool handleWantNodeNum(NodeNum n); @@ -148,6 +171,17 @@ class NodeDB virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); size_t getNumMeshNodes() { return numMeshNodes; } + UserLicenseStatus getLicenseStatus(uint32_t nodeNum); + + size_t getMaxNodesAllocatedSize() + { + meshtastic_NodeDatabase emptyNodeDatabase; + emptyNodeDatabase.version = DEVICESTATE_CUR_VER; + size_t nodeDatabaseSize; + pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); + return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); + } + // returns true if the maximum number of nodes is reached or we are running low on memory bool isFull(); @@ -188,8 +222,8 @@ class NodeDB void cleanupMeshDB(); /// Reinit device state from scratch (not loading from disk) - void installDefaultDeviceState(), installDefaultChannels(), installDefaultConfig(bool preserveKey), - installDefaultModuleConfig(); + void installDefaultDeviceState(), installDefaultNodeDatabase(), installDefaultChannels(), + installDefaultConfig(bool preserveKey), installDefaultModuleConfig(); /// write to flash /// @return true if the save was successful @@ -197,6 +231,7 @@ class NodeDB bool saveChannelsToDisk(); bool saveDeviceStateToDisk(); + bool saveNodeDatabaseToDisk(); }; extern NodeDB *nodeDB; diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 6eb4b6ea1..15fa9cdcd 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -16,7 +16,7 @@ PacketHistory::PacketHistory() /** * Update recentBroadcasts and return true if we have already seen this packet */ -bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate) +bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop) { if (p->id == 0) { LOG_DEBUG("Ignore message with zero id"); @@ -27,6 +27,9 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd r.id = p->id; r.sender = getFrom(p); r.rxTimeMsec = millis(); + r.next_hop = p->next_hop; + r.relayed_by[0] = p->relay_node; + // LOG_INFO("Add relayed_by 0x%x for id=0x%x", p->relay_node, r.id); auto found = recentPackets.find(r); bool seenRecently = (found != recentPackets.end()); // found not equal to .end() means packet was seen recently @@ -40,14 +43,36 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd if (seenRecently) { LOG_DEBUG("Found existing packet record for fr=0x%x,to=0x%x,id=0x%x", p->from, p->to, p->id); + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); + if (wasFallback) { + // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already + // before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle + // it now. + if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && + found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, found) && + !wasRelayer(ourRelayID, found) && !wasRelayer(found->next_hop, found)) { + *wasFallback = true; + } + } + + // Check if we were the next hop for this packet + if (weWereNextHop) { + *weWereNextHop = found->next_hop == ourRelayID; + } } if (withUpdate) { - if (found != recentPackets.end()) { // delete existing to updated timestamp (re-insert) - recentPackets.erase(found); // as unsorted_set::iterator is const (can't update timestamp - so re-insert..) + if (found != recentPackets.end()) { // delete existing to updated timestamp and relayed_by (re-insert) + // Add the existing relayed_by to the new record + for (uint8_t i = 0; i < NUM_RELAYERS - 1; i++) { + if (found->relayed_by[i]) + r.relayed_by[i + 1] = found->relayed_by[i]; + } + r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) + recentPackets.erase(found); // as unsorted_set::iterator is const (can't update - so re-insert..) } recentPackets.insert(r); - printPacket("Add packet record", p); + LOG_DEBUG("Add packet record fr=0x%x, id=0x%x", p->from, p->id); } // Capacity is reerved, so only purge expired packets if recentPackets fills past 90% capacity @@ -75,4 +100,59 @@ void PacketHistory::clearExpiredRecentPackets() } LOG_DEBUG("recentPackets size=%ld (after clearing expired packets)", recentPackets.size()); +} + +/* Check if a certain node was a relayer of a packet in the history given an ID and sender + * @return true if node was indeed a relayer, false if not */ +bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +{ + if (relayer == 0) + return false; + + PacketRecord r = {.sender = sender, .id = id, .rxTimeMsec = 0, .next_hop = 0}; + auto found = recentPackets.find(r); + + if (found == recentPackets.end()) { + return false; + } + + return wasRelayer(relayer, found); +} + +/* Check if a certain node was a relayer of a packet in the history given iterator + * @return true if node was indeed a relayer, false if not */ +bool PacketHistory::wasRelayer(const uint8_t relayer, std::unordered_set::iterator r) +{ + for (uint8_t i = 0; i < NUM_RELAYERS; i++) { + if (r->relayed_by[i] == relayer) { + return true; + } + } + return false; +} + +// Remove a relayer from the list of relayers of a packet in the history given an ID and sender +void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +{ + PacketRecord r = {.sender = sender, .id = id, .rxTimeMsec = 0, .next_hop = 0}; + auto found = recentPackets.find(r); + + if (found == recentPackets.end()) { + return; + } + // Make a copy of the found record + r.next_hop = found->next_hop; + r.rxTimeMsec = found->rxTimeMsec; + + // Only add the relayers that are not the one we want to remove + uint8_t j = 0; + for (uint8_t i = 0; i < NUM_RELAYERS; i++) { + if (found->relayed_by[i] != relayer) { + r.relayed_by[j] = found->relayed_by[i]; + j++; + } + } + + recentPackets.erase(found); + recentPackets.insert(r); } \ No newline at end of file diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 0417d0997..db7698f5b 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -1,6 +1,6 @@ #pragma once -#include "Router.h" +#include "NodeDB.h" #include /// We clear our old flood record 10 minutes after we see the last of it @@ -10,13 +10,18 @@ #define FLOOD_EXPIRE_TIME (10 * 60 * 1000L) #endif +#define NUM_RELAYERS \ + 3 // Number of relayer we keep track of. Use 3 to be efficient with memory alignment of PacketRecord to 16 bytes + /** * A record of a recent message broadcast */ struct PacketRecord { NodeNum sender; PacketId id; - uint32_t rxTimeMsec; // Unix time in msecs - the time we received it + uint32_t rxTimeMsec; // Unix time in msecs - the time we received it + uint8_t next_hop; // The next hop asked for this packet + uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet bool operator==(const PacketRecord &p) const { return sender == p.sender && id == p.id; } }; @@ -44,6 +49,20 @@ class PacketHistory * Update recentBroadcasts and return true if we have already seen this packet * * @param withUpdate if true and not found we add an entry to recentPackets + * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if so + * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true if so */ - bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true); -}; + bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr, + bool *weWereNextHop = nullptr); + + /* Check if a certain node was a relayer of a packet in the history given an ID and sender + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); + + /* Check if a certain node was a relayer of a packet in the history given iterator + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, std::unordered_set::iterator r); + + // Remove a relayer from the list of relayers of a packet in the history given an ID and sender + void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); +}; \ No newline at end of file diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 6789acbb3..204886be5 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -12,6 +12,7 @@ #include "PhoneAPI.h" #include "PowerFSM.h" #include "RadioInterface.h" +#include "Router.h" #include "SPILock.h" #include "TypeConversions.h" #include "main.h" @@ -643,6 +644,11 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + return false; } else if (p.decoded.portnum == meshtastic_PortNum_POSITION_APP && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], FIVE_SECONDS_MS)) { LOG_WARN("Rate limit portnum %d", p.decoded.portnum); diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index d91cba116..695c5be77 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -261,7 +261,7 @@ uint8_t RadioInterface::getCWsize(float snr) const uint32_t SNR_MIN = -20; // The maximum value for a LoRa SNR - const uint32_t SNR_MAX = 15; + const uint32_t SNR_MAX = 10; return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); } @@ -340,6 +340,10 @@ void printPacket(const char *prefix, const meshtastic_MeshPacket *p) out += DEBUG_PORT.mt_sprintf(" via MQTT"); if (p->hop_start != 0) out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start); + if (p->next_hop != 0) + out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop); + if (p->relay_node != 0) + out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node); if (p->priority != 0) out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); @@ -566,7 +570,7 @@ void RadioInterface::applyModemConfig() saveChannelNum(channel_num); saveFreq(freq + loraConfig.frequency_offset); - slotTimeMsec = computeSlotTimeMsec(bw, sf); + slotTimeMsec = computeSlotTimeMsec(); preambleTimeMsec = getPacketTime((uint32_t)0); maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); @@ -581,6 +585,25 @@ void RadioInterface::applyModemConfig() LOG_INFO("Slot time: %u msec", slotTimeMsec); } +/** Slottime is the time to detect a transmission has started, consisting of: + - CAD duration; + - roundtrip air propagation time (assuming max. 30km between nodes); + - Tx/Rx turnaround time (maximum of SX126x and SX127x); + - MAC processing time (measured on T-beam) */ +uint32_t RadioInterface::computeSlotTimeMsec() +{ + float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds + float symbolTime = pow(2, sf) / bw; // in milliseconds + + if (myRegion->wideLora) { + // CAD duration derived from AN1200.22 of SX1280 + return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime; + } else { + // CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol + return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime; + } +} + /** * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it @@ -620,8 +643,8 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) radioBuffer.header.to = p->to; radioBuffer.header.id = p->id; radioBuffer.header.channel = p->channel; - radioBuffer.header.next_hop = 0; // *** For future use *** - radioBuffer.header.relay_node = 0; // *** For future use *** + radioBuffer.header.next_hop = p->next_hop; + radioBuffer.header.relay_node = p->relay_node; if (p->hop_limit > HOP_MAX) { LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE); p->hop_limit = HOP_RELIABLE; diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 652b2269c..68ae09635 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -38,10 +38,10 @@ typedef struct { /** The channel hash - used as a hint for the decoder to limit which channels we consider */ uint8_t channel; - // ***For future use*** Last byte of the NodeNum of the next-hop for this packet + // Last byte of the NodeNum of the next-hop for this packet uint8_t next_hop; - // ***For future use*** Last byte of the NodeNum of the node that will relay/relayed this packet + // Last byte of the NodeNum of the node that will relay/relayed this packet uint8_t relay_node; } PacketHeader; @@ -83,24 +83,22 @@ class RadioInterface float bw = 125; uint8_t sf = 9; uint8_t cr = 5; - /** Slottime is the minimum time to wait, consisting of: - - CAD duration (maximum of SX126x and SX127x); - - roundtrip air propagation time (assuming max. 30km between nodes); - - Tx/Rx turnaround time (maximum of SX126x and SX127x); - - MAC processing time (measured on T-beam) */ - uint32_t slotTimeMsec = computeSlotTimeMsec(bw, sf); + + const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 + const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 + uint32_t slotTimeMsec = computeSlotTimeMsec(); uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast uint32_t maxPacketTimeMsec = 3246; // calculated on startup, this is the default for LongFast const uint32_t PROCESSING_TIME_MSEC = 4500; // time to construct, process and construct a packet again (empirically determined) - const uint8_t CWmin = 2; // minimum CWsize - const uint8_t CWmax = 7; // maximum CWsize + const uint8_t CWmin = 3; // minimum CWsize + const uint8_t CWmax = 8; // maximum CWsize meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending uint32_t lastTxStart = 0L; - uint32_t computeSlotTimeMsec(float bw, float sf) { return 8.5 * pow(2, sf) / bw + 0.2 + 0.4 + 7; } + uint32_t computeSlotTimeMsec(); /** * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need @@ -155,6 +153,9 @@ class RadioInterface /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ virtual bool cancelSending(NodeNum from, PacketId id) { return false; } + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; } + // methods from radiohead /// Initialise the Driver transport hardware and software. diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index e7697c872..bdcf38d29 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -222,6 +222,12 @@ bool RadioLibInterface::cancelSending(NodeNum from, PacketId id) return result; } +/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ +bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) +{ + return txQueue.find(from, id); +} + /** radio helper thread callback. We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of 'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision. @@ -445,6 +451,9 @@ void RadioLibInterface::handleReceiveInterrupt() mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); + // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) + mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; + mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; addReceiveMetadata(mp); diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 877cc761a..39fbd5ec7 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -135,6 +135,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ virtual bool cancelSending(NodeNum from, PacketId id) override; + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) override; + void enableFan(); void disableFan(); diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 3e2850bcf..6e5c6231b 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -23,7 +23,7 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) } auto copy = packetPool.allocCopy(*p); - startRetransmission(copy); + startRetransmission(copy, NUM_RELIABLE_RETX); } /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an @@ -35,7 +35,7 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) } } - return FloodingRouter::send(p); + return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p); } bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) @@ -73,7 +73,7 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) i->second.nextTxMsec += iface->getPacketTime(p); } - return FloodingRouter::shouldFilterReceived(p); + return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p); } /** @@ -138,126 +138,5 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas } // handle the packet as normal - FloodingRouter::sniffReceived(p, c); -} - -#define NUM_RETRANSMISSIONS 3 - -PendingPacket::PendingPacket(meshtastic_MeshPacket *p) -{ - packet = p; - numRetransmissions = NUM_RETRANSMISSIONS - 1; // We subtract one, because we assume the user just did the first send -} - -PendingPacket *ReliableRouter::findPendingPacket(GlobalPacketId key) -{ - auto old = pending.find(key); // If we have an old record, someone messed up because id got reused - if (old != pending.end()) { - return &old->second; - } else - return NULL; -} -/** - * Stop any retransmissions we are doing of the specified node/packet ID pair - */ -bool ReliableRouter::stopRetransmission(NodeNum from, PacketId id) -{ - auto key = GlobalPacketId(from, id); - return stopRetransmission(key); -} - -bool ReliableRouter::stopRetransmission(GlobalPacketId key) -{ - auto old = findPendingPacket(key); - if (old) { - auto p = old->packet; - /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue - to avoid canceling a transmission if it was ACKed super fast via MQTT */ - if (old->numRetransmissions < NUM_RETRANSMISSIONS - 1) { - // remove the 'original' (identified by originator and packet->id) from the txqueue and free it - cancelSending(getFrom(p), p->id); - } - // now free the pooled copy for retransmission too - packetPool.release(p); - auto numErased = pending.erase(key); - assert(numErased == 1); - return true; - } else - return false; -} - -/** - * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. - */ -PendingPacket *ReliableRouter::startRetransmission(meshtastic_MeshPacket *p) -{ - auto id = GlobalPacketId(p); - auto rec = PendingPacket(p); - - stopRetransmission(getFrom(p), p->id); - - setNextTx(&rec); - pending[id] = rec; - - return &pending[id]; -} - -/** - * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) - */ -int32_t ReliableRouter::doRetransmissions() -{ - uint32_t now = millis(); - int32_t d = INT32_MAX; - - // FIXME, we should use a better datastructure rather than walking through this map. - // for(auto el: pending) { - for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { - ++nextIt; // we use this odd pattern because we might be deleting it... - auto &p = it->second; - - bool stillValid = true; // assume we'll keep this record around - - // FIXME, handle 51 day rollover here!!! - if (p.nextTxMsec <= now) { - if (p.numRetransmissions == 0) { - LOG_DEBUG("Reliable send failed, return a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, - p.packet->id); - sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); - // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived - stopRetransmission(it->first); - stillValid = false; // just deleted it - } else { - LOG_DEBUG("Send reliable retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, - p.packet->id, p.numRetransmissions); - - // Note: we call the superclass version because we don't want to have our version of send() add a new - // retransmission record - FloodingRouter::send(packetPool.allocCopy(*p.packet)); - - // Queue again - --p.numRetransmissions; - setNextTx(&p); - } - } - - if (stillValid) { - // Update our desired sleep delay - int32_t t = p.nextTxMsec - now; - - d = min(t, d); - } - } - - return d; -} - -void ReliableRouter::setNextTx(PendingPacket *pending) -{ - assert(iface); - auto d = iface->getRetransmissionMsec(pending->packet); - pending->nextTxMsec = millis() + d; - LOG_DEBUG("Set next retransmission in %u msecs: ", d); - printPacket("", pending->packet); - setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time + isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c); } \ No newline at end of file diff --git a/src/mesh/ReliableRouter.h b/src/mesh/ReliableRouter.h index ba9ab8c25..2cf10fb99 100644 --- a/src/mesh/ReliableRouter.h +++ b/src/mesh/ReliableRouter.h @@ -1,61 +1,12 @@ #pragma once -#include "FloodingRouter.h" -#include - -/** - * An identifier for a globally unique message - a pair of the sending nodenum and the packet id assigned - * to that message - */ -struct GlobalPacketId { - NodeNum node; - PacketId id; - - bool operator==(const GlobalPacketId &p) const { return node == p.node && id == p.id; } - - explicit GlobalPacketId(const meshtastic_MeshPacket *p) - { - node = getFrom(p); - id = p->id; - } - - GlobalPacketId(NodeNum _from, PacketId _id) - { - node = _from; - id = _id; - } -}; - -/** - * A packet queued for retransmission - */ -struct PendingPacket { - meshtastic_MeshPacket *packet; - - /** The next time we should try to retransmit this packet */ - uint32_t nextTxMsec = 0; - - /** Starts at NUM_RETRANSMISSIONS -1(normally 3) and counts down. Once zero it will be removed from the list */ - uint8_t numRetransmissions = 0; - - PendingPacket() {} - explicit PendingPacket(meshtastic_MeshPacket *p); -}; - -class GlobalPacketIdHashFunction -{ - public: - size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } -}; +#include "NextHopRouter.h" /** * This is a mixin that extends Router with the ability to do (one hop only) reliable message sends. */ -class ReliableRouter : public FloodingRouter +class ReliableRouter : public NextHopRouter { - private: - std::unordered_map pending; - public: /** * Constructor @@ -70,54 +21,14 @@ class ReliableRouter : public FloodingRouter */ virtual ErrorCode send(meshtastic_MeshPacket *p) override; - /** Do our retransmission handling */ - virtual int32_t runOnce() override - { - // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation - auto d = doRetransmissions(); - - int32_t r = FloodingRouter::runOnce(); - - return min(d, r); - } - protected: /** * Look for acks/naks or someone retransmitting us */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /** - * Try to find the pending packet record for this ID (or NULL if not found) - */ - PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } - PendingPacket *findPendingPacket(GlobalPacketId p); - /** * We hook this method so we can see packets before FloodingRouter says they should be discarded */ virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; - - /** - * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. - */ - PendingPacket *startRetransmission(meshtastic_MeshPacket *p); - - private: - /** - * Stop any retransmissions we are doing of the specified node/packet ID pair - * - * @return true if we found and removed a transmission with this ID - */ - bool stopRetransmission(NodeNum from, PacketId id); - bool stopRetransmission(GlobalPacketId p); - - /** - * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) - * - * @return the number of msecs until our next retransmission or MAXINT if none scheduled - */ - int32_t doRetransmissions(); - - void setNextTx(PendingPacket *pending); -}; +}; \ No newline at end of file diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index bfd4c45fd..9e1e41d53 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -249,6 +249,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) // the lora we need to make sure we have replaced it with our local address p->from = getFrom(p); + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us // If we are the original transmitter, set the hop limit with which we start if (isFromUs(p)) p->hop_start = p->hop_limit; @@ -274,6 +275,11 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) abortSendAndNak(encodeResult, p); return encodeResult; // FIXME - this isn't a valid ErrorCode } +#if HAS_UDP_MULTICAST + if (udpThread && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpThread->onSend(const_cast(p)); + } +#endif #if !MESHTASTIC_EXCLUDE_MQTT // Only publish to MQTT if we're the original transmitter of the packet if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { @@ -290,7 +296,18 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ bool Router::cancelSending(NodeNum from, PacketId id) { - return iface ? iface->cancelSending(from, id) : false; + if (iface && iface->cancelSending(from, id)) { + // We are not a relayer of this packet anymore + removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); + return true; + } + return false; +} + +/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ +bool Router::findInTxQueue(NodeNum from, PacketId id) +{ + return iface->findInTxQueue(from, id); } /** diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 0fe2bc551..bf6b77226 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -4,6 +4,7 @@ #include "MemoryPool.h" #include "MeshTypes.h" #include "Observer.h" +#include "PacketHistory.h" #include "PointerQueue.h" #include "RadioInterface.h" #include "concurrency/OSThread.h" @@ -11,7 +12,7 @@ /** * A mesh aware router that supports multiple interfaces. */ -class Router : protected concurrency::OSThread +class Router : protected concurrency::OSThread, protected PacketHistory { private: /// Packets which have just arrived from the radio, ready to be processed by this service and possibly @@ -50,6 +51,9 @@ class Router : protected concurrency::OSThread /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ bool cancelSending(NodeNum from, PacketId id); + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + bool findInTxQueue(NodeNum from, PacketId id); + /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. * The returned packet is guaranteed to have a unique packet ID already assigned */ diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index d64519fb6..9e4726a99 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -296,10 +296,17 @@ template void SX126xInterface::startReceive() template bool SX126xInterface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; setStandby(); - result = lora.scanChannel(); + result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) return true; if (result != RADIOLIB_CHANNEL_FREE) diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 6f960c6b3..c8c24113b 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -275,10 +275,17 @@ template void SX128xInterface::startReceive() template bool SX128xInterface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD_24GHZ, + .detPeak = 0, + .detMin = 0, + .exitMode = 0, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; setStandby(); - result = lora.scanChannel(); + result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) return true; if (result != RADIOLIB_CHANNEL_FREE) diff --git a/src/mesh/api/PacketAPI.cpp b/src/mesh/api/PacketAPI.cpp new file mode 100644 index 000000000..45bbe19d3 --- /dev/null +++ b/src/mesh/api/PacketAPI.cpp @@ -0,0 +1,127 @@ +#ifdef USE_PACKET_API + +#include "api/PacketAPI.h" +#include "MeshService.h" +#include "PowerFSM.h" +#include "RadioInterface.h" +#include "modules/NodeInfoModule.h" + +PacketAPI *packetAPI = nullptr; + +PacketAPI *PacketAPI::create(PacketServer *_server) +{ + if (!packetAPI) { + packetAPI = new PacketAPI(_server); + } + return packetAPI; +} + +PacketAPI::PacketAPI(PacketServer *_server) + : concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server) +{ +} + +int32_t PacketAPI::runOnce() +{ + bool success = false; +#ifndef ARCH_PORTDUINO + if (config.bluetooth.enabled) { + if (!programmingMode) { + // in programmingMode we don't send any packets to the client except this one notify + programmingMode = true; + success = notifyProgrammingMode(); + } + } else +#endif + { + success = sendPacket(); + } + success |= receivePacket(); + return success ? 10 : 50; +} + +bool PacketAPI::receivePacket(void) +{ + bool data_received = false; + while (server->hasData()) { + isConnected = true; + data_received = true; + + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + lastContactMsec = millis(); + + meshtastic_ToRadio *mr; + auto p = server->receivePacket()->move(); + int id = p->getPacketId(); + LOG_DEBUG("Received packet id=%u", id); + mr = (meshtastic_ToRadio *)&static_cast *>(p.get())->getData(); + + switch (mr->which_payload_variant) { + case meshtastic_ToRadio_packet_tag: { + meshtastic_MeshPacket *mp = &mr->packet; + printPacket("PACKET FROM QUEUE", mp); + service->handleToRadio(*mp); + break; + } + case meshtastic_ToRadio_want_config_id_tag: { + uint32_t config_nonce = mr->want_config_id; + LOG_INFO("Screen wants config, nonce=%u", config_nonce); + handleStartConfig(); + break; + } + case meshtastic_ToRadio_heartbeat_tag: + if (mr->heartbeat.dummy_field == 1) { + if (nodeInfoModule) { + LOG_INFO("Broadcasting nodeinfo ping"); + nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true); + } + } else { + LOG_DEBUG("Got client heartbeat"); + } + break; + default: + LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant); + break; + } + } + return data_received; +} + +bool PacketAPI::sendPacket(void) +{ + // fill dummy buffer; we don't use it, we directly send the fromRadio structure + uint32_t len = getFromRadio(txBuf); + if (len != 0) { + static uint32_t id = 0; + fromRadioScratch.id = ++id; + bool result = server->sendPacket(DataPacket(id, fromRadioScratch)); + if (!result) { + LOG_ERROR("send queue full"); + } + return result; + } else + return false; +} + +bool PacketAPI::notifyProgrammingMode(void) +{ + // tell the client we are in programming mode by sending only the bluetooth config state + LOG_INFO("force client into programmingMode"); + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.id = nodeDB->getNodeNum(); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; + fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; + fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; + return server->sendPacket(DataPacket(0, fromRadioScratch)); +} + +/** + * return true if we got (once!) contact from our client and the server send queue is not full + */ +bool PacketAPI::checkIsConnected() +{ + isConnected |= server->hasData(); + return isConnected && server->available(); +} + +#endif \ No newline at end of file diff --git a/src/mesh/api/PacketAPI.h b/src/mesh/api/PacketAPI.h new file mode 100644 index 000000000..fc08ab209 --- /dev/null +++ b/src/mesh/api/PacketAPI.h @@ -0,0 +1,38 @@ +#pragma once + +#include "PhoneAPI.h" +#include "comms/PacketServer.h" +#include "concurrency/OSThread.h" + +/** + * A version of the phone API used for inter task communication based on protobuf packets, e.g. + * between two tasks running on CPU0 and CPU1, respectively. + * + */ +class PacketAPI : public PhoneAPI, public concurrency::OSThread +{ + public: + static PacketAPI *create(PacketServer *_server); + virtual ~PacketAPI(){}; + virtual int32_t runOnce(); + + protected: + PacketAPI(PacketServer *_server); + // Check the current underlying physical queue to see if the client is fetching packets + bool checkIsConnected() override; + + void onNowHasData(uint32_t fromRadioNum) override {} + void onConnectionChanged(bool connected) override {} + + private: + bool receivePacket(void); + bool sendPacket(void); + bool notifyProgrammingMode(void); + + bool isConnected; + bool programmingMode; + PacketServer *server; + uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI +}; + +extern PacketAPI *packetAPI; \ No newline at end of file diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index e28e4c815..1a506421c 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -51,6 +51,8 @@ template int32_t APIServerPort::runOnce() #else auto client = U::available(); #endif +#elif defined(ARCH_RP2040) + auto client = U::accept(); #else auto client = U::available(); #endif @@ -78,4 +80,4 @@ template int32_t APIServerPort::runOnce() waitTime = 100; #endif return 100; // only check occasionally for incoming connections -} +} \ No newline at end of file diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 7ce3c74ce..2e527f669 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -20,3 +20,5 @@ PB_BIND(meshtastic_NodeRemoteHardwarePinsResponse, meshtastic_NodeRemoteHardware + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index d9b8de384..02d50127e 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -70,6 +70,13 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType { meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12 } meshtastic_AdminMessage_ModuleConfigType; +typedef enum _meshtastic_AdminMessage_BackupLocation { + /* Backup to the internal flash */ + meshtastic_AdminMessage_BackupLocation_FLASH = 0, + /* Backup to the SD card */ + meshtastic_AdminMessage_BackupLocation_SD = 1 +} meshtastic_AdminMessage_BackupLocation; + /* Struct definitions */ /* Parameters for setting up Meshtastic for ameteur radio usage */ typedef struct _meshtastic_HamParameters { @@ -145,6 +152,12 @@ typedef struct _meshtastic_AdminMessage { char delete_file_request[201]; /* Set zero and offset for scale chips */ uint32_t set_scale; + /* Backup the node's preferences */ + meshtastic_AdminMessage_BackupLocation backup_preferences; + /* Restore the node's preferences */ + meshtastic_AdminMessage_BackupLocation restore_preferences; + /* Remove backups of the node's preferences */ + meshtastic_AdminMessage_BackupLocation remove_backup_preferences; /* Set the owner for this node */ meshtastic_User set_owner; /* Set channels (using the new API). @@ -226,8 +239,15 @@ extern "C" { #define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG #define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG+1)) +#define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH +#define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD +#define _meshtastic_AdminMessage_BackupLocation_ARRAYSIZE ((meshtastic_AdminMessage_BackupLocation)(meshtastic_AdminMessage_BackupLocation_SD+1)) + #define meshtastic_AdminMessage_payload_variant_get_config_request_ENUMTYPE meshtastic_AdminMessage_ConfigType #define meshtastic_AdminMessage_payload_variant_get_module_config_request_ENUMTYPE meshtastic_AdminMessage_ModuleConfigType +#define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation +#define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation +#define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation @@ -268,6 +288,9 @@ extern "C" { #define meshtastic_AdminMessage_enter_dfu_mode_request_tag 21 #define meshtastic_AdminMessage_delete_file_request_tag 22 #define meshtastic_AdminMessage_set_scale_tag 23 +#define meshtastic_AdminMessage_backup_preferences_tag 24 +#define meshtastic_AdminMessage_restore_preferences_tag 25 +#define meshtastic_AdminMessage_remove_backup_preferences_tag 26 #define meshtastic_AdminMessage_set_owner_tag 32 #define meshtastic_AdminMessage_set_channel_tag 33 #define meshtastic_AdminMessage_set_config_tag 34 @@ -320,6 +343,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_node_remote_hardware_pin X(a, STATIC, ONEOF, BOOL, (payload_variant,enter_dfu_mode_request,enter_dfu_mode_request), 21) \ X(a, STATIC, ONEOF, STRING, (payload_variant,delete_file_request,delete_file_request), 22) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,set_scale,set_scale), 23) \ +X(a, STATIC, ONEOF, UENUM, (payload_variant,backup_preferences,backup_preferences), 24) \ +X(a, STATIC, ONEOF, UENUM, (payload_variant,restore_preferences,restore_preferences), 25) \ +X(a, STATIC, ONEOF, UENUM, (payload_variant,remove_backup_preferences,remove_backup_preferences), 26) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_owner,set_owner), 32) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_channel,set_channel), 33) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_config,set_config), 34) \ diff --git a/src/mesh/generated/meshtastic/device_ui.pb.cpp b/src/mesh/generated/meshtastic/device_ui.pb.cpp index 3a9e28725..4bb3cc66c 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.cpp +++ b/src/mesh/generated/meshtastic/device_ui.pb.cpp @@ -15,6 +15,12 @@ PB_BIND(meshtastic_NodeFilter, meshtastic_NodeFilter, AUTO) PB_BIND(meshtastic_NodeHighlight, meshtastic_NodeHighlight, AUTO) +PB_BIND(meshtastic_GeoPoint, meshtastic_GeoPoint, AUTO) + + +PB_BIND(meshtastic_Map, meshtastic_Map, AUTO) + + diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index f090b5b4f..8cfc0b8cd 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -90,6 +90,25 @@ typedef struct _meshtastic_NodeHighlight { char node_name[16]; } meshtastic_NodeHighlight; +typedef struct _meshtastic_GeoPoint { + /* Zoom level */ + int8_t zoom; + /* Coordinate: latitude */ + int32_t latitude; + /* Coordinate: longitude */ + int32_t longitude; +} meshtastic_GeoPoint; + +typedef struct _meshtastic_Map { + /* Home coordinates */ + bool has_home; + meshtastic_GeoPoint home; + /* Map tile style */ + char style[20]; + /* Map scroll follows GPS */ + bool follow_gps; +} meshtastic_Map; + typedef PB_BYTES_ARRAY_T(16) meshtastic_DeviceUIConfig_calibration_data_t; typedef struct _meshtastic_DeviceUIConfig { /* A version integer used to invalidate saved files when we make incompatible changes. */ @@ -118,6 +137,9 @@ typedef struct _meshtastic_DeviceUIConfig { meshtastic_NodeHighlight node_highlight; /* 8 integers for screen calibration data */ meshtastic_DeviceUIConfig_calibration_data_t calibration_data; + /* Map related data */ + bool has_map_data; + meshtastic_Map map_data; } meshtastic_DeviceUIConfig; @@ -140,13 +162,19 @@ extern "C" { + + /* Initializer values for message structs */ -#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}} +#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}, false, meshtastic_Map_init_default} #define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_default {0, 0, 0, 0, ""} -#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}} +#define meshtastic_GeoPoint_init_default {0, 0, 0} +#define meshtastic_Map_init_default {false, meshtastic_GeoPoint_init_default, "", 0} +#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}, false, meshtastic_Map_init_zero} #define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_zero {0, 0, 0, 0, ""} +#define meshtastic_GeoPoint_init_zero {0, 0, 0} +#define meshtastic_Map_init_zero {false, meshtastic_GeoPoint_init_zero, "", 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_NodeFilter_unknown_switch_tag 1 @@ -161,6 +189,12 @@ extern "C" { #define meshtastic_NodeHighlight_telemetry_switch_tag 3 #define meshtastic_NodeHighlight_iaq_switch_tag 4 #define meshtastic_NodeHighlight_node_name_tag 5 +#define meshtastic_GeoPoint_zoom_tag 1 +#define meshtastic_GeoPoint_latitude_tag 2 +#define meshtastic_GeoPoint_longitude_tag 3 +#define meshtastic_Map_home_tag 1 +#define meshtastic_Map_style_tag 2 +#define meshtastic_Map_follow_gps_tag 3 #define meshtastic_DeviceUIConfig_version_tag 1 #define meshtastic_DeviceUIConfig_screen_brightness_tag 2 #define meshtastic_DeviceUIConfig_screen_timeout_tag 3 @@ -175,6 +209,7 @@ extern "C" { #define meshtastic_DeviceUIConfig_node_filter_tag 12 #define meshtastic_DeviceUIConfig_node_highlight_tag 13 #define meshtastic_DeviceUIConfig_calibration_data_tag 14 +#define meshtastic_DeviceUIConfig_map_data_tag 15 /* Struct field encoding specification for nanopb */ #define meshtastic_DeviceUIConfig_FIELDLIST(X, a) \ @@ -191,11 +226,13 @@ X(a, STATIC, SINGULAR, UINT32, ring_tone_id, 10) \ X(a, STATIC, SINGULAR, UENUM, language, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, node_filter, 12) \ X(a, STATIC, OPTIONAL, MESSAGE, node_highlight, 13) \ -X(a, STATIC, SINGULAR, BYTES, calibration_data, 14) +X(a, STATIC, SINGULAR, BYTES, calibration_data, 14) \ +X(a, STATIC, OPTIONAL, MESSAGE, map_data, 15) #define meshtastic_DeviceUIConfig_CALLBACK NULL #define meshtastic_DeviceUIConfig_DEFAULT NULL #define meshtastic_DeviceUIConfig_node_filter_MSGTYPE meshtastic_NodeFilter #define meshtastic_DeviceUIConfig_node_highlight_MSGTYPE meshtastic_NodeHighlight +#define meshtastic_DeviceUIConfig_map_data_MSGTYPE meshtastic_Map #define meshtastic_NodeFilter_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, unknown_switch, 1) \ @@ -217,18 +254,39 @@ X(a, STATIC, SINGULAR, STRING, node_name, 5) #define meshtastic_NodeHighlight_CALLBACK NULL #define meshtastic_NodeHighlight_DEFAULT NULL +#define meshtastic_GeoPoint_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, INT32, zoom, 1) \ +X(a, STATIC, SINGULAR, INT32, latitude, 2) \ +X(a, STATIC, SINGULAR, INT32, longitude, 3) +#define meshtastic_GeoPoint_CALLBACK NULL +#define meshtastic_GeoPoint_DEFAULT NULL + +#define meshtastic_Map_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, home, 1) \ +X(a, STATIC, SINGULAR, STRING, style, 2) \ +X(a, STATIC, SINGULAR, BOOL, follow_gps, 3) +#define meshtastic_Map_CALLBACK NULL +#define meshtastic_Map_DEFAULT NULL +#define meshtastic_Map_home_MSGTYPE meshtastic_GeoPoint + extern const pb_msgdesc_t meshtastic_DeviceUIConfig_msg; extern const pb_msgdesc_t meshtastic_NodeFilter_msg; extern const pb_msgdesc_t meshtastic_NodeHighlight_msg; +extern const pb_msgdesc_t meshtastic_GeoPoint_msg; +extern const pb_msgdesc_t meshtastic_Map_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_DeviceUIConfig_fields &meshtastic_DeviceUIConfig_msg #define meshtastic_NodeFilter_fields &meshtastic_NodeFilter_msg #define meshtastic_NodeHighlight_fields &meshtastic_NodeHighlight_msg +#define meshtastic_GeoPoint_fields &meshtastic_GeoPoint_msg +#define meshtastic_Map_fields &meshtastic_Map_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_MAX_SIZE meshtastic_DeviceUIConfig_size -#define meshtastic_DeviceUIConfig_size 128 +#define meshtastic_DeviceUIConfig_size 188 +#define meshtastic_GeoPoint_size 33 +#define meshtastic_Map_size 58 #define meshtastic_NodeFilter_size 47 #define meshtastic_NodeHighlight_size 25 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.cpp b/src/mesh/generated/meshtastic/deviceonly.pb.cpp index aa020467a..5a9695702 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.cpp +++ b/src/mesh/generated/meshtastic/deviceonly.pb.cpp @@ -18,7 +18,13 @@ PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO) PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2) +PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO) + + PB_BIND(meshtastic_ChannelFile, meshtastic_ChannelFile, 2) +PB_BIND(meshtastic_BackupPreferences, meshtastic_BackupPreferences, 2) + + diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index c0a0fee91..83563a9fc 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -9,6 +9,7 @@ #include "meshtastic/mesh.pb.h" #include "meshtastic/telemetry.pb.h" #include "meshtastic/config.pb.h" +#include "meshtastic/localonly.pb.h" #if PB_PROTO_HEADER_VERSION != 40 #error Regenerate this file with the current version of nanopb generator. @@ -122,7 +123,8 @@ typedef struct _meshtastic_DeviceState { Indicates developer is testing and changes should never be saved to flash. Deprecated in 2.3.1 */ bool no_save; - /* Some GPS receivers seem to have bogus settings from the factory, so we always do one factory reset. */ + /* Previously used to manage GPS factory resets. + Deprecated in 2.5.23 */ bool did_gps_reset; /* We keep the last received waypoint stored in the device flash, so we can show it on the screen. @@ -132,10 +134,17 @@ typedef struct _meshtastic_DeviceState { /* The mesh's nodes with their available gpio pins for RemoteHardware module */ pb_size_t node_remote_hardware_pins_count; meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12]; - /* New lite version of NodeDB to decrease memory footprint */ - std::vector node_db_lite; } meshtastic_DeviceState; +typedef struct _meshtastic_NodeDatabase { + /* A version integer used to invalidate old save files when we make + incompatible changes This integer is set at build time and is private to + NodeDB.cpp in the device code. */ + uint32_t version; + /* New lite version of NodeDB to decrease memory footprint */ + std::vector nodes; +} meshtastic_NodeDatabase; + /* The on-disk saved channels */ typedef struct _meshtastic_ChannelFile { /* The channels our node knows about */ @@ -147,6 +156,26 @@ typedef struct _meshtastic_ChannelFile { uint32_t version; } meshtastic_ChannelFile; +/* The on-disk backup of the node's preferences */ +typedef struct _meshtastic_BackupPreferences { + /* The version of the backup */ + uint32_t version; + /* The timestamp of the backup (if node has time) */ + uint32_t timestamp; + /* The node's configuration */ + bool has_config; + meshtastic_LocalConfig config; + /* The node's module configuration */ + bool has_module_config; + meshtastic_LocalModuleConfig module_config; + /* The node's channels */ + bool has_channels; + meshtastic_ChannelFile channels; + /* The node's user (owner) information */ + bool has_owner; + meshtastic_User owner; +} meshtastic_BackupPreferences; + #ifdef __cplusplus extern "C" { @@ -156,13 +185,17 @@ extern "C" { #define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} #define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}, {0}} +#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} +#define meshtastic_NodeDatabase_init_default {0, {0}} #define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0} +#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default} #define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} #define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}, {0}} +#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} +#define meshtastic_NodeDatabase_init_zero {0, {0}} #define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0} +#define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_PositionLite_latitude_i_tag 1 @@ -198,9 +231,16 @@ extern "C" { #define meshtastic_DeviceState_did_gps_reset_tag 11 #define meshtastic_DeviceState_rx_waypoint_tag 12 #define meshtastic_DeviceState_node_remote_hardware_pins_tag 13 -#define meshtastic_DeviceState_node_db_lite_tag 14 +#define meshtastic_NodeDatabase_version_tag 1 +#define meshtastic_NodeDatabase_nodes_tag 2 #define meshtastic_ChannelFile_channels_tag 1 #define meshtastic_ChannelFile_version_tag 2 +#define meshtastic_BackupPreferences_version_tag 1 +#define meshtastic_BackupPreferences_timestamp_tag 2 +#define meshtastic_BackupPreferences_config_tag 3 +#define meshtastic_BackupPreferences_module_config_tag 4 +#define meshtastic_BackupPreferences_channels_tag 5 +#define meshtastic_BackupPreferences_owner_tag 6 /* Struct field encoding specification for nanopb */ #define meshtastic_PositionLite_FIELDLIST(X, a) \ @@ -251,10 +291,8 @@ X(a, STATIC, SINGULAR, UINT32, version, 8) \ X(a, STATIC, SINGULAR, BOOL, no_save, 9) \ X(a, STATIC, SINGULAR, BOOL, did_gps_reset, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, rx_waypoint, 12) \ -X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13) \ -X(a, CALLBACK, REPEATED, MESSAGE, node_db_lite, 14) -extern bool meshtastic_DeviceState_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); -#define meshtastic_DeviceState_CALLBACK meshtastic_DeviceState_callback +X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13) +#define meshtastic_DeviceState_CALLBACK NULL #define meshtastic_DeviceState_DEFAULT NULL #define meshtastic_DeviceState_my_node_MSGTYPE meshtastic_MyNodeInfo #define meshtastic_DeviceState_owner_MSGTYPE meshtastic_User @@ -262,7 +300,14 @@ extern bool meshtastic_DeviceState_callback(pb_istream_t *istream, pb_ostream_t #define meshtastic_DeviceState_rx_text_message_MSGTYPE meshtastic_MeshPacket #define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket #define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin -#define meshtastic_DeviceState_node_db_lite_MSGTYPE meshtastic_NodeInfoLite + +#define meshtastic_NodeDatabase_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, version, 1) \ +X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) +extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); +#define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback +#define meshtastic_NodeDatabase_DEFAULT NULL +#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite #define meshtastic_ChannelFile_FIELDLIST(X, a) \ X(a, STATIC, REPEATED, MESSAGE, channels, 1) \ @@ -271,23 +316,43 @@ X(a, STATIC, SINGULAR, UINT32, version, 2) #define meshtastic_ChannelFile_DEFAULT NULL #define meshtastic_ChannelFile_channels_MSGTYPE meshtastic_Channel +#define meshtastic_BackupPreferences_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, version, 1) \ +X(a, STATIC, SINGULAR, FIXED32, timestamp, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, config, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, module_config, 4) \ +X(a, STATIC, OPTIONAL, MESSAGE, channels, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, owner, 6) +#define meshtastic_BackupPreferences_CALLBACK NULL +#define meshtastic_BackupPreferences_DEFAULT NULL +#define meshtastic_BackupPreferences_config_MSGTYPE meshtastic_LocalConfig +#define meshtastic_BackupPreferences_module_config_MSGTYPE meshtastic_LocalModuleConfig +#define meshtastic_BackupPreferences_channels_MSGTYPE meshtastic_ChannelFile +#define meshtastic_BackupPreferences_owner_MSGTYPE meshtastic_User + extern const pb_msgdesc_t meshtastic_PositionLite_msg; extern const pb_msgdesc_t meshtastic_UserLite_msg; extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg; extern const pb_msgdesc_t meshtastic_DeviceState_msg; +extern const pb_msgdesc_t meshtastic_NodeDatabase_msg; extern const pb_msgdesc_t meshtastic_ChannelFile_msg; +extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_PositionLite_fields &meshtastic_PositionLite_msg #define meshtastic_UserLite_fields &meshtastic_UserLite_msg #define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg #define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg +#define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg #define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg +#define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg /* Maximum encoded size of messages (where known) */ -/* meshtastic_DeviceState_size depends on runtime parameters */ -#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_ChannelFile_size +/* meshtastic_NodeDatabase_size depends on runtime parameters */ +#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size +#define meshtastic_BackupPreferences_size 2263 #define meshtastic_ChannelFile_size 718 +#define meshtastic_DeviceState_size 1720 #define meshtastic_NodeInfoLite_size 188 #define meshtastic_PositionLite_size 28 #define meshtastic_UserLite_size 96 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index 7a6712bf0..6a59b8eb0 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -188,7 +188,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size #define meshtastic_LocalConfig_size 743 -#define meshtastic_LocalModuleConfig_size 699 +#define meshtastic_LocalModuleConfig_size 667 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index de8a1a353..3353a020f 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -1775,4 +1775,4 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; } /* extern "C" */ #endif -#endif \ No newline at end of file +#endif diff --git a/src/mesh/generated/meshtastic/module_config.pb.cpp b/src/mesh/generated/meshtastic/module_config.pb.cpp index 9843f0e91..f262df6a3 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.cpp +++ b/src/mesh/generated/meshtastic/module_config.pb.cpp @@ -6,10 +6,10 @@ #error Regenerate this file with the current version of nanopb generator. #endif -PB_BIND(meshtastic_ModuleConfig, meshtastic_ModuleConfig, 2) +PB_BIND(meshtastic_ModuleConfig, meshtastic_ModuleConfig, AUTO) -PB_BIND(meshtastic_ModuleConfig_MQTTConfig, meshtastic_ModuleConfig_MQTTConfig, 2) +PB_BIND(meshtastic_ModuleConfig_MQTTConfig, meshtastic_ModuleConfig_MQTTConfig, AUTO) PB_BIND(meshtastic_ModuleConfig_MapReportSettings, meshtastic_ModuleConfig_MapReportSettings, AUTO) diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 848b010d3..d5031cb89 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -126,7 +126,7 @@ typedef struct _meshtastic_ModuleConfig_MQTTConfig { /* MQTT password to use (most useful for a custom MQTT server). If using a custom server, this will be honoured even if empty. If using the default server, this will only be honoured if set, otherwise the device will use the default password */ - char password[64]; + char password[32]; /* Whether to send encrypted or decrypted packets to MQTT. This parameter is only honoured if you also set server (the default official mqtt.meshtastic.org server can handle encrypted packets) @@ -887,7 +887,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_CannedMessageConfig_size 49 #define meshtastic_ModuleConfig_DetectionSensorConfig_size 44 #define meshtastic_ModuleConfig_ExternalNotificationConfig_size 42 -#define meshtastic_ModuleConfig_MQTTConfig_size 254 +#define meshtastic_ModuleConfig_MQTTConfig_size 222 #define meshtastic_ModuleConfig_MapReportSettings_size 12 #define meshtastic_ModuleConfig_NeighborInfoConfig_size 10 #define meshtastic_ModuleConfig_PaxcounterConfig_size 30 @@ -896,7 +896,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 #define meshtastic_ModuleConfig_TelemetryConfig_size 46 -#define meshtastic_ModuleConfig_size 257 +#define meshtastic_ModuleConfig_size 225 #define meshtastic_RemoteHardwarePin_size 21 #ifdef __cplusplus diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 039b36d8d..f91c48560 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -23,8 +23,6 @@ #define MAX_NUM_NODES 100 #endif -#define MAX_NUM_NODES_FS 100 - /// Max number of channels allowed #define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0])) diff --git a/src/mesh/udp/UdpMulticastThread.h b/src/mesh/udp/UdpMulticastThread.h new file mode 100644 index 000000000..9128d3b5c --- /dev/null +++ b/src/mesh/udp/UdpMulticastThread.h @@ -0,0 +1,70 @@ +#pragma once +#if HAS_UDP_MULTICAST +#include "configuration.h" +#include "main.h" +#include "mesh/Router.h" + +#include +#include + +#define UDP_MULTICAST_DEFAUL_PORT 4403 // Default port for UDP multicast is same as TCP api server +#define UDP_MULTICAST_THREAD_INTERVAL_MS 15000 + +class UdpMulticastThread : public concurrency::OSThread +{ + public: + UdpMulticastThread() : OSThread("UdpMulticast") { udpIpAddress = IPAddress(224, 0, 0, 69); } + + void start() + { + if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT)) { + LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); + udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); + } else { + LOG_DEBUG("Failed to listen on UDP"); + } + } + + void onReceive(AsyncUDPPacket packet) + { + size_t packetLength = packet.length(); + LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); + meshtastic_MeshPacket mp; + uint8_t bytes[meshtastic_MeshPacket_size]; // Allocate buffer for the data + size_t packetSize = packet.readBytes(bytes, packet.length()); + LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetSize); + bool isPacketDecoded = pb_decode_from_bytes(bytes, packetLength, &meshtastic_MeshPacket_msg, &mp); + if (isPacketDecoded && router) { + UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); + // Unset received SNR/RSSI + p->rx_snr = 0; + p->rx_rssi = 0; + router->enqueueReceivedMessage(p.release()); + } + } + + bool onSend(const meshtastic_MeshPacket *mp) + { + if (!mp || WiFi.status() != WL_CONNECTED) { + return false; + } + LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); + uint8_t buffer[meshtastic_MeshPacket_size]; + size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); + udp.broadcastTo(buffer, encodedLength, UDP_MULTICAST_DEFAUL_PORT); + return true; + } + + protected: + int32_t runOnce() override + { + canSleep = true; + // TODO: Implement nodeinfo broadcast + return UDP_MULTICAST_THREAD_INTERVAL_MS; + } + + private: + IPAddress udpIpAddress; + AsyncUDP udp; +}; +#endif // ARCH_ESP32 \ No newline at end of file diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index d4a5dbf94..ee50ee56f 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -108,6 +108,12 @@ static void onNetworkConnected() #endif APStartupComplete = true; } + +#if HAS_UDP_MULTICAST + if (udpThread) { + udpThread->start(); + } +#endif } static int32_t reconnectWiFi() @@ -428,4 +434,4 @@ uint8_t getWifiDisconnectReason() { return wifiDisconnectReason; } -#endif +#endif \ No newline at end of file diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 72ac39db7..913374bbc 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -123,23 +123,23 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta * Getters */ case meshtastic_AdminMessage_get_owner_request_tag: - LOG_INFO("Client got owner"); + LOG_DEBUG("Client got owner"); handleGetOwner(mp); break; case meshtastic_AdminMessage_get_config_request_tag: - LOG_INFO("Client got config"); + LOG_DEBUG("Client got config"); handleGetConfig(mp, r->get_config_request); break; case meshtastic_AdminMessage_get_module_config_request_tag: - LOG_INFO("Client got module config"); + LOG_DEBUG("Client got module config"); handleGetModuleConfig(mp, r->get_module_config_request); break; case meshtastic_AdminMessage_get_channel_request_tag: { uint32_t i = r->get_channel_request - 1; - LOG_INFO("Client got channel %u", i); + LOG_DEBUG("Client got channel %u", i); if (i >= MAX_NUM_CHANNELS) myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); else @@ -151,35 +151,35 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta * Setters */ case meshtastic_AdminMessage_set_owner_tag: - LOG_INFO("Client set owner"); + LOG_DEBUG("Client set owner"); handleSetOwner(r->set_owner); break; case meshtastic_AdminMessage_set_config_tag: - LOG_INFO("Client set config"); + LOG_DEBUG("Client set config"); handleSetConfig(r->set_config); break; case meshtastic_AdminMessage_set_module_config_tag: - LOG_INFO("Client set module config"); + LOG_DEBUG("Client set module config"); if (!handleSetModuleConfig(r->set_module_config)) { myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } break; case meshtastic_AdminMessage_set_channel_tag: - LOG_INFO("Client set channel %d", r->set_channel.index); + LOG_DEBUG("Client set channel %d", r->set_channel.index); if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); else handleSetChannel(r->set_channel); break; case meshtastic_AdminMessage_set_ham_mode_tag: - LOG_INFO("Client set ham mode"); + LOG_DEBUG("Client set ham mode"); handleSetHamMode(r->set_ham_mode); break; case meshtastic_AdminMessage_get_ui_config_request_tag: { - LOG_INFO("Client is getting device-ui config"); + LOG_DEBUG("Client is getting device-ui config"); handleGetDeviceUIConfig(mp); handled = true; break; @@ -285,7 +285,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); if (node != NULL) { node->is_favorite = true; - saveChanges(SEGMENT_DEVICESTATE, false); + saveChanges(SEGMENT_NODEDATABASE, false); } break; } @@ -294,7 +294,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); if (node != NULL) { node->is_favorite = false; - saveChanges(SEGMENT_DEVICESTATE, false); + saveChanges(SEGMENT_NODEDATABASE, false); } break; } @@ -307,7 +307,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta node->has_position = false; node->user.public_key.size = 0; node->user.public_key.bytes[0] = 0; - saveChanges(SEGMENT_DEVICESTATE, false); + saveChanges(SEGMENT_NODEDATABASE, false); } break; } @@ -316,7 +316,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); if (node != NULL) { node->is_ignored = false; - saveChanges(SEGMENT_DEVICESTATE, false); + saveChanges(SEGMENT_NODEDATABASE, false); } break; } @@ -327,7 +327,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); nodeDB->setLocalPosition(r->set_fixed_position); config.position.fixed_position = true; - saveChanges(SEGMENT_DEVICESTATE | SEGMENT_CONFIG, false); + saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); #if !MESHTASTIC_EXCLUDE_GPS if (gps != nullptr) gps->enable(); @@ -340,7 +340,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_INFO("Client received remove_fixed_position command"); nodeDB->clearLocalPosition(); config.position.fixed_position = false; - saveChanges(SEGMENT_DEVICESTATE | SEGMENT_CONFIG, false); + saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); break; } case meshtastic_AdminMessage_set_time_only_tag: { @@ -391,7 +391,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant); } else if (handleResult != AdminMessageHandleResult::HANDLED) { // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages - LOG_INFO("Ignore irrelevant admin %d", r->which_payload_variant); + LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant); } break; } @@ -450,11 +450,14 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) if (owner.is_licensed != o.is_licensed) { changed = 1; owner.is_licensed = o.is_licensed; + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } } if (changed) { // If nothing really changed, don't broadcast on the network or write to flash service->reloadOwner(!hasOpenEditTransaction); - saveChanges(SEGMENT_DEVICESTATE); + saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE); } } @@ -748,6 +751,9 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) void AdminModule::handleSetChannel(const meshtastic_Channel &cc) { channels.setChannel(cc); + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } channels.onConfigChanged(); // tell the radios about this change saveChanges(SEGMENT_CHANNELS, false); } @@ -1085,15 +1091,14 @@ void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; // Remove PSK of primary channel for plaintext amateur usage - auto primaryChannel = channels.getByIndex(channels.getPrimaryIndex()); - auto &channelSettings = primaryChannel.settings; - channelSettings.psk.bytes[0] = 0; - channelSettings.psk.size = 0; - channels.setChannel(primaryChannel); + + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } channels.onConfigChanged(); service->reloadOwner(false); - saveChanges(SEGMENT_CONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); } AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg) @@ -1179,4 +1184,4 @@ void disableBluetooth() nrf52Bluetooth->shutdown(); #endif #endif -} \ No newline at end of file +} diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 12c857e04..246d39e37 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -64,6 +64,9 @@ class AdminModule : public ProtobufModule, public Obser void sendWarning(const char *message); }; +static constexpr const char *licensedModeMessage = + "Licensed mode activated, removing admin channel and encryption from all channels"; + extern AdminModule *adminModule; void disableBluetooth(); \ No newline at end of file diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index a501e319b..34ef2ddd1 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -20,6 +20,11 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) return false; + } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { + // Don't let licensed users to rebroadcast packets from unlicensed users + // If we know they are in-fact unlicensed + LOG_DEBUG("Packet from unlicensed user, ignoring packet"); + return false; } printPacket("Routing sniffing", &mp); diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 04bcbe200..14901f0af 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -31,7 +31,6 @@ int32_t PowerTelemetryModule::runOnce() doDeepSleep(nightyNightMs, true, false); } - uint32_t result = UINT32_MAX; /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. @@ -46,25 +45,33 @@ int32_t PowerTelemetryModule::runOnce() return disable(); } + uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes); + if (firstTime) { // This is the first time the OSThread library has called this function, so do some setup firstTime = 0; + uint32_t result = UINT32_MAX; + #if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) if (moduleConfig.telemetry.power_measurement_enabled) { LOG_INFO("Power Telemetry: init"); - // it's possible to have this module enabled, only for displaying values on the screen. - // therefore, we should only enable the sensor loop if measurement is also enabled - if (ina219Sensor.hasSensor() && !ina219Sensor.isInitialized()) - result = ina219Sensor.runOnce(); - if (ina226Sensor.hasSensor() && !ina226Sensor.isInitialized()) - result = ina226Sensor.runOnce(); - if (ina260Sensor.hasSensor() && !ina260Sensor.isInitialized()) - result = ina260Sensor.runOnce(); - if (ina3221Sensor.hasSensor() && !ina3221Sensor.isInitialized()) - result = ina3221Sensor.runOnce(); - if (max17048Sensor.hasSensor() && !max17048Sensor.isInitialized()) - result = max17048Sensor.runOnce(); + // If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again, + // but we need to set the result to != UINT32_MAX to avoid it being disabled + if (ina219Sensor.hasSensor()) + result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce(); + if (ina226Sensor.hasSensor()) + result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce(); + if (ina260Sensor.hasSensor()) + result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor()) + result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce(); + if (max17048Sensor.hasSensor()) + result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); } + + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled return result == UINT32_MAX ? disable() : setStartDelay(); #else return disable(); @@ -74,10 +81,7 @@ int32_t PowerTelemetryModule::runOnce() if (!moduleConfig.telemetry.power_measurement_enabled) return disable(); - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.power_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); lastSentToMesh = millis(); @@ -89,8 +93,9 @@ int32_t PowerTelemetryModule::runOnce() lastSentToPhone = millis(); } } - return min(sendToPhoneIntervalMs, result); + return min(sendToPhoneIntervalMs, sendToMeshIntervalMs); } + bool PowerTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.power_screen_enabled; diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 79b14de0a..41cb35649 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -109,7 +109,7 @@ void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, floa void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) { #ifdef DEBUG_PORT - std::string route = "Route traced:"; + std::string route = "Route traced:\n"; route += vformat("0x%x --> ", origin); for (uint8_t i = 0; i < r->route_count; i++) { if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) @@ -129,6 +129,7 @@ void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, // If there's a route back (or we are the destination as then the route is complete), print it if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) { + route += "\n"; if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum()) route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin); else @@ -150,6 +151,12 @@ meshtastic_MeshPacket *TraceRouteModule::allocReply() { assert(currentRequest); + // Ignore multi-hop broadcast requests + if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { + ignoreRequest = true; + return NULL; + } + // Copy the payload of the current request auto req = *currentRequest; const auto &p = req.decoded; diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 08b48b682..479a973c2 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -144,9 +144,9 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, bearingToOther -= myHeading; screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2*PI : bearingToOther; - bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; - + float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; + bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; + // Distance to Waypoint float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { @@ -161,7 +161,6 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees); } - } // If our node doesn't have position diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 2662ef0bc..009439f25 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -26,7 +26,7 @@ class BluetoothPhoneAPI : public PhoneAPI { PhoneAPI::onNowHasData(fromRadioNum); - LOG_INFO("BLE notify fromNum"); + LOG_DEBUG("BLE notify fromNum"); uint8_t val[4]; put_le32(val, fromRadioNum); @@ -51,7 +51,7 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { virtual void onWrite(NimBLECharacteristic *pCharacteristic) { - LOG_INFO("To Radio onwrite"); + LOG_DEBUG("To Radio onwrite"); auto val = pCharacteristic->getValue(); if (memcmp(lastToRadio, val.data(), val.length()) != 0) { @@ -91,7 +91,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); -#if HAS_SCREEN + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); + +#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { char btPIN[16] = "888888"; snprintf(btPIN, sizeof(btPIN), "%06u", passkey); @@ -127,6 +129,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE authentication complete"); + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { passkeyShowing = false; screen->endAlert(); @@ -137,6 +142,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE disconnect"); + bluetoothStatus->updateStatus( + new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); } @@ -298,4 +306,4 @@ void clearNVS() ESP.restart(); #endif } -#endif \ No newline at end of file +#endif diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index b98620f33..87d8adfa9 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -57,6 +57,9 @@ void onConnect(uint16_t conn_handle) char central_name[32] = {0}; connection->getPeerName(central_name, sizeof(central_name)); LOG_INFO("BLE Connected to %s", central_name); + + // Notify UI (or any other interested firmware components) + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); } /** * Callback invoked when a connection is dropped @@ -69,6 +72,9 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); } + + // Notify UI (or any other interested firmware components) + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -319,7 +325,17 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke { LOG_INFO("BLE pair process started with passkey %.3s %.3s", passkey, passkey + 3); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); -#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + + // Get passkey as string + // Note: possible leading zeros + std::string textkey; + for (uint8_t i = 0; i < 6; i++) + textkey += (char)passkey[i]; + + // Notify UI (or other components) of pairing event and passkey + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { char btPIN[16] = "888888"; snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); @@ -358,10 +374,18 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke } void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) { - if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) + if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - else + bluetoothStatus->updateStatus( + new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + } else { LOG_INFO("BLE pair failed"); + // Notify UI (or any other interested firmware components) + bluetoothStatus->updateStatus( + new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + } + + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus screen->endAlert(); } diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 7e63b995e..4e748c5f9 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -139,6 +139,12 @@ bool SimRadio::cancelSending(NodeNum from, PacketId id) return result; } +/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ +bool SimRadio::findInTxQueue(NodeNum from, PacketId id) +{ + return txQueue.find(from, id); +} + void SimRadio::onNotify(uint32_t notification) { switch (notification) { diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index c082444e5..ea534bd65 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -33,6 +33,9 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ virtual bool cancelSending(NodeNum from, PacketId id) override; + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) override; + /** * Start waiting to receive a message * diff --git a/src/shutdown.h b/src/shutdown.h index 9e30e772c..c2ba6f670 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -3,6 +3,7 @@ #include "graphics/Screen.h" #include "main.h" #include "power.h" +#include "sleep.h" #if defined(ARCH_PORTDUINO) #include "api/WiFiServerAPI.h" #include "input/LinuxInputImpl.h" @@ -13,6 +14,7 @@ void powerCommandsCheck() { if (rebootAtMsec && millis() > rebootAtMsec) { LOG_INFO("Rebooting"); + notifyReboot.notifyObservers(NULL); #if defined(ARCH_ESP32) ESP.restart(); #elif defined(ARCH_NRF52) diff --git a/src/sleep.cpp b/src/sleep.cpp index 437d7b88b..202b8c354 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -4,7 +4,6 @@ #include "GPS.h" #endif -#include "ButtonThread.h" #include "Default.h" #include "Led.h" #include "MeshRadio.h" @@ -39,9 +38,19 @@ esp_sleep_source_t wakeCause; // the reason we booted this time /// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen Observable preflightSleep; -/// Called to tell observers we are now entering sleep and you should prepare. Must return 0 -/// notifySleep will be called for light or deep sleep, notifyDeepSleep is only called for deep sleep -Observable notifySleep, notifyDeepSleep; +/// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0 +Observable notifyDeepSleep; + +/// Called to tell observers we are rebooting ASAP. Must return 0 +Observable notifyReboot; + +#ifdef ARCH_ESP32 +/// Called to tell observers that light sleep is about to begin +Observable notifyLightSleep; + +/// Called to tell observers that light sleep has just ended, and why it ended +Observable notifyLightSleepEnd; +#endif // deep sleep support RTC_DATA_ATTR int bootCount = 0; @@ -183,8 +192,6 @@ static void waitEnterSleep(bool skipPreflight = false) // Code that still needs to be moved into notifyObservers console->flush(); // send all our characters before we stop cpu clock setBluetoothEnable(false); // has to be off before calling light sleep - - notifySleep.notifyObservers(NULL); } void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false) @@ -206,11 +213,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #endif #ifdef ARCH_ESP32 - if (shouldLoraWake(msecToWake)) { - notifySleep.notifyObservers(NULL); - } else { + if (!shouldLoraWake(msecToWake)) notifyDeepSleep.notifyObservers(NULL); - } #else notifyDeepSleep.notifyObservers(NULL); #endif @@ -353,6 +357,7 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #endif waitEnterSleep(false); + notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here uint64_t sleepUsec = sleepMsec * 1000LL; @@ -388,9 +393,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); - // Have to *fully* detach the normal button-interrupts first - buttonThread->detachButtonInterrupts(); - gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); esp_sleep_enable_gpio_wakeup(); #endif @@ -429,7 +431,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #ifdef BUTTON_PIN // Disable wake-on-button interrupt. Re-attach normal button-interrupts gpio_wakeup_disable(pin); - buttonThread->attachButtonInterrupts(); #endif #ifdef T_WATCH_S3 @@ -448,6 +449,8 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #endif esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here + #ifdef BUTTON_PIN if (cause == ESP_SLEEP_WAKEUP_GPIO) { LOG_INFO("Exit light sleep gpio: btn=%d", diff --git a/src/sleep.h b/src/sleep.h index 8d3cb17e8..f780fb3c0 100644 --- a/src/sleep.h +++ b/src/sleep.h @@ -34,12 +34,20 @@ extern bool bluetoothOn; /// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen extern Observable preflightSleep; -/// Called to tell observers we are now entering (light or deep) sleep and you should prepare. Must return 0 -extern Observable notifySleep; - /// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0 extern Observable notifyDeepSleep; +/// Called to tell observers we are rebooting ASAP. Must return 0 +extern Observable notifyReboot; + +#ifdef ARCH_ESP32 +/// Called to tell observers that light sleep is about to begin +extern Observable notifyLightSleep; + +/// Called to tell observers that light sleep has just ended, and why it ended +extern Observable notifyLightSleepEnd; +#endif + void enableModemSleep(); #ifdef ARCH_ESP32 void enableLoraInterrupt(); diff --git a/test/test_crypto/test_main.cpp b/test/test_crypto/test_main.cpp index fd7706e6e..ac507116c 100644 --- a/test/test_crypto/test_main.cpp +++ b/test/test_crypto/test_main.cpp @@ -1,3 +1,4 @@ +// trunk-ignore-all(gitleaks): These are dummy values. Not real secrets. #include "CryptoEngine.h" #include "TestUtil.h" diff --git a/userPrefs.jsonc b/userPrefs.jsonc index de610464d..6a3fdbb55 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -27,9 +27,11 @@ // "USERPREFS_FIXED_GPS_ALT": "0", // "USERPREFS_FIXED_GPS_LAT": "48.85873920", // "USERPREFS_FIXED_GPS_LON": "2.294508368", + // "USERPREFS_CONFIG_SMART_POSITION_ENABLED": "false", + // "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600", + // "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800", // "USERPREFS_LORACONFIG_CHANNEL_NUM": "31", // "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST", - "USERPREFS_TZ_STRING": "tzplaceholder " // "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }", // "USERPREFS_USE_ADMIN_KEY_1": "{}", // "USERPREFS_USE_ADMIN_KEY_2": "{}", @@ -37,5 +39,6 @@ // "USERPREFS_OEM_FONT_SIZE": "0", // "USERPREFS_OEM_IMAGE_WIDTH": "50", // "USERPREFS_OEM_IMAGE_HEIGHT": "28", - // "USERPREFS_OEM_IMAGE_DATA": "{ 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x67, 0x00, 0x00, 0x00, 0x18, 0x1F, 0xF0, 0x67, 0x00, 0x00, 0x00, 0x30, 0x1F, 0xF8, 0x33, 0x00, 0x00, 0x00, 0x30, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x00, 0x60, 0x00, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x60, 0x00, 0x7E, 0x18, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x81, 0x1F, 0x06, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x0F, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x8F, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xC7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00}" + // "USERPREFS_OEM_IMAGE_DATA": "{ 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x67, 0x00, 0x00, 0x00, 0x18, 0x1F, 0xF0, 0x67, 0x00, 0x00, 0x00, 0x30, 0x1F, 0xF8, 0x33, 0x00, 0x00, 0x00, 0x30, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x00, 0x60, 0x00, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x60, 0x00, 0x7E, 0x18, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x81, 0x1F, 0x06, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x0F, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x8F, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xC7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00}", + "USERPREFS_TZ_STRING": "tzplaceholder " } diff --git a/variants/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/Dongle_nRF52840-pca10059-v1/platformio.ini index a98656e86..9e87fd237 100644 --- a/variants/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -10,5 +10,5 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/Dongle_nRF52840-pca10059-v build_src_filter = ${nrf52_base.build_src_filter} +<../variants/Dongle_nRF52840-pca10059-v1> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.4.9 -debug_tool = jlink \ No newline at end of file + zinggjm/GxEPD2@^1.6.2 +debug_tool = jlink diff --git a/variants/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/ME25LS01-4Y10TD_e-ink/platformio.ini index f2e3a49e3..62314040a 100644 --- a/variants/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -13,7 +13,7 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/ME25LS01-4Y10TD_e-ink> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.5.8 + zinggjm/GxEPD2@^1.6.2 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil -upload_port = /dev/ttyACM1 \ No newline at end of file +upload_port = /dev/ttyACM1 diff --git a/variants/MakePython_nRF52840_eink/platformio.ini b/variants/MakePython_nRF52840_eink/platformio.ini index b11b54c7d..b7ce97dcb 100644 --- a/variants/MakePython_nRF52840_eink/platformio.ini +++ b/variants/MakePython_nRF52840_eink/platformio.ini @@ -5,13 +5,13 @@ board = nordic_pca10059 build_flags = ${nrf52840_base.build_flags} -Ivariants/MakePython_nRF52840_eink -D PRIVATE_HW -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" -D PIN_EINK_EN + -DEINK_DISPLAY_MODEL=GxEPD2_290_T5D + -DEINK_WIDTH=296 + -DEINK_HEIGHT=128 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/MakePython_nRF52840_eink> lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f - zinggjm/GxEPD2@^1.4.9 - -DEINK_DISPLAY_MODEL=GxEPD2_290_T5D - -DEINK_WIDTH=296 - -DEINK_HEIGHT=128 + zinggjm/GxEPD2@^1.6.2 debug_tool = jlink -;upload_port = /dev/ttyACM4 +;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/TWC_mesh_v4/platformio.ini b/variants/TWC_mesh_v4/platformio.ini index 4fb382334..2eb58bf9f 100644 --- a/variants/TWC_mesh_v4/platformio.ini +++ b/variants/TWC_mesh_v4/platformio.ini @@ -6,5 +6,5 @@ build_flags = ${nrf52840_base.build_flags} -I variants/TWC_mesh_v4 -D TWC_mesh_v build_src_filter = ${nrf52_base.build_src_filter} +<../variants/TWC_mesh_v4> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.4.9 -debug_tool = jlink \ No newline at end of file + zinggjm/GxEPD2@^1.6.2 +debug_tool = jlink diff --git a/variants/esp32-s3-pico/platformio.ini b/variants/esp32-s3-pico/platformio.ini index 916f623bd..20a41ba56 100644 --- a/variants/esp32-s3-pico/platformio.ini +++ b/variants/esp32-s3-pico/platformio.ini @@ -21,5 +21,5 @@ build_flags = ${esp32s3_base.build_flags} -DEINK_HEIGHT=128 lib_deps = ${esp32s3_base.lib_deps} - zinggjm/GxEPD2@^1.5.3 + zinggjm/GxEPD2@^1.6.2 adafruit/Adafruit NeoPixel @ ^1.12.0 diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h new file mode 100644 index 000000000..f7a37fc61 --- /dev/null +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -0,0 +1,115 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// #include "graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h" +// #include "graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" +#include "graphics/niche/Inputs/TwoButton.h" + +#include "graphics/niche/Fonts/FreeSans6pt7b.h" +#include "graphics/niche/Fonts/FreeSans6pt8bCyrillic.h" +#include + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::LCMEN213EFC1; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance(); + + // Set the driver + windowManager->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + windowManager->setDisplayResilience(10, 1.5); + + // Prepare fonts + InkHUD::AppletFont largeFont(FreeSans9pt7b); + InkHUD::AppletFont smallFont(FreeSans6pt7b); + /* + // Font localization demo: Cyrillic + InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic); + smallFont.addSubstitutionsWin1251(); + */ + InkHUD::Applet::setDefaultFonts(largeFont, smallFont); + + // Init settings, and customize defaults + InkHUD::settings.userTiles.maxCount = 2; // How many tiles can the display handle? + InkHUD::settings.rotation = 3; // 270 degrees clockwise + InkHUD::settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + InkHUD::settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + windowManager->addApplet("DMs", new InkHUD::DMApplet); + windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet); + windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet); + // windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running window manager + windowManager->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + constexpr uint8_t MAIN_BUTTON = 0; + constexpr uint8_t AUX_BUTTON = 1; + + // Setup the main user button + buttons->setWiring(MAIN_BUTTON, BUTTON_PIN); + buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); }); + buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); }); + + // Setup the aux button + // Bonus feature of VME213 + buttons->setWiring(AUX_BUTTON, BUTTON_PIN_SECONDARY); + buttons->setHandlerShortPress(AUX_BUTTON, []() { InkHUD::WindowManager::getInstance()->nextTile(); }); + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini index cc6f283b5..3d8f2e7cd 100644 --- a/variants/heltec_vision_master_e213/platformio.ini +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -5,6 +5,7 @@ build_flags = ${esp32s3_base.build_flags} -Ivariants/heltec_vision_master_e213 -DHELTEC_VISION_MASTER_E213 + -DUSE_EINK -DEINK_DISPLAY_MODEL=GxEPD2_213_FC1 -DEINK_WIDTH=250 -DEINK_HEIGHT=122 @@ -16,4 +17,24 @@ lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 + +; Using experimental InkHUD UI (work in progress) +[platformio] +extra_configs = src/graphics/niche/InkHUD/PlatformioConfig.ini +[env:heltec-vision-master-e213-inkhud] +extends = esp32s3_base, inkhud +board = heltec_vision_master_e213 +build_src_filter = + ${esp32_base.build_src_filter} + ${inkhud.build_src_filter} +build_flags = + ${esp32s3_base.build_flags} + ${inkhud.build_flags} + -I variants/heltec_vision_master_e213 + -D HELTEC_VISION_MASTER_E213 + -D MAX_THREADS=40 +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX + ${esp32s3_base.lib_deps} upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_vision_master_e213/variant.h b/variants/heltec_vision_master_e213/variant.h index 386df6fcf..49b8e91f5 100644 --- a/variants/heltec_vision_master_e213/variant.h +++ b/variants/heltec_vision_master_e213/variant.h @@ -8,7 +8,6 @@ #define I2C_SCL SCL // Display (E-Ink) -#define USE_EINK #define PIN_EINK_CS 5 #define PIN_EINK_BUSY 1 #define PIN_EINK_DC 2 diff --git a/variants/heltec_vision_master_e290/nicheGraphics.h b/variants/heltec_vision_master_e290/nicheGraphics.h new file mode 100644 index 000000000..c55a84ec0 --- /dev/null +++ b/variants/heltec_vision_master_e290/nicheGraphics.h @@ -0,0 +1,129 @@ +/* + +Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants. +NicheGraphics attempts a different approach: + +Per-device config takes place in this setupNicheGraphics() method +(And a small amount in platformio.ini) + +This file sets up InkHUD for Heltec VM-E290. +Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. + +*/ + +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// #include "graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h" +// #include "graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" +#include "graphics/niche/Inputs/TwoButton.h" + +#include "graphics/niche/Fonts/FreeSans6pt7b.h" +#include "graphics/niche/Fonts/FreeSans6pt8bCyrillic.h" +#include + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + + // InkHUD + // ---------------------------- + + InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance(); + + // Set the driver + windowManager->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + windowManager->setDisplayResilience(7, 1.5); + + // Prepare fonts + InkHUD::AppletFont largeFont(FreeSans9pt7b); + InkHUD::AppletFont smallFont(FreeSans6pt7b); + /* + // Font localization demo: Cyrillic + InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic); + smallFont.addSubstitutionsWin1251(); + */ + InkHUD::Applet::setDefaultFonts(largeFont, smallFont); + + // Init settings, and customize defaults + InkHUD::settings.userTiles.maxCount = 2; // How many tiles can the display handle? + InkHUD::settings.rotation = 1; // 90 degrees clockwise + InkHUD::settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + InkHUD::settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + windowManager->addApplet("DMs", new InkHUD::DMApplet); + windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet); + windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet); + // windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running window manager + windowManager->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + constexpr uint8_t MAIN_BUTTON = 0; + constexpr uint8_t AUX_BUTTON = 1; + + // Setup the main user button + buttons->setWiring(MAIN_BUTTON, BUTTON_PIN); + buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); }); + buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); }); + + // Setup the aux button + // Bonus feature of VME290 + buttons->setWiring(AUX_BUTTON, BUTTON_PIN_SECONDARY); + buttons->setHandlerShortPress(AUX_BUTTON, []() { InkHUD::WindowManager::getInstance()->nextTile(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/heltec_vision_master_e290/platformio.ini b/variants/heltec_vision_master_e290/platformio.ini index 06804e4f2..d3aa85d65 100644 --- a/variants/heltec_vision_master_e290/platformio.ini +++ b/variants/heltec_vision_master_e290/platformio.ini @@ -1,14 +1,17 @@ +; Using the original screen class [env:heltec-vision-master-e290] extends = esp32s3_base board = heltec_vision_master_e290 build_flags = ${esp32s3_base.build_flags} -I variants/heltec_vision_master_e290 + -D DISPLAY_FLIP_SCREEN ; Orient so the LoRa antenna faces up -D HELTEC_VISION_MASTER_E290 -D BUTTON_CLICK_MS=200 -D EINK_DISPLAY_MODEL=GxEPD2_290_BN8 -D EINK_WIDTH=296 -D EINK_HEIGHT=128 + -D USE_EINK -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" @@ -18,4 +21,24 @@ lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2#448c8538129fde3d02a7cb5e6fc81971ad92547f lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 + +; Using experimental InkHUD UI (work in progress) +[platformio] +extra_configs = src/graphics/niche/InkHUD/PlatformioConfig.ini +[env:heltec-vision-master-e290-inkhud] +extends = esp32s3_base, inkhud +board = heltec_vision_master_e290 +build_src_filter = + ${esp32_base.build_src_filter} + ${inkhud.build_src_filter} +build_flags = + ${esp32s3_base.build_flags} + ${inkhud.build_flags} + -I variants/heltec_vision_master_e290 + -D HELTEC_VISION_MASTER_E290 + -D MAX_THREADS=40 +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX + ${esp32s3_base.lib_deps} upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_vision_master_e290/variant.h b/variants/heltec_vision_master_e290/variant.h index 299186549..9d6041539 100644 --- a/variants/heltec_vision_master_e290/variant.h +++ b/variants/heltec_vision_master_e290/variant.h @@ -8,7 +8,6 @@ #define I2C_SCL SCL // Display (E-Ink) -#define USE_EINK #define PIN_EINK_CS 3 #define PIN_EINK_BUSY 6 #define PIN_EINK_DC 4 diff --git a/variants/heltec_wireless_paper/nicheGraphics.h b/variants/heltec_wireless_paper/nicheGraphics.h new file mode 100644 index 000000000..0c26f453c --- /dev/null +++ b/variants/heltec_wireless_paper/nicheGraphics.h @@ -0,0 +1,111 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// #include "graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h" +// #include "graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" +#include "graphics/niche/Inputs/TwoButton.h" + +#include "graphics/niche/Fonts/FreeSans6pt7b.h" +#include "graphics/niche/Fonts/FreeSans6pt8bCyrillic.h" +#include + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::LCMEN213EFC1; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance(); + + // Set the driver + windowManager->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + windowManager->setDisplayResilience(10, 1.5); + + // Prepare fonts + InkHUD::AppletFont largeFont(FreeSans9pt7b); + InkHUD::AppletFont smallFont(FreeSans6pt7b); + /* + // Font localization demo: Cyrillic + InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic); + smallFont.addSubstitutionsWin1251(); + */ + InkHUD::Applet::setDefaultFonts(largeFont, smallFont); + + // Init settings, and customize defaults + InkHUD::settings.userTiles.maxCount = 2; // How many tiles can the display handle? + InkHUD::settings.rotation = 3; // 270 degrees clockwise + InkHUD::settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + windowManager->addApplet("DMs", new InkHUD::DMApplet); + windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet); + windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet); + // windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running window manager + windowManager->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + constexpr uint8_t MAIN_BUTTON = 0; + + // Setup the main user button + buttons->setWiring(MAIN_BUTTON, BUTTON_PIN); + buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); }); + buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); }); + + // No aux button on this board + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index a7045b182..36dbfd35b 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -1,3 +1,4 @@ +; Using the original screen class [env:heltec-wireless-paper] extends = esp32s3_base board = heltec_wifi_lora_32_V3 @@ -8,6 +9,7 @@ build_flags = -D EINK_DISPLAY_MODEL=GxEPD2_213_FC1 -D EINK_WIDTH=250 -D EINK_HEIGHT=122 + -D USE_EINK -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted -D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. @@ -16,4 +18,24 @@ lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 + +; Using experimental InkHUD UI (work in progress) +[platformio] +extra_configs = src/graphics/niche/InkHUD/PlatformioConfig.ini +[env:heltec-wireless-paper-inkhud] +extends = esp32s3_base, inkhud +board = heltec_wifi_lora_32_V3 +build_src_filter = + ${esp32_base.build_src_filter} + ${inkhud.build_src_filter} +build_flags = + ${esp32s3_base.build_flags} + ${inkhud.build_flags} + -I variants/heltec_wireless_paper + -D HELTEC_WIRELESS_PAPER + -D MAX_THREADS=40 +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX + ${esp32s3_base.lib_deps} upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_wireless_paper/variant.h b/variants/heltec_wireless_paper/variant.h index fe8f391df..0385945e6 100644 --- a/variants/heltec_wireless_paper/variant.h +++ b/variants/heltec_wireless_paper/variant.h @@ -6,7 +6,6 @@ #define I2C_SCL SCL // Display (E-Ink) -#define USE_EINK #define PIN_EINK_CS 4 #define PIN_EINK_BUSY 7 #define PIN_EINK_DC 5 diff --git a/variants/icarus/platformio.ini b/variants/icarus/platformio.ini index 11f09cab4..b1dc01fc1 100644 --- a/variants/icarus/platformio.ini +++ b/variants/icarus/platformio.ini @@ -6,7 +6,7 @@ board_check = true board_build.mcu = esp32s3 upload_protocol = esptool upload_speed = 921600 -platform_packages = framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip +platform_packages = platformio/framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip lib_deps = ${esp32s3_base.lib_deps} build_unflags = @@ -16,4 +16,4 @@ build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/icarus -DBOARD_HAS_PSRAM - -DARDUINO_USB_MODE=0 + -DARDUINO_USB_MODE=0 \ No newline at end of file diff --git a/variants/m5stack_coreink/platformio.ini b/variants/m5stack_coreink/platformio.ini index c0c8bd30e..70da53379 100644 --- a/variants/m5stack_coreink/platformio.ini +++ b/variants/m5stack_coreink/platformio.ini @@ -17,11 +17,11 @@ build_flags = -DM5STACK lib_deps = ${esp32_base.lib_deps} - zinggjm/GxEPD2@^1.5.3 + zinggjm/GxEPD2@^1.6.2 lewisxhe/PCF8563_Library@^1.0.1 lib_ignore = m5stack-coreink monitor_filters = esp32_exception_decoder board_build.f_cpu = 240000000L upload_protocol = esptool -upload_port = /dev/ttyACM0 \ No newline at end of file +upload_port = /dev/ttyACM0 diff --git a/variants/mesh-tab/platformio.ini b/variants/mesh-tab/platformio.ini index d6fd1a3ac..f76c36bdd 100644 --- a/variants/mesh-tab/platformio.ini +++ b/variants/mesh-tab/platformio.ini @@ -36,6 +36,7 @@ build_flags = ${esp32s3_base.build_flags} -D RAM_SIZE=1024 -D LGFX_DRIVER_TEMPLATE -D LGFX_DRIVER=LGFX_GENERIC + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" -D LGFX_PIN_SCK=12 -D LGFX_PIN_MOSI=13 -D LGFX_PIN_MISO=11 diff --git a/variants/my_esp32s3_diy_eink/platformio.ini b/variants/my_esp32s3_diy_eink/platformio.ini index b2404566f..98613e4fb 100644 --- a/variants/my_esp32s3_diy_eink/platformio.ini +++ b/variants/my_esp32s3_diy_eink/platformio.ini @@ -9,10 +9,10 @@ upload_protocol = esptool ;upload_port = /dev/ttyACM1 upload_speed = 921600 platform_packages = - tool-esptoolpy@^1.40500.0 + platformio/tool-esptoolpy@^1.40801.0 lib_deps = ${esp32_base.lib_deps} - zinggjm/GxEPD2@^1.5.1 + zinggjm/GxEPD2@^1.6.2 adafruit/Adafruit NeoPixel @ ^1.12.0 build_unflags = ${esp32s3_base.build_unflags} diff --git a/variants/my_esp32s3_diy_oled/platformio.ini b/variants/my_esp32s3_diy_oled/platformio.ini index 0fbbaa899..346cc9cac 100644 --- a/variants/my_esp32s3_diy_oled/platformio.ini +++ b/variants/my_esp32s3_diy_oled/platformio.ini @@ -9,7 +9,7 @@ upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 platform_packages = - tool-esptoolpy@^1.40500.0 + platformio/tool-esptoolpy@^1.40801.0 lib_deps = ${esp32_base.lib_deps} adafruit/Adafruit NeoPixel @ ^1.12.0 diff --git a/variants/picomputer-s3/platformio.ini b/variants/picomputer-s3/platformio.ini index 5a05d7b90..b8de94f12 100644 --- a/variants/picomputer-s3/platformio.ini +++ b/variants/picomputer-s3/platformio.ini @@ -1,7 +1,7 @@ [env:picomputer-s3] extends = esp32s3_base board = bpi_picow_esp32_s3 - +board_check = true ;OpenOCD flash method ;upload_protocol = esp-builtin ;Normal method @@ -15,3 +15,50 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} lovyan03/LovyanGFX@^1.2.0 + +build_src_filter = + ${esp32s3_base.build_src_filter} + + +[env:picomputer-s3-tft] +extends = env:picomputer-s3 +board_build.partitions = default_8MB.csv ; just for test + +build_flags = + ${env:picomputer-s3.build_flags} + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 + -D MESHTASTIC_EXCLUDE_SERIAL=1 + -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 + -D INPUTDRIVER_MATRIX_TYPE=1 + -D USE_PIN_BUZZER=PIN_BUZZER + -D USE_SX127x + -D MAX_NUM_NODES=200 + -D HAS_SCREEN=0 + -D HAS_TFT=1 + -D RAM_SIZE=1024 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D LGFX_DRIVER=LGFX_PICOMPUTER_S3 + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_PICOMPUTER_S3.h\" + -D VIEW_320x240 +; -D USE_DOUBLE_BUFFER + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + +build_src_filter = + ${env:picomputer-s3.build_src_filter} + +<../lib/device-ui/generated/ui_320x240> + +<../lib/device-ui/resources> + +<../lib/device-ui/locale> + +<../lib/device-ui/source> \ No newline at end of file diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini index 2c7030b5b..f77831ad7 100644 --- a/variants/portduino/platformio.ini +++ b/variants/portduino/platformio.ini @@ -1,15 +1,79 @@ -[env:native] +[native_base] extends = portduino_base -; The pkg-config commands below optionally add link flags. -; the || : is just a "or run the null command" to avoid returning an error code -build_flags = ${portduino_base.build_flags} -O0 -I variants/portduino +build_flags = ${portduino_base.build_flags} -I variants/portduino + -D ARCH_PORTDUINO -I /usr/include - !pkg-config --libs libulfius --silence-errors || : - !pkg-config --libs openssl --silence-errors || : board = cross_platform lib_deps = ${portduino_base.lib_deps} build_src_filter = ${portduino_base.build_src_filter} +[env:native] +extends = native_base +; The pkg-config commands below optionally add link flags. +; the || : is just a "or run the null command" to avoid returning an error code +build_flags = ${native_base.build_flags} + !pkg-config --libs libulfius --silence-errors || : + !pkg-config --libs openssl --silence-errors || : + +[env:native-tft] +extends = native_base +build_type = release +build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D RAM_SIZE=16384 + -D USE_X11=1 + -D HAS_TFT=1 + -D HAS_SCREEN=0 + -D LV_BUILD_TEST=0 + -D LV_USE_LIBINPUT=1 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + !pkg-config --libs libulfius --silence-errors || : + !pkg-config --libs openssl --silence-errors || : +build_src_filter = ${native_base.build_src_filter} + - + +<../lib/device-ui/generated/ui_320x240> + +<../lib/device-ui/generated/ui_320x240/fonts> + +<../lib/device-ui/resources> + +<../lib/device-ui/portduino> + +<../lib/device-ui/locale> + +<../lib/device-ui/source> + +[env:native-tft-debug] +extends = native_base +build_type = debug +board_level = extra +build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D DEBUG_HEAP + -D RAM_SIZE=16384 + -D USE_X11=1 + -D HAS_TFT=1 + -D HAS_SCREEN=0 +; -D CALIBRATE_TOUCH=0 + -D LV_BUILD_TEST=0 + -D LV_USE_LOG=1 + -D LV_USE_SYSMON=1 + -D LV_USE_PERF_MONITOR=1 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_PROFILER=0 + -D LV_USE_LIBINPUT=1 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + !pkg-config --libs libulfius --silence-errors || : + !pkg-config --libs openssl --silence-errors || : +build_src_filter = ${env:native-tft.build_src_filter} + [env:coverage] extends = env:native build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:native.build_flags} diff --git a/variants/portduino/variant.h b/variants/portduino/variant.h index b7b39d6e8..ce7dbd865 100644 --- a/variants/portduino/variant.h +++ b/variants/portduino/variant.h @@ -1,4 +1,6 @@ +#ifndef HAS_SCREEN #define HAS_SCREEN 1 +#endif #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE settingsMap[maxtophone] diff --git a/variants/rak11310/platformio.ini b/variants/rak11310/platformio.ini index 0cc60bc7c..d1da962ca 100644 --- a/variants/rak11310/platformio.ini +++ b/variants/rak11310/platformio.ini @@ -18,4 +18,4 @@ lib_deps = ${networking_base.lib_deps} https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 debug_build_flags = ${rp2040_base.build_flags}, -g -debug_tool = cmsis-dap ; for e.g. Picotool +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index f50f3b880..bc5541336 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -107,11 +107,15 @@ static const uint8_t AREF = PIN_AREF; /* * SPI Interfaces */ -#define SPI_INTERFACES_COUNT 1 +#define SPI_INTERFACES_COUNT 2 -#define PIN_SPI_MISO (29) -#define PIN_SPI_MOSI (30) -#define PIN_SPI_SCK (3) +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) static const uint8_t SS = 42; static const uint8_t MOSI = PIN_SPI_MOSI; @@ -126,8 +130,8 @@ static const uint8_t SCK = PIN_SPI_SCK; #define PIN_EINK_BUSY (0 + 4) #define PIN_EINK_DC (0 + 17) #define PIN_EINK_RES (-1) -#define PIN_EINK_SCLK PIN_SPI_SCK -#define PIN_EINK_MOSI PIN_SPI_MOSI // also called SDI +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI // #define USE_EINK @@ -255,7 +259,7 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS PIN_EINK_CS -#define ETH_SPI_PORT SPI +#define ETH_SPI_PORT SPI1 #define AQ_SET_PIN 10 #ifdef __cplusplus diff --git a/variants/rak4631_epaper/platformio.ini b/variants/rak4631_epaper/platformio.ini index 2479f09c8..b851691ed 100644 --- a/variants/rak4631_epaper/platformio.ini +++ b/variants/rak4631_epaper/platformio.ini @@ -13,10 +13,10 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_epaper -D RAK_4631 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_epaper> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.4.9 + zinggjm/GxEPD2@^1.6.2 melopero/Melopero RV3028@^1.1.0 rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAKwireless RAK12034@^1.0.0 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) -;upload_protocol = jlink \ No newline at end of file +;upload_protocol = jlink diff --git a/variants/rak4631_epaper_onrxtx/platformio.ini b/variants/rak4631_epaper_onrxtx/platformio.ini index 8c1b8eee8..8612a3f3d 100644 --- a/variants/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/rak4631_epaper_onrxtx/platformio.ini @@ -15,11 +15,11 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_epaper -D RAK_4631 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_epaper_onrxtx> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.5.1 + zinggjm/GxEPD2@^1.6.2 melopero/Melopero RV3028@^1.1.0 rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAKwireless RAK12034@^1.0.0 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink -;upload_port = /dev/ttyACM3 \ No newline at end of file +;upload_port = /dev/ttyACM3 diff --git a/variants/rpipico2/platformio.ini b/variants/rpipico2/platformio.ini index 24714efd5..de4954ea2 100644 --- a/variants/rpipico2/platformio.ini +++ b/variants/rpipico2/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${rp2350_base.build_flags} -Ivariants/rpipico2 -DDEBUG_RP2040_PORT=Serial -DHW_SPI1_DEVICE - -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m33" lib_deps = ${rp2350_base.lib_deps} debug_build_flags = ${rp2350_base.build_flags}, -g diff --git a/variants/rpipico2w/platformio.ini b/variants/rpipico2w/platformio.ini new file mode 100644 index 000000000..351774221 --- /dev/null +++ b/variants/rpipico2w/platformio.ini @@ -0,0 +1,30 @@ +[env:pico2w] +extends = rp2350_base +board = rpipico2w +upload_protocol = jlink +# debug settings for external openocd with RP2040 support (custom build) +debug_tool = custom +debug_init_cmds = + target extended-remote localhost:3333 + $INIT_BREAK + monitor reset halt + $LOAD_CMDS + monitor init + monitor reset halt + +# add our variants files to the include and src paths +build_flags = ${rp2350_base.build_flags} + -DRPI_PICO2 + -Ivariants/rpipico2w +# -DDEBUG_RP2040_PORT=Serial + -DHW_SPI1_DEVICE + -DARDUINO_RASPBERRY_PI_PICO_2W + -DARDUINO_ARCH_RP2040 + -DHAS_WIFI=1 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m33" + -fexceptions # for exception handling in MQTT +build_src_filter = ${rp2350_base.build_src_filter} + +lib_deps = + ${rp2350_base.lib_deps} + ${networking_base.lib_deps} +debug_build_flags = ${rp2350_base.build_flags}, -g \ No newline at end of file diff --git a/variants/rpipico2w/variant.h b/variants/rpipico2w/variant.h new file mode 100644 index 000000000..c7689048a --- /dev/null +++ b/variants/rpipico2w/variant.h @@ -0,0 +1,52 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +#ifndef HAS_WIFI +#define HAS_WIFI 1 +#endif + +// default I2C pins: +// SDA = 4 +// SCL = 5 + +// Recommended pins for SerialModule: +// txd = 8 +// rxd = 9 + +#define EXT_NOTIFY_OUT 22 +#define BUTTON_PIN 17 + +#define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 10 +#define LORA_MISO 12 +#define LORA_MOSI 11 +#define LORA_CS 3 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 15 +#define LORA_DIO1 20 +#define LORA_DIO2 2 +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif \ No newline at end of file diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index 1b64ed6e1..31566edbe 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -26,3 +26,57 @@ lib_deps = ${esp32s3_base.lib_deps} https://github.com/mverch67/LovyanGFX#develop earlephilhower/ESP8266Audio@^1.9.9 earlephilhower/ESP8266SAM@^1.0.1 + + +[env:seeed-sensecap-indicator-tft] +extends = env:seeed-sensecap-indicator +board_level = main +upload_speed = 460800 +board_build.partitions = default_8MB.csv ; must be here for some reason, board.json is not enough !? + +build_flags = + ${env:seeed-sensecap-indicator.build_flags} + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_SCREEN=1 + -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 + -D MESHTASTIC_EXCLUDE_SERIAL=1 + -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 + -D INPUTDRIVER_BUTTON_TYPE=38 + -D HAS_TELEMETRY=0 + -D CONFIG_DISABLE_HAL_LOCKS=1 + -D MAX_NUM_NODES=250 + -D HAS_SCREEN=0 + -D HAS_TFT=1 + -D DISPLAY_SET_RESOLUTION + -D USE_I2S_BUZZER + -D RAM_SIZE=4096 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D CUSTOM_TOUCH_DRIVER + -D LGFX_DRIVER=LGFX_INDICATOR + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_INDICATOR.h\" + -D VIEW_320x240 +; -D USE_DOUBLE_BUFFER + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + +build_src_filter = + ${env:seeed-sensecap-indicator.build_src_filter} + +<../lib/device-ui/generated/ui_320x240> + +<../lib/device-ui/resources> + +<../lib/device-ui/locale> + +<../lib/device-ui/source> + +lib_deps = + ${env:seeed-sensecap-indicator.lib_deps} + https://github.com/bitbank2/bb_captouch.git#8f2f06462ff597847921739376a299db93612417 ; alternative touch library supporting FT6x36 \ No newline at end of file diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h index 58eed7d96..1010e04c8 100644 --- a/variants/seeed-sensecap-indicator/variant.h +++ b/variants/seeed-sensecap-indicator/variant.h @@ -7,7 +7,9 @@ #define SENSOR_PORT_NUM 2 #define SENSOR_BAUD_RATE 115200 +#if !HAS_TFT #define BUTTON_PIN 38 +#endif // #define BUTTON_NEED_PULLUP // #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/seeed_xiao_s3/variant.h b/variants/seeed_xiao_s3/variant.h index 8f9282a7a..d8dcbc8d4 100644 --- a/variants/seeed_xiao_s3/variant.h +++ b/variants/seeed_xiao_s3/variant.h @@ -36,6 +36,10 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define BUTTON_PIN 21 // This is the Program Button #define BUTTON_NEED_PULLUP +#define BATTERY_PIN -1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define BATTERY_SENSE_RESOLUTION_BITS 12 + /*Warning: https://www.seeedstudio.com/L76K-GNSS-Module-for-Seeed-Studio-XIAO-p-5864.html L76K Expansion Board can not directly used, L76K Reset Pin needs to override or physically remove it, diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index 003dd184d..1eb8a1abd 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -4,7 +4,6 @@ extends = esp32s3_base board = t-deck board_check = true upload_protocol = esptool -#upload_port = COM29 build_flags = ${esp32s3_base.build_flags} -DT_DECK @@ -17,3 +16,64 @@ lib_deps = ${esp32s3_base.lib_deps} lovyan03/LovyanGFX@^1.2.0 earlephilhower/ESP8266Audio@^1.9.9 earlephilhower/ESP8266SAM@^1.0.1 + + +[env:t-deck-tft] +extends = env:t-deck +board_build.partitions = default_16MB.csv + +build_flags = + ${env:t-deck.build_flags} + -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 + -D MESHTASTIC_EXCLUDE_SERIAL=1 + -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 + -D INPUTDRIVER_I2C_KBD_TYPE=0x55 + -D INPUTDRIVER_ENCODER_TYPE=3 + -D INPUTDRIVER_ENCODER_LEFT=1 + -D INPUTDRIVER_ENCODER_RIGHT=2 + -D INPUTDRIVER_ENCODER_UP=3 + -D INPUTDRIVER_ENCODER_DOWN=15 + -D INPUTDRIVER_ENCODER_BTN=0 + -D INPUTDRIVER_BUTTON_TYPE=0 + -D MAX_NUM_NODES=250 + -D HAS_SCREEN=0 + -D HAS_TFT=1 + -D USE_I2S_BUZZER + -D RAM_SIZE=4096 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D RADIOLIB_DEBUG_BASIC=0 + -D RADIOLIB_DEBUG_SPI=0 + -D RADIOLIB_DEBUG_PROTOCOL=0 + -D RADIOLIB_SPI_PARANOID=0 + -D CALIBRATE_TOUCH=0 + -D LGFX_DRIVER=LGFX_TDECK + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_DECK.h\" +; -D LVGL_DRIVER=LVGL_TDECK +; -D GFX_DRIVER_INC=\"graphics/LVGL/LVGL_T_DECK.h\" +; -D LV_USE_ST7789=1 + -D VIEW_320x240 +; -D USE_DOUBLE_BUFFER + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + +build_src_filter = + ${env:t-deck.build_src_filter} + +<../lib/device-ui/generated/ui_320x240> + +<../lib/device-ui/resources> + +<../lib/device-ui/locale> + +<../lib/device-ui/source> + +lib_deps = + ${env:t-deck.lib_deps} diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h index 4aeeb7ca8..8ffc4ea44 100644 --- a/variants/t-deck/variant.h +++ b/variants/t-deck/variant.h @@ -1,5 +1,10 @@ + +#define TFT_CS 12 +#ifndef HAS_TFT // for TFT-UI the definitions are in device-ui +#define BUTTON_PIN 0 + // ST7789 TFT LCD -#define ST7789_CS 12 +#define ST7789_CS TFT_CS #define ST7789_RS 11 // DC #define ST7789_SDA 41 // MOSI #define ST7789_SCK 40 @@ -19,6 +24,7 @@ #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness +#endif #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 @@ -42,6 +48,7 @@ #define SPI_MISO (38) #define SPI_CS (39) #define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage // ratio of voltage divider = 2.0 (RD2=100k, RD3=100k) diff --git a/variants/t-echo/nicheGraphics.h b/variants/t-echo/nicheGraphics.h new file mode 100644 index 000000000..44d8ef4c3 --- /dev/null +++ b/variants/t-echo/nicheGraphics.h @@ -0,0 +1,126 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// #include "graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h" +// #include "graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/GDEY0154D67.h" +#include "graphics/niche/Inputs/TwoButton.h" + +#include "graphics/niche/Fonts/FreeSans6pt7b.h" +#include "graphics/niche/Fonts/FreeSans6pt8bCyrillic.h" +#include + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // For NRF52 platforms, SPI pins are defined in variant.h, not passed to begin() + SPIClass *inkSPI = &SPI1; + inkSPI->begin(); + + // Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(inkSPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance(); + + // Set the driver + windowManager->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + windowManager->setDisplayResilience(20, 1.5); + + // Prepare fonts + InkHUD::AppletFont largeFont(FreeSans9pt7b); + InkHUD::AppletFont smallFont(FreeSans6pt7b); + /* + // Font localization demo: Cyrillic + InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic); + smallFont.addSubstitutionsWin1251(); + */ + InkHUD::Applet::setDefaultFonts(largeFont, smallFont); + + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + InkHUD::settings.userTiles.maxCount = 2; // Two applets side-by-side + InkHUD::settings.rotation = 3; // 270 degrees clockwise + InkHUD::settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + InkHUD::settings.optionalMenuItems.backlight = true; // Until proven (by touch) that user still has the capacitive button + + // Setup backlight + // Note: AUX button behavior configured further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + windowManager->addApplet("DMs", new InkHUD::DMApplet); + windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet); + windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet); + // windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running window manager + windowManager->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + constexpr uint8_t MAIN_BUTTON = 0; + constexpr uint8_t TOUCH_BUTTON = 1; + + // Setup the main user button + buttons->setWiring(MAIN_BUTTON, BUTTON_PIN, LOW); + buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); }); + buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); }); + + // Setup the capacitive touch button + // - short: momentary backlight + // - long: latch backlight on + buttons->setWiring(TOUCH_BUTTON, PIN_BUTTON_TOUCH, LOW); + buttons->setTiming(TOUCH_BUTTON, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC + buttons->setHandlerDown(TOUCH_BUTTON, [backlight]() { + backlight->peek(); + InkHUD::settings.optionalMenuItems.backlight = false; // We've proved user still has the button. No need for menu entry. + }); + buttons->setHandlerLongPress(TOUCH_BUTTON, [backlight]() { backlight->latch(); }); + buttons->setHandlerShortPress(TOUCH_BUTTON, [backlight]() { backlight->off(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/t-echo/platformio.ini b/variants/t-echo/platformio.ini index ce58c0b88..e0e26fe6a 100644 --- a/variants/t-echo/platformio.ini +++ b/variants/t-echo/platformio.ini @@ -1,4 +1,4 @@ -; First prototype eink/nrf52840/sx1262 device +; Using original screen class [env:t-echo] extends = nrf52840_base board = t-echo @@ -12,6 +12,7 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/t-echo -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 -DEINK_WIDTH=200 -DEINK_HEIGHT=200 + -DUSE_EINK -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. @@ -21,4 +22,26 @@ lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a lewisxhe/PCF8563_Library@^1.0.1 -;upload_protocol = fs \ No newline at end of file +;upload_protocol = fs + +; Using experimental InkHUD UI (work in progress) +[platformio] +extra_configs = src/graphics/niche/InkHUD/PlatformioConfig.ini +[env:t-echo-inkhud] +extends = nrf52840_base, inkhud +board = t-echo +board_check = true +debug_tool = jlink +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/t-echo + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} + +<../variants/t-echo> +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 \ No newline at end of file diff --git a/variants/t-echo/variant.h b/variants/t-echo/variant.h index 365dfd804..38b7f4743 100644 --- a/variants/t-echo/variant.h +++ b/variants/t-echo/variant.h @@ -162,8 +162,6 @@ External serial flash WP25R1635FZUIL0 #define PIN_POWER_EN (0 + 12) // #define PIN_POWER_EN1 (0 + 13) -#define USE_EINK - #define PIN_SPI1_MISO \ (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO #define PIN_SPI1_MOSI PIN_EINK_MOSI diff --git a/variants/tracker-t1000-e/variant.h b/variants/tracker-t1000-e/variant.h index e65f26c93..0d98a3033 100644 --- a/variants/tracker-t1000-e/variant.h +++ b/variants/tracker-t1000-e/variant.h @@ -111,6 +111,7 @@ extern "C" { #define GPS_TX_PIN PIN_SERIAL1_TX #define GPS_BAUDRATE 115200 +#define GPS_PROBETRIES 5 #define PIN_GPS_EN (32 + 11) // P1.11 #define GPS_EN_ACTIVE HIGH diff --git a/variants/trackerd/platformio.ini b/variants/trackerd/platformio.ini index 6fba190f3..654534a15 100644 --- a/variants/trackerd/platformio.ini +++ b/variants/trackerd/platformio.ini @@ -1,13 +1,8 @@ [env:trackerd] extends = esp32_base -;platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream -platform = espressif32 board = pico32 board_build.f_flash = 80000000L build_flags = ${esp32_base.build_flags} -D PRIVATE_HW -I variants/trackerd -D BSFILE=\"boards/dragino_lbt2.h\" -;board_build.partitions = no_ota.csv -;platform_packages = -; platformio/framework-arduinoespressif32@3 -;platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.1-RC1 +;board_build.partitions = no_ota.csv \ No newline at end of file diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini index e21e9ed77..e17d3e373 100644 --- a/variants/unphone/platformio.ini +++ b/variants/unphone/platformio.ini @@ -1,18 +1,14 @@ ; platformio.ini for unphone meshtastic [env:unphone] - extends = esp32s3_base -board = unphone9 +board = unphone upload_speed = 921600 monitor_speed = 115200 monitor_filters = esp32_exception_decoder -build_unflags = - ${esp32s3_base.build_unflags} - -D ARDUINO_USB_MODE - -build_flags = ${esp32_base.build_flags} +build_flags = + ${esp32s3_base.build_flags} -D UNPHONE -I variants/unphone -D ARDUINO_USB_MODE=0 @@ -22,8 +18,11 @@ build_flags = ${esp32_base.build_flags} -D UNPHONE_UI0=0 -D UNPHONE_LORA=0 -D UNPHONE_FACTORY_MODE=0 + -D USE_SX127x -build_src_filter = ${esp32_base.build_src_filter} +<../variants/unphone> +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/unphone> lib_deps = ${esp32s3_base.lib_deps} lovyan03/LovyanGFX@ 1.2.0 @@ -32,46 +31,43 @@ lib_deps = ${esp32s3_base.lib_deps} [env:unphone-tft] -extends = esp32s3_base -board_level = extra -board = unphone +extends = env:unphone board_build.partitions = default_8MB.csv -monitor_speed = 115200 -monitor_filters = esp32_exception_decoder -build_flags = ${esp32_base.build_flags} - -D UNPHONE - -D UNPHONE_ACCEL=0 - -D UNPHONE_TOUCHS=0 - -D UNPHONE_SDCARD=0 - -D UNPHONE_UI0=0 - -D UNPHONE_LORA=0 - -D UNPHONE_FACTORY_MODE=0 +build_flags = + ${env:unphone.build_flags} + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 + -D MESHTASTIC_EXCLUDE_SERIAL=1 + -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 + -D INPUTDRIVER_BUTTON_TYPE=21 + -D MAX_NUM_NODES=200 -D MAX_THREADS=40 -D HAS_SCREEN=0 -D HAS_TFT=1 - -D RAM_SIZE=512 + -D DISPLAY_SET_RESOLUTION + -D RAM_SIZE=3072 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_BUILD_TEST=0 + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 -D LV_USE_PERF_MONITOR=0 -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -; -D CALIBRATE_TOUCH=0 -D LGFX_DRIVER=LGFX_UNPHONE_V9 + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_UNPHONE.h\" -D VIEW_320x240 -; -D USE_DOUBLE_BUFFER -D USE_PACKET_API -I lib/device-ui/generated/ui_320x240 - -I variants/unphone -build_src_filter = ${esp32_base.build_src_filter} +<../variants/unphone> +build_src_filter = + ${env:unphone.build_src_filter} +<../lib/device-ui/generated/ui_320x240> +<../lib/device-ui/resources> - +<../lib/device-ui/source> - -lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 - adafruit/Adafruit NeoPixel@1.12.0 + +<../lib/device-ui/locale> + +<../lib/device-ui/source> \ No newline at end of file diff --git a/variants/unphone/variant.h b/variants/unphone/variant.h index 0a94c5987..e846b064a 100644 --- a/variants/unphone/variant.h +++ b/variants/unphone/variant.h @@ -48,7 +48,8 @@ #undef GPS_RX_PIN #undef GPS_TX_PIN -// #define HAS_SDCARD 1 // causes hang if defined +#define HAS_SDCARD 1 +#define SD_SPI_FREQUENCY 25000000 #define SDCARD_CS 43 #define LED_PIN 13 // the red part of the RGB LED diff --git a/variants/wio-tracker-wm1110/variant.h b/variants/wio-tracker-wm1110/variant.h index 32e84485d..807ca8dbb 100644 --- a/variants/wio-tracker-wm1110/variant.h +++ b/variants/wio-tracker-wm1110/variant.h @@ -103,6 +103,11 @@ extern "C" { #define LR1110_GNSS_ANT_PIN (32 + 5) // P1.05 37 +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define HAS_GPS 1 + #ifdef __cplusplus } #endif diff --git a/variants/xiao_ble/README.md b/variants/xiao_ble/README.md index 6fff9cd22..2a08138ba 100644 --- a/variants/xiao_ble/README.md +++ b/variants/xiao_ble/README.md @@ -116,24 +116,26 @@ The default pin mapping in `variant.h` uses 'automatic Tx/Rx switching' mode. If   MCU -> E22 connections -| Xiao BLE pin | variant.h definition | E22 pin | Notes | -| :------------ | :---------------------------- | :-----------------| :------------------------------------------------------------------------------------------------------------------- | -| D0 | SX126X_CS | 19 (NSS) | | -| D1 | SX126X_DIO1 | 13 (DIO1) | | -| D2 | SX126X_BUSY | 14 (BUSY) | | -| D3 | SX126X_RESET | 15 (NRST) | | -| D7 | SX126X_RXEN | 6 (RXEN) | These pins must still be connected, and `SX126X_RXEN` defined in `variant.h`, otherwise Rx sensitivity will be poor. | -| D8 | PIN_SPI_SCK | 18 (SCK) | | -| D9 | PIN_SPI_MISO | 16 (MISO) | | -| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +| Xiao BLE pin | variant.h definition | E22 pin | Notes | +| :----------- | :------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D7 | SX126X_RXEN | 6 (RXEN) | These pins must still be connected, and `SX126X_RXEN` defined in `variant.h`, otherwise Rx sensitivity will be poor. | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | |     E22 -> E22 connections: -| E22 pin | E22 pin | Notes | -| :------------ | :---------------------------- | :------------------------------------------------------------------------ | -| TXEN | DIO2 | These must be physically connected for automatic Tx/Rx switching to work. | + +| E22 pin | E22 pin | Notes | +| :------ | :------ | :------------------------------------------------------------------------ | +| TXEN | DIO2 | These must be physically connected for automatic Tx/Rx switching to work. |

Note

@@ -148,17 +150,18 @@ The schematic (`xiao-ble-e22-schematic.png`) in the `eagle-project` directory us

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

MCU -> E22 connections -| Xiao BLE pin | variant.h definition | E22 pin | Notes | -| :------------ | :---------------------------- | :-----------------| :--------------------------- | -| D0 | SX126X_CS | 19 (NSS) | | -| D1 | SX126X_DIO1 | 13 (DIO1) | | -| D2 | SX126X_BUSY | 14 (BUSY) | | -| D3 | SX126X_RESET | 15 (NRST) | | -| D6 | SX126X_TXEN | 7 (TXEN) | | -| D7 | SX126X_RXEN | 6 (RXEN) | | -| D8 | PIN_SPI_SCK | 18 (SCK) | | -| D9 | PIN_SPI_MISO | 16 (MISO) | | -| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +| Xiao BLE pin | variant.h definition | E22 pin | Notes | +| :----------- | :------------------- | :-------- | :---- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D6 | SX126X_TXEN | 7 (TXEN) | | +| D7 | SX126X_RXEN | 6 (RXEN) | | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | E22 -> E22 connections: (none) diff --git a/version.properties b/version.properties index 2e207e21e..4cb750c2c 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 -minor = 5 -build = 22 +minor = 6 +build = 0 \ No newline at end of file