diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 7cd0dfcac..350ca290c 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -5,7 +5,7 @@ runs: using: composite steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} @@ -23,7 +23,7 @@ runs: sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x cache: pip diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index 5c441f085..7f3f8b672 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive path: meshtasticd diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index df1035e62..2ef67405a 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -20,7 +20,7 @@ jobs: name: build-${{ inputs.platform }} runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index cde7fd274..26a9cff18 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -47,7 +47,7 @@ jobs: runs-on: ${{ inputs.runs-on }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index d1d1a5634..20b9ceee6 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml index 94aaca49c..2204cc02c 100644 --- a/.github/workflows/hook_copr.yml +++ b/.github/workflows/hook_copr.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive ref: ${{ github.ref }} diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index a98bdc011..66143cc01 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -42,8 +42,8 @@ jobs: - check runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -72,7 +72,7 @@ jobs: version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -93,7 +93,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name != 'workflow_dispatch' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build base id: base uses: ./.github/actions/setup-base @@ -288,12 +288,12 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -322,7 +322,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -367,10 +367,10 @@ jobs: - package-pio-deps-native-tft steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -386,14 +386,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -436,14 +436,14 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -460,7 +460,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip merge-multiple: true @@ -491,14 +491,14 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 309772b12..f26073ec4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Trunk Check uses: trunk-io/trunk-action@v1 @@ -31,7 +31,7 @@ jobs: pull-requests: write # For trunk to create PRs steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Trunk Upgrade uses: trunk-io/trunk-action/upgrade@v1 diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index 275ffce0e..4c547eadc 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -34,7 +34,7 @@ jobs: needs: build-debian-src steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive path: meshtasticd @@ -58,7 +58,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 9f535b7b1..d8ff6e631 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -24,14 +24,14 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index a54b0bd36..aece730a0 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -32,7 +32,7 @@ jobs: needs: build-debian-src steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive path: meshtasticd @@ -60,7 +60,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index 93114e2c7..5fca90961 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check for PR labels - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml new file mode 100644 index 000000000..4e285852d --- /dev/null +++ b/.github/workflows/pr_tests.yml @@ -0,0 +1,238 @@ +name: Tests + +# DISABLED: Changed from automatic PR triggers to manual only +on: + workflow_dispatch: + inputs: + reason: + description: "Reason for manual test run" + required: false + default: "Manual test execution" + +concurrency: + group: tests-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + actions: read + checks: write + pull-requests: write + +jobs: + native-tests: + name: "πŸ§ͺ Native Tests" + if: github.repository == 'meshtastic/firmware' + uses: ./.github/workflows/test_native.yml + permissions: + contents: read + actions: read + checks: write + + test-summary: + name: "πŸ“Š Test Results" + runs-on: ubuntu-latest + needs: [native-tests] + if: always() + permissions: + contents: read + actions: read + checks: write + pull-requests: write + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Get release version string + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + + - name: Download test artifacts + if: needs.native-tests.result != 'skipped' + uses: actions/download-artifact@v5 + with: + name: platformio-test-report-${{ steps.version.outputs.long }}.zip + merge-multiple: true + + - name: Parse test results and create detailed summary + id: test-results + run: | + echo "## πŸ§ͺ Test Results Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check overall job status first + if [[ "${{ needs.native-tests.result }}" == "success" ]]; then + echo "βœ… **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY + elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then + echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY + elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then + echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Parse detailed test results if available + if [ -f "testreport.xml" ]; then + echo "### πŸ” Individual Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + python3 << 'EOF' + import xml.etree.ElementTree as ET + import os + + try: + tree = ET.parse('testreport.xml') + root = tree.getroot() + + total_tests = 0 + passed_tests = 0 + failed_tests = 0 + skipped_tests = 0 + + # Parse testsuite elements + for testsuite in root.findall('.//testsuite'): + suite_name = testsuite.get('name', 'Unknown') + suite_tests = int(testsuite.get('tests', '0')) + suite_failures = int(testsuite.get('failures', '0')) + suite_errors = int(testsuite.get('errors', '0')) + suite_skipped = int(testsuite.get('skipped', '0')) + + total_tests += suite_tests + failed_tests += suite_failures + suite_errors + skipped_tests += suite_skipped + passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped + + if suite_tests > 0: + status = "βœ…" if (suite_failures + suite_errors) == 0 else "❌" + print(f"**{status} Test Suite: {suite_name}**") + print(f"- Total: {suite_tests}") + print(f"- Passed: βœ… {suite_tests - suite_failures - suite_errors - suite_skipped}") + print(f"- Failed: ❌ {suite_failures + suite_errors}") + if suite_skipped > 0: + print(f"- Skipped: ⏭️ {suite_skipped}") + print("") + + # Show individual test results for failed suites + if suite_failures + suite_errors > 0: + print("**Failed Tests:**") + for testcase in testsuite.findall('testcase'): + test_name = testcase.get('name', 'Unknown') + failure = testcase.find('failure') + error = testcase.find('error') + + if failure is not None: + msg = failure.get('message', 'Unknown error')[:100] + print(f"- ❌ `{test_name}`: {msg}") + elif error is not None: + msg = error.get('message', 'Unknown error')[:100] + print(f"- ❌ `{test_name}`: ERROR - {msg}") + print("") + else: + # Show passed tests for successful suites + passed_count = 0 + for testcase in testsuite.findall('testcase'): + if testcase.find('failure') is None and testcase.find('error') is None: + if passed_count < 5: # Limit to first 5 to avoid spam + test_name = testcase.get('name', 'Unknown') + print(f"- βœ… `{test_name}`: PASSED") + passed_count += 1 + if passed_count > 5: + print(f"- ... and {passed_count - 5} more tests passed") + print("") + + # Summary statistics + print("### πŸ“Š Test Statistics") + print(f"- **Total Tests**: {total_tests}") + print(f"- **Passed**: βœ… {passed_tests}") + print(f"- **Failed**: ❌ {failed_tests}") + if skipped_tests > 0: + print(f"- **Skipped**: ⏭️ {skipped_tests}") + + if failed_tests > 0: + print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**") + else: + print(f"\nβœ… **All {total_tests} tests passed!**") + + except Exception as e: + print(f"❌ Error parsing test results: {e}") + EOF + else + echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY + + - name: Comment test results on PR + if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + + // Read the step summary to use as PR comment + let testSummary = "## πŸ§ͺ Test Results Summary\n\n"; + + if ("${{ needs.native-tests.result }}" === "success") { + testSummary += "βœ… **All tests passed!**\n\n"; + } else if ("${{ needs.native-tests.result }}" === "failure") { + testSummary += "❌ **Some tests failed.**\n\n"; + } else { + testSummary += "⚠️ **Tests did not complete normally.**\n\n"; + } + + testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`; + testSummary += "---\n"; + testSummary += "*This comment will be automatically updated when new commits are pushed.*"; + + // Find existing comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('πŸ§ͺ Test Results Summary') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: testSummary + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: testSummary + }); + } + + - name: Set overall status + run: | + if [[ "${{ needs.native-tests.result }}" == "success" ]]; then + echo "All tests passed! βœ…" + exit 0 + else + echo "Some tests failed! ❌" + exit 1 + fi diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index e52e67227..486f4b1a6 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -60,10 +60,10 @@ jobs: shell: bash steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index e391aa07b..96c993cba 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -21,7 +21,7 @@ jobs: steps: # step 1 - name: clone application source code - uses: actions/checkout@v4 + uses: actions/checkout@v5 # step 2 - name: full scan diff --git a/.github/workflows/sec_sast_semgrep_pull.yml b/.github/workflows/sec_sast_semgrep_pull.yml index 3707c91b8..e93b2ae8b 100644 --- a/.github/workflows/sec_sast_semgrep_pull.yml +++ b/.github/workflows/sec_sast_semgrep_pull.yml @@ -13,7 +13,7 @@ jobs: steps: # step 1 - name: clone application source code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 5a11fdfa8..32e2c2c8b 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: days-before-stale: 45 exempt-issue-labels: pinned,3.0 diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index dc05959fd..6b788f4c7 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -14,7 +14,7 @@ jobs: name: Native Simulator Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -70,7 +70,7 @@ jobs: name: Native PlatformIO Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -127,7 +127,7 @@ jobs: - platformio-tests if: always() steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -137,7 +137,7 @@ jobs: id: version - name: Download test artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: platformio-test-report-${{ steps.version.outputs.long }}.zip merge-multiple: true @@ -150,7 +150,7 @@ jobs: reporter: java-junit - name: Download coverage artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip path: code-coverage-report diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 34b28b39c..942659348 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: test-runner steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 # - uses: actions/setup-python@v5 # with: @@ -47,7 +47,7 @@ jobs: pio upgrade - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 diff --git a/.github/workflows/trunk_annotate_pr.yml b/.github/workflows/trunk_annotate_pr.yml index 62c1c01b7..23dcf8d09 100644 --- a/.github/workflows/trunk_annotate_pr.yml +++ b/.github/workflows/trunk_annotate_pr.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Trunk Check uses: trunk-io/trunk-action@v1 diff --git a/.github/workflows/trunk_check.yml b/.github/workflows/trunk_check.yml index 55656bf48..41731d491 100644 --- a/.github/workflows/trunk_check.yml +++ b/.github/workflows/trunk_check.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Trunk Check uses: trunk-io/trunk-action@v1 diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml index 33f4182eb..51082fc5f 100644 --- a/.github/workflows/trunk_format_pr.yml +++ b/.github/workflows/trunk_format_pr.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -39,7 +39,7 @@ jobs: git push - name: Comment on PR - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index 3952d9d02..c06e06b0a 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: true diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 2d4d3fc16..e10e20a04 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,31 +1,31 @@ version: 0.1 cli: - version: 1.24.0 + version: 1.25.0 plugins: sources: - id: trunk - ref: v1.7.1 + ref: v1.7.2 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.451 - - renovate@41.40.0 + - checkov@3.2.469 + - renovate@41.94.0 - prettier@3.6.2 - - trufflehog@3.90.1 + - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.64.1 - - taplo@0.9.3 - - ruff@0.12.4 + - trivy@0.66.0 + - taplo@0.10.0 + - ruff@0.12.11 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 - svgo@4.0.0 - actionlint@1.7.7 - flake8@7.3.0 - - hadolint@2.12.1-beta + - hadolint@2.13.1 - shfmt@3.6.0 - - shellcheck@0.10.0 + - shellcheck@0.11.0 - black@25.1.0 - git-diff-check - gitleaks@8.28.0 diff --git a/Dockerfile b/Dockerfile index e033b1bba..b1e151ac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions -FROM python:3.13-bookworm AS builder +FROM python:3.13-slim-trixie AS builder ARG PIO_ENV=native ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC @@ -36,7 +36,7 @@ RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/fir ##### PRODUCTION BUILD ############# -FROM debian:bookworm-slim +FROM debian:trixie-slim LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \ org.opencontainers.image.url="https://meshtastic.org" \ @@ -51,8 +51,8 @@ ENV TZ=Etc/UTC USER root RUN apt-get update && apt-get --no-install-recommends -y install \ - libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev \ - liborcania2.3 libulfius2.7 libssl3 \ + libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \ + liborcania2.3 libulfius2.7t64 libssl3t64 \ libx11-6 libinput10 libxkbcommon-x11-0 \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/lib/meshtasticd \ @@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \ # Fetch compiled binary from the builder COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ -COPY --from=builder /tmp/web /usr/share/meshtasticd/ +COPY --from=builder /tmp/web /usr/share/meshtasticd/web/ # Copy config templates COPY ./bin/config.d /etc/meshtasticd/available.d diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini index 4a77ec4b2..36effe017 100644 --- a/arch/nrf52/nrf52.ini +++ b/arch/nrf52/nrf52.ini @@ -23,7 +23,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 build_src_filter = - ${arduino_base.build_src_filter} - - - - - - - - - - + ${arduino_base.build_src_filter} - - - - - - - - - - - lib_deps= ${arduino_base.lib_deps} diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index 693ab63b7..95c3bf3d9 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip + https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip framework = arduino build_src_filter = @@ -17,7 +17,6 @@ build_src_filter = + - - - +<../variants/portduino> lib_deps = ${env.lib_deps} @@ -32,9 +31,12 @@ lib_deps = https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 + # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main + https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip build_flags = ${arduino_base.build_flags} + -D ARCH_PORTDUINO -fPIC -Isrc/platform/portduino -DRADIOLIB_EEPROM_UNSUPPORTED diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index d91607a7d..8b7d256b3 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -50,7 +50,7 @@ lib_deps = ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main - https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip + https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip lib_ignore = OneButton diff --git a/bin/config.d/lora-RAK6421.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml similarity index 56% rename from bin/config.d/lora-RAK6421.yaml rename to bin/config.d/lora-RAK6421-13300-slot1.yaml index bbf38a474..6f65f9ccd 100644 --- a/bin/config.d/lora-RAK6421.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -9,13 +9,4 @@ Lora: DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 - # CS: 8 - - - ### RAK13300in Slot 2 pins -# IRQ: 18 #IO6 -# Reset: 24 # IO4 -# Busy: 19 # IO5 -# # Ant_sw: 23 # IO3 -# spidev: spidev0.1 -# # CS: 7 \ No newline at end of file + # CS: 8 \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..cbc794d39 --- /dev/null +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -0,0 +1,8 @@ +Lora: + ### RAK13300in Slot 2 pins + IRQ: 18 #IO6 + Reset: 24 # IO4 + Busy: 19 # IO5 + # Ant_sw: 23 # IO3 + spidev: spidev0.1 + # CS: 7 \ No newline at end of file diff --git a/bin/device-install.bat b/bin/device-install.bat index 12bfd4f6e..56de4dc10 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -14,11 +14,11 @@ SET "LOGCOUNTER=0" SET "BPS_RESET=0" @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. -SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone" +SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv" SET "C3=esp32c3" @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" -SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3" +SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv" GOTO getopts :help @@ -100,7 +100,6 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( @@ -120,11 +119,10 @@ IF NOT "__%PYTHON%__"=="____" ( CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 -IF %ERRORLEVEL% GEQ 2 ( - @REM esptool exits with code 1 if help is displayed. +IF %ERRORLEVEL% EQU 9009 ( + @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 - GOTO eof ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." @@ -142,7 +140,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %BPS_RESET% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status GOTO eof ) @@ -234,14 +232,14 @@ IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!S @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase-flash || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x00 "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." @@ -253,9 +251,9 @@ EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. -@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename] +@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write-flash] [OFFSET] [Filename] @REM. -@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin" +@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 diff --git a/bin/device-install.sh b/bin/device-install.sh index 4674113b6..98937f29a 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -5,6 +5,10 @@ BPS_RESET=false TFT_BUILD=false MCU="" +# Constants +RESET_BAUD=1200 +FIRMWARE_OFFSET=0x00 + # Variant groups BIGDB_8MB=( "picomputer-s3" @@ -33,10 +37,9 @@ BIGDB_16MB=( "m5stack-cores3" "station-g2" "t-eth-elite" + "tlora-pager" "t-watch-s3" - "elecrow-adv-35-tft" - "elecrow-adv-24-28-tft" - "elecrow-adv1-43-50-70-tft" + "elecrow-adv" ) S3_VARIANTS=( "s3" @@ -47,6 +50,7 @@ S3_VARIANTS=( "station-g2" "unphone" "t-eth-elite" + "tlora-pager" "mesh-tab" "dreamcatcher" "ESP32-S3-Pico" @@ -121,7 +125,7 @@ while [ $# -gt 0 ]; do done if [[ $BPS_RESET == true ]]; then - $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status exit 0 fi @@ -201,12 +205,12 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then fi echo "Trying to flash ${FILENAME}, but first erasing and writing system information" - $ESPTOOL_CMD erase_flash - $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" + $ESPTOOL_CMD erase-flash + $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" - $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" + $ESPTOOL_CMD write-flash $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" - $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" + $ESPTOOL_CMD write-flash $OFFSET "${SPIFFSFILE}" else show_help diff --git a/bin/device-update.bat b/bin/device-update.bat index 6d55294a7..a263da992 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -6,6 +6,8 @@ SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" SET "ESPTOOL_BAUD=115200" +SET "RESET_BAUD=1200" +SET "UPDATE_OFFSET=0x10000" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" SET "CHANGE_MODE=0" @@ -85,14 +87,13 @@ IF "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( - SET "ESPTOOL_CMD=!PYTHON! -m esptool" + SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool" CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." ) ELSE ( - CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool... + CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." WHERE esptool >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( @REM WHERE exits with code 0 if esptool is found. @@ -105,11 +106,11 @@ IF NOT "__%PYTHON%__"=="____" ( CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 -IF %ERRORLEVEL% GEQ 2 ( - @REM esptool exits with code 1 if help is displayed. +CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%" +IF %ERRORLEVEL% EQU 9009 ( + @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 - GOTO eof ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." @@ -127,13 +128,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %CHANGE_MODE% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status GOTO eof ) @REM Flashing operations. -CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof +CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..." +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." @@ -145,9 +146,9 @@ EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. -@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename] +@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename] @REM. -@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin" +@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 diff --git a/bin/device-update.sh b/bin/device-update.sh index 2196d3af9..6f29496e9 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -3,6 +3,11 @@ PYTHON=${PYTHON:-$(which python3 python|head -n 1)} CHANGE_MODE=false +# Constants +FLASH_BAUD=115200 +RESET_BAUD=1200 +UPDATE_OFFSET=0x10000 + # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then ESPTOOL_CMD="$PYTHON -m esptool" @@ -64,7 +69,7 @@ done shift "$((OPTIND-1))" if [ "$CHANGE_MODE" = true ]; then - $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status exit 0 fi @@ -75,7 +80,7 @@ fi if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then echo "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}" + $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" else show_help echo "Invalid file: ${FILENAME}" diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index d52b804ee..108ca4910 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,18 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5 diff --git a/boards/heltec_mesh_solar.json b/boards/heltec_mesh_solar.json new file mode 100644 index 000000000..9e551c082 --- /dev/null +++ b/boards/heltec_mesh_solar.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x0071"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_solar", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/project/meshsolar/", + "vendor": "Heltec" +} diff --git a/boards/meshtiny.json b/boards/meshtiny.json new file mode 100644 index 000000000..1473388ea --- /dev/null +++ b/boards/meshtiny.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "MeshTiny", + "mcu": "nrf52840", + "variant": "meshtiny", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "freertos"], + "name": "MeshTiny", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://github.com/meshtastic/firmware", + "vendor": "MTools Tec" +} diff --git a/boards/wiscore_rak3172.json b/boards/wiscore_rak3172.json index 714e09115..69ee506b4 100644 --- a/boards/wiscore_rak3172.json +++ b/boards/wiscore_rak3172.json @@ -5,7 +5,7 @@ }, "core": "stm32", "cpu": "cortex-m4", - "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX", + "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "f_cpu": "48000000L", "mcu": "stm32wle5ccu", "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", diff --git a/debian/changelog b/debian/changelog index 9421e9925..29841d0db 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.5.0) UNRELEASED; urgency=medium +meshtasticd (2.7.9.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -37,4 +37,14 @@ meshtasticd (2.7.5.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Sat, 09 Aug 2025 12:46:53 +0000 + [ ] + * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + -- Wed, 03 Sep 2025 23:39:17 +0000 diff --git a/platformio.ini b/platformio.ini index 95fe58608..8d4f093f0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,9 +60,9 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip - # renovate: datasource=custom.pio depName=OneButton packageName=mathertel/library/OneButton - mathertel/OneButton@2.6.1 + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip + # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master + https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip # renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master @@ -101,6 +101,14 @@ lib_deps = # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient arduino-libraries/NTPClient@3.2.1 +; Minimal networking libs for nrf52 (excludes Syslog to save flash) +[nrf52_networking_base] +lib_deps = + # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient + thingsboard/TBPubSubClient@2.12.1 + # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient + arduino-libraries/NTPClient@3.2.1 + [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib @@ -109,7 +117,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/0cd108ff783539e41ef38258ba2784ab3b1bdc97.zip + https://github.com/meshtastic/device-ui/archive/a04bc94b45dacdabf3ae1832d4591390e35fc61f.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -148,8 +156,8 @@ lib_deps = emotibit/EmotiBit MLX90632@1.0.8 # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library adafruit/Adafruit MLX90614 Library@2.1.5 - # renovate: datasource=github-tags depName=INA3221 packageName=KodinLanewave/INA3221 - https://github.com/KodinLanewave/INA3221/archive/1.0.1.zip + # renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 + https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU @@ -168,6 +176,8 @@ lib_deps = adafruit/Adafruit PCT2075@1.0.5 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 dfrobot/DFRobot_BMM150@1.0.0 + # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 + adafruit/Adafruit TSL2561@1.1.2 ; (not included in native / portduino) [environmental_extra] diff --git a/protobufs b/protobufs index e2c0831aa..a84657c22 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit e2c0831aa3d34a58a36c2b9fdcb828e58961cbc5 +Subproject commit a84657c220421536f18d11fc5edf680efadbceeb diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index f6bb43cc2..680aec929 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -89,14 +89,22 @@ class BluetoothStatus : public Status case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); #ifdef BLE_LED +#ifdef BLE_LED_INVERTED + digitalWrite(BLE_LED, LOW); +#else digitalWrite(BLE_LED, HIGH); +#endif #endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); #ifdef BLE_LED +#ifdef BLE_LED_INVERTED + digitalWrite(BLE_LED, HIGH); +#else digitalWrite(BLE_LED, LOW); +#endif #endif break; } diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 44bc0897b..d367aa661 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -1,7 +1,14 @@ #include "DisplayFormatters.h" -const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName) +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset) { + + // If use_preset is false, always return "Custom" + if (!usePreset) { + return "Custom"; + } + switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return useShortName ? "ShortT" : "ShortTurbo"; diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index f8ccfcbb6..2d7a3e8db 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -4,5 +4,6 @@ class DisplayFormatters { public: - static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName); + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset); }; diff --git a/src/Power.cpp b/src/Power.cpp index 8a16132f1..06c6a9089 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -128,6 +128,7 @@ RAK9154Sensor rak9154Sensor; #ifdef HAS_PPM // note: XPOWERS_CHIP_XXX must be defined in variant.h #include +XPowersPPM *PPM = NULL; #endif #ifdef HAS_BQ27220 @@ -681,6 +682,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (meshSolarInit()) { + found = true; } else if (analogInit()) { found = true; } @@ -743,7 +746,11 @@ void Power::shutdown() #if HAS_SCREEN if (screen) { +#ifdef T_DECK_PRO + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button +#else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen +#endif } #endif #if !defined(ARCH_STM32WL) @@ -761,7 +768,7 @@ void Power::shutdown() #ifdef PIN_LED3 ledOff(PIN_LED3); #endif - doDeepSleep(DELAY_FOREVER, false, true); + doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) exit(EXIT_SUCCESS); #else @@ -826,16 +833,25 @@ void Power::readPowerStatus() newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP if (lastheap != memGet.getFreeHeap()) { - std::string threadlist = "Threads running:"; + // Use stack-allocated buffer to avoid heap allocations in monitoring code + char threadlist[256] = "Threads running:"; + int threadlistLen = strlen(threadlist); int running = 0; for (int i = 0; i < MAX_THREADS; i++) { auto thread = concurrency::mainController.get(i); if ((thread != nullptr) && (thread->enabled)) { - threadlist += vformat(" %s", thread->ThreadName.c_str()); + // Use snprintf to safely append to stack buffer without heap allocation + int remaining = sizeof(threadlist) - threadlistLen - 1; + if (remaining > 0) { + int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); + if (written > 0 && written < remaining) { + threadlistLen += written; + } + } running++; } } - LOG_DEBUG(threadlist.c_str()); + LOG_DEBUG(threadlist); LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); lastheap = memGet.getFreeHeap(); @@ -849,15 +865,19 @@ void Power::readPowerStatus() sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); auto newHeap = memGet.getFreeHeap(); - std::string heapTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac); - std::string heapString = std::to_string(newHeap); - mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false); + // Use stack-allocated buffers to avoid heap allocations in monitoring code + char heapTopic[128]; + snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char heapString[16]; + snprintf(heapString, sizeof(heapString), "%u", newHeap); + mqtt->pubSub.publish(heapTopic, heapString, false); + auto wifiRSSI = WiFi.RSSI(); - std::string wifiTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac); - std::string wifiString = std::to_string(wifiRSSI); - mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false); + char wifiTopic[128]; + snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char wifiString[16]; + snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); + mqtt->pubSub.publish(wifiTopic, wifiString, false); } #endif @@ -1318,7 +1338,6 @@ bool Power::lipoInit() class LipoCharger : public HasBatteryLevel { private: - XPowersPPM *ppm = nullptr; BQ27220 *bq = nullptr; public: @@ -1327,41 +1346,41 @@ class LipoCharger : public HasBatteryLevel */ bool runOnce() { - if (ppm == nullptr) { - ppm = new XPowersPPM; - bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); + if (PPM == nullptr) { + PPM = new XPowersPPM; + bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); // Set the minimum operating voltage. Below this voltage, the PPM will protect - // ppm->setSysPowerDownVoltage(3100); + // PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA - // ppm->setInputCurrentLimit(800); + // PPM->setInputCurrentLimit(800); // Disable current limit pin - // ppm->disableCurrentLimitPin(); + // PPM->disableCurrentLimitPin(); // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV - ppm->setChargeTargetVoltage(4288); + PPM->setChargeTargetVoltage(4288); // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA - // ppm->setPrechargeCurr(64); + // PPM->setPrechargeCurr(64); // The premise is that limit pin is disabled, or it will // only follow the maximum charging current set by limit pin. // Set the charging current , Range:0~5056mA ,step:64mA - ppm->setChargerConstantCurr(1024); + PPM->setChargerConstantCurr(1024); // To obtain voltage data, the ADC must be enabled first - ppm->enableMeasure(); + PPM->enableMeasure(); // Turn on charging function // If there is no battery connected, do not turn on the charging function - ppm->enableCharge(); + PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); - delete ppm; - ppm = nullptr; + delete PPM; + PPM = nullptr; return false; } } @@ -1402,23 +1421,23 @@ class LipoCharger : public HasBatteryLevel /** * return true if there is a battery installed in this unit */ - virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; } + virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; } + virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } /** * return true if the battery is currently charging */ virtual bool isCharging() override { - bool isCharging = ppm->isCharging(); + bool isCharging = PPM->isCharging(); if (isCharging) { LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); } else { - if (!ppm->isVbusIn()) { + if (!PPM->isVbusIn()) { LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); } } @@ -1450,3 +1469,73 @@ bool Power::lipoChargerInit() return false; } #endif + +#ifdef HELTEC_MESH_SOLAR +#include "meshSolarApp.h" + +/** + * meshSolar class for an SMBUS battery sensor. + */ +class meshSolarBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + meshSolarStart(); + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } + + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return meshSolarIsCharging(); } +}; + +meshSolarBatteryLevel meshSolarLevel; + +/** + * Init the meshSolar battery level sensor + */ +bool Power::meshSolarInit() +{ + bool result = meshSolarLevel.runOnce(); + LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &meshSolarLevel; + return true; +} + +#else +/** + * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::meshSolarInit() +{ + return false; +} +#endif \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 68c41980d..093a24678 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -64,6 +64,14 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con int32_t SerialConsole::runOnce() { +#ifdef HELTEC_MESH_SOLAR + //After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. + if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port + && moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) + { + return 250; + } +#endif return runOncePart(); } diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index b09d7a82c..b0d162a44 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -140,6 +140,10 @@ bool playNextLeadUpNote() playTones(¬e, 1); // Play single note using existing playTones function leadUpNoteIndex++; + + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // this was the final note + } return true; // Note was played (playTones handles buzzer availability internally) } diff --git a/src/configuration.h b/src/configuration.h index 0e24990b5..81632c89e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -26,10 +26,10 @@ along with this program. If not, see . #include -#ifdef RV3028_RTC +#if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif -#ifdef PCF8563_RTC +#if __has_include("pcf8563.h") #include "pcf8563.h" #endif @@ -135,7 +135,7 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- -#if defined(SEEED_WIO_TRACKER_L1) +#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index c1358861b..470a416c0 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -79,7 +79,9 @@ class ScanI2C BQ27220, LTR553ALS, BHI260AP, - BMM150 + BMM150, + TSL2561, + DRV2605 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 8b3670cd9..5cb4fca32 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -461,7 +461,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address); + case TSL25911_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + if (registerValue == 0x50) { + type = TSL2591; + logFoundDevice("TSL25911", (uint8_t)addr.address); + } else { + type = TSL2561; + logFoundDevice("TSL2561", (uint8_t)addr.address); + } + break; + SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); @@ -483,8 +493,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = MLX90614; logFoundDevice("MLX90614", (uint8_t)addr.address); } else { - type = MPR121KB; - logFoundDevice("MPR121KB", (uint8_t)addr.address); + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS + if (registerValue == 0xe0) { + type = DRV2605; + logFoundDevice("DRV2605", (uint8_t)addr.address); + } else { + type = MPR121KB; + logFoundDevice("MPR121KB", (uint8_t)addr.address); + } } break; diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index ae74f0fe2..9ae7ae97d 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -843,9 +843,6 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) setPowerPMU(true); // Power (PMU): on writePinStandby(false); // Standby (pin): awake (not standby) setPowerUBLOX(true); // Standby (UBLOX): awake -#ifdef GNSS_AIROHA - lastFixStartMsec = 0; -#endif break; case GPS_SOFTSLEEP: @@ -863,9 +860,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep (not awake) setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; @@ -877,9 +872,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; } @@ -1062,6 +1055,8 @@ void GPS::down() } // If update interval long enough (or softsleep unsupported): hardsleep instead setPowerState(GPS_HARDSLEEP, sleepTime); + // Reset the fix quality to 0, since we're off. + fixQual = 0; } } @@ -1121,11 +1116,19 @@ int32_t GPS::runOnce() shouldPublish = true; } + uint8_t prev_fixQual = fixQual; bool gotLoc = lookForLocation(); if (gotLoc && !hasValidLocation) { // declare that we have location ASAP LOG_DEBUG("hasValidLocation RISING EDGE"); hasValidLocation = true; shouldPublish = true; + // Hold for 20secs after getting a lock to download ephemeris etc + fixHoldEnds = millis() + 20000; + } + + if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on. + fixHoldEnds = millis() + 20000; + shouldPublish = true; // Publish immediately, since next publish is at end of hold } bool tooLong = scheduling.searchedTooLong(); @@ -1134,8 +1137,7 @@ int32_t GPS::runOnce() // Once we get a location we no longer desperately want an update if ((gotLoc && gotTime) || tooLong) { - - if (tooLong) { + if (tooLong && !gotLoc) { // we didn't get a location during this ack window, therefore declare loss of lock if (hasValidLocation) { LOG_DEBUG("hasValidLocation FALLING EDGE"); @@ -1143,9 +1145,15 @@ int32_t GPS::runOnce() p = meshtastic_Position_init_default; hasValidLocation = false; } - - down(); - shouldPublish = true; // publish our update for this just finished acquisition window + if (millis() > fixHoldEnds) { + shouldPublish = true; // publish our update at the end of the lock hold + publishUpdate(); + down(); +#ifdef GPS_DEBUG + } else { + LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); +#endif + } } // If state has changed do a publish @@ -1504,28 +1512,10 @@ static int32_t toDegInt(RawDegrees d) * Perform any processing that should be done only while the GPS is awake and looking for a fix. * Override this method to check for new locations * - * @return true if we've acquired a new location + * @return true if we've set a new time */ bool GPS::lookForTime() { - -#ifdef GNSS_AIROHA - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } -#endif auto ti = reader.time; auto d = reader.date; if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed @@ -1544,11 +1534,12 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s if (t.tm_mon > -1) { LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { - // Clear the GPS buffer if we got an invalid time - clearBuffer(); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("Time set."); + return true; + } else { + return false; } - return true; } else return false; } else @@ -1563,25 +1554,6 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s */ bool GPS::lookForLocation() { -#ifdef GNSS_AIROHA - if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) { - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } - } -#endif // By default, TinyGPS++ does not parse GPGSA lines, which give us // the 2D/3D fixType (see NMEAGPS.h) // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 9be57017f..177cfe74b 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -159,7 +159,7 @@ class GPS : private concurrency::OSThread uint8_t fixType = 0; // fix type from GPGSA #endif - uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0; + uint32_t fixHoldEnds = 0; uint32_t rx_gpio = 0; uint32_t tx_gpio = 0; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index d574c9ad0..e208e2df9 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -23,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -void readFromRTC() +RTCSetResult readFromRTC() { struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC @@ -44,15 +44,23 @@ void readFromRTC() t.tm_sec = rtc.getSecond(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #elif defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { @@ -75,15 +83,23 @@ void readFromRTC() t.tm_sec = tc.second; tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #else if (!gettimeofday(&tv, NULL)) { @@ -92,8 +108,10 @@ void readFromRTC() LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; + return RTCSetResultSuccess; } #endif + return RTCSetResultNotSet; } /** @@ -101,7 +119,7 @@ void readFromRTC() * * @param q The quality of the provided time. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. - * @return True if the RTC was set, false otherwise. + * @return RTCSetResult * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ @@ -114,6 +132,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd if (tv->tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); return RTCSetResultInvalidTime; + } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, + BUILD_EPOCH + FORTY_YEARS); + return RTCSetResultInvalidTime; } #endif @@ -232,6 +254,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) if (tv.tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); return RTCSetResultInvalidTime; + } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, + BUILD_EPOCH + FORTY_YEARS); + return RTCSetResultInvalidTime; } #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 96dec575b..03350823c 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -48,10 +48,13 @@ uint32_t getTime(bool local = false); /// Return time since 1970 in secs. If quality is RTCQualityNone return zero uint32_t getValidTime(RTCQuality minQuality, bool local = false); -void readFromRTC(); +RTCSetResult readFromRTC(); time_t gm_mktime(struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 +#ifdef BUILD_EPOCH +#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware +#endif diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index a627a42cc..c0c09cc27 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -67,20 +67,28 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) // FIXME - only draw bits have changed (use backbuf similar to the other displays) const bool flipped = config.display.flip_screen; + // HACK for L1 EInk +#if defined(SEEED_WIO_TRACKER_L1_EINK) + // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + } + } +#else for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); - - // Handle flip here, rather than with setRotation(), - // Avoids issues when display width is not a multiple of 8 if (flipped) adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); else adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); } } +#endif // Trigger the refresh in GxEPD2 LOG_DEBUG("Update E-Paper"); @@ -140,13 +148,13 @@ bool EInkDisplay::connect() #endif #endif -#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) +#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(); -#ifdef ELECROW_ThinkNode_M1 +#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) adafruitDisplay->setRotation(4); #else adafruitDisplay->setRotation(3); @@ -235,7 +243,7 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } -#elif defined(HELTEC_MESH_POCKET) +#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) { spi1 = &SPI1; spi1->begin(); @@ -249,6 +257,7 @@ bool EInkDisplay::connect() // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index b840ce9ba..b4cee81fe 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay SPIClass *hspi = NULL; #endif -#if defined(HELTEC_MESH_POCKET) +#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) SPIClass *spi1 = NULL; #endif diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8d5635f89..dea08d5ba 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -318,7 +318,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) @@ -365,9 +365,6 @@ void Screen::doDeepSleep() { #ifdef USE_EINK setOn(false, graphics::UIRenderer::drawDeepSleepFrame); -#ifdef PIN_EINK_EN - digitalWrite(PIN_EINK_EN, LOW); // power off backlight -#endif #else // Without E-Ink display: setOn(false); @@ -391,8 +388,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) dispdev->displayOn(); #endif -#ifdef ELECROW_ThinkNode_M5 - io.digitalWrite(PCA_PIN_EINK_EN, HIGH); +#ifdef PIN_EINK_EN + if (uiconfig.screen_brightness == 1) + digitalWrite(PIN_EINK_EN, HIGH); +#elif defined(PCA_PIN_EINK_EN) + if (uiconfig.screen_brightness == 1) + io.digitalWrite(PCA_PIN_EINK_EN, HIGH); #endif #if defined(ST7789_CS) && \ @@ -424,13 +425,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) // eInkScreensaver parameter is usually NULL (default argument), default frame used instead setScreensaverFrames(einkScreensaver); #endif -#ifdef ELECROW_ThinkNode_M1 - if (digitalRead(PIN_EINK_EN) == HIGH) { - digitalWrite(PIN_EINK_EN, LOW); - } -#endif -#ifdef ELECROW_ThinkNode_M5 +#ifdef PIN_EINK_EN + digitalWrite(PIN_EINK_EN, LOW); +#elif defined(PCA_PIN_EINK_EN) io.digitalWrite(PCA_PIN_EINK_EN, LOW); #endif @@ -552,7 +550,7 @@ void Screen::setup() #else if (!config.display.flip_screen) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); @@ -694,7 +692,7 @@ int32_t Screen::runOnce() #ifndef DISABLE_WELCOME_UNSET if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - menuHandler::LoraRegionPicker(0); + menuHandler::OnboardMessage(); } #endif if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 3373a47a7..a25417b05 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -16,7 +16,7 @@ #include "graphics/fonts/OLEDDisplayFontsCS.h" #endif -#ifdef CROWPANEL_ESP32S3_5_EPAPER +#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif @@ -40,6 +40,9 @@ #ifdef OLED_PL #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19 #else +#ifdef OLED_RU +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_RU // Height: 19 +#else #ifdef OLED_UA #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19 #else @@ -50,9 +53,13 @@ #endif #endif #endif +#endif #ifdef OLED_PL #define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28 #else +#ifdef OLED_RU +#define FONT_LARGE_LOCAL ArialMT_Plain_24_RU // Height: 28 +#else #ifdef OLED_UA #define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28 #else @@ -63,9 +70,10 @@ #endif #endif #endif +#endif #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 @@ -77,7 +85,7 @@ #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) +#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #undef FONT_SMALL #undef FONT_MEDIUM #undef FONT_LARGE diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 3e9bafc6c..37ea9b94a 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -562,6 +562,91 @@ class LGFX : public lgfx::LGFX_Device static LGFX *tft = nullptr; +#elif defined(ST7796_CS) +#include // Graphics and font library for ST7796 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7796 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // SPI + cfg.spi_host = ST7796_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) + + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) +#ifdef TFT_DUMMY_READ_PIXELS + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout +#else + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout +#endif + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + _panel_instance.config(cfg); + } + +#ifdef ST7796_BL + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + cfg.freq = 44100; + cfg.pwm_channel = 7; + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. + } +}; + +static LGFX *tft = nullptr; + #elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) #include // Graphics and font library for ILI9341/ILI9342 driver chip @@ -667,15 +752,19 @@ static LGFX *tft = nullptr; static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h #elif ARCH_PORTDUINO #include // Graphics and font library for ST7735 driver chip +#if defined(LGFX_SDL) +#include +#endif class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_Device *_panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::ITouch *_touch_instance; public: + lgfx::Panel_Device *_panel_instance; + LGFX(void) { if (settingsMap[displayPanel] == st7789) @@ -694,6 +783,11 @@ class LGFX : public lgfx::LGFX_Device _panel_instance = new lgfx::Panel_ILI9488; else if (settingsMap[displayPanel] == hx8357d) _panel_instance = new lgfx::Panel_HX8357D; +#if defined(LGFX_SDL) + else if (settingsMap[displayPanel] == x11) { + _panel_instance = new lgfx::Panel_sdl; + } +#endif else { _panel_instance = new lgfx::Panel_NULL; LOG_ERROR("Unknown display panel configured!"); @@ -754,7 +848,13 @@ class LGFX : public lgfx::LGFX_Device _touch_instance->config(touch_cfg); _panel_instance->setTouch(_touch_instance); } - +#if defined(LGFX_SDL) + if (settingsMap[displayPanel] == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; + sdl_panel_->setup(); + sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); + } +#endif setPanel(_panel_instance); // Sets the panel to use. } }; @@ -849,9 +949,29 @@ static LGFX *tft = nullptr; #include #include +class PanelInit_ST7701 : public lgfx::Panel_ST7701 +{ + public: + const uint8_t *getInitCommands(uint8_t listno) const override + { + // 180 degree hw rotation: vertical flip, horizontal flip + static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL + 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip) + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS + 0xFF, 0xFF}; + switch (listno) { + case 1: + return list1; + default: + return lgfx::Panel_ST7701::getInitCommands(listno); + } + } +}; + class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_ST7701 _panel_instance; + PanelInit_ST7701 _panel_instance; lgfx::Bus_RGB _bus_instance; lgfx::Light_PWM _light_instance; lgfx::Touch_FT5x06 _touch_instance; @@ -962,8 +1082,9 @@ static LGFX *tft = nullptr; #endif -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0) +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ + defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ + (ARCH_PORTDUINO && HAS_SCREEN != 0) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -1007,37 +1128,154 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g #endif } +TFTDisplay::~TFTDisplay() +{ + // Clean up allocated line pixel buffer to prevent memory leak + if (linePixelBuffer != nullptr) { + free(linePixelBuffer); + linePixelBuffer = nullptr; + } +} + // Write the buffer to the display memory void TFTDisplay::display(bool fromBlank) { if (fromBlank) tft->fillScreen(TFT_BLACK); - // tft->clear(); + concurrency::LockGuard g(spiLock); - uint16_t x, y; + uint32_t x, y; + uint32_t y_byteIndex; + uint8_t y_byteMask; + uint32_t x_FirstPixelUpdate; + uint32_t x_LastPixelUpdate; + bool isset, dblbuf_isset; + uint16_t colorTftMesh, colorTftBlack; + bool somethingChanged = false; - for (y = 0; y < displayHeight; y++) { - for (x = 0; x < displayWidth; x++) { - auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); + // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step + colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); + colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + + y = 0; + while (y < displayHeight) { + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); + + // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. + if (y_byteMask == 1) { if (!fromBlank) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent - auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); - if (isset != dblbuf_isset) { - tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK); + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) + break; } - } else if (isset) { - tft->drawPixel(x, y, TFT_MESH); + } else { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != 0) + break; + } + } + if (x >= displayWidth) { + // No changed pixels found in these 8 rows, fast-forward to the next 8 + y = y + 8; + continue; } } + + // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating + for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { + isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses + dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + break; + } + } else if (isset) { + break; + } + } + + // Did we find a pixel that needs updating on this row? + if (x_FirstPixelUpdate < displayWidth) { + + // Quickly write out the first changed pixel (saves another array lookup) + linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; + x_LastPixelUpdate = x_FirstPixelUpdate; + + // Step 3: copy all remaining pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating + for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; + linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; + + if (!fromBlank) { + dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + x_LastPixelUpdate = x; + } + } else if (isset) { + x_LastPixelUpdate = x; + } + } + + // Step 4: Send the changed pixels on this line to the screen as a single block transfer. + // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. + tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, + &linePixelBuffer[x_FirstPixelUpdate]); + + somethingChanged = true; + } + y++; } // Copy the Buffer to the Back Buffer - for (y = 0; y < (displayHeight / 8); y++) { - for (x = 0; x < displayWidth; x++) { - uint16_t pos = x + y * displayWidth; - buffer_back[pos] = buffer[pos]; + if (somethingChanged) + memcpy(buffer_back, buffer, displayBufferSize); +} + +void TFTDisplay::sdlLoop() +{ +#if defined(LGFX_SDL) + static int lastPressed = 0; + static int shuttingDown = false; + if (settingsMap[displayPanel] == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; + if (sdl_panel_->loop() && !shuttingDown) { + LOG_WARN("Window Closed!"); + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + + // debounce + if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed)) + return; + if (!lgfx::v1::gpio_in(37)) { + lastPressed = 37; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(36)) { + lastPressed = 36; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(38)) { + lastPressed = 38; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(39)) { + lastPressed = 39; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) { + lastPressed = SDL_SCANCODE_KP_ENTER; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else { + lastPressed = 0; } } +#endif } // Send a command to the display (low level function) @@ -1184,15 +1422,23 @@ bool TFTDisplay::connect() attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); #elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) tft->setRotation(1); // T-Deck has the TFT in landscape -#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) +#elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation -#elif ARCH_PORTDUINO +#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif tft->fillScreen(TFT_BLACK); + if (this->linePixelBuffer == NULL) { + this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); + + if (!this->linePixelBuffer) { + LOG_ERROR("Not enough memory to create TFT line buffer\n"); + return false; + } + } return true; } diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 38cd53ebb..a64922d23 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -20,9 +20,13 @@ class TFTDisplay : public OLEDDisplay */ TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); + // Destructor to clean up allocated memory + ~TFTDisplay(); + // Write the buffer to the display memory virtual void display() override { display(false); }; virtual void display(bool fromBlank); + void sdlLoop(); // Turn the display upside down virtual void flipScreenVertically(); @@ -57,4 +61,6 @@ class TFTDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; + + uint16_t *linePixelBuffer = nullptr; }; \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5d9b5a33b..d5835a335 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -94,7 +94,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); @@ -106,7 +107,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); @@ -121,7 +122,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 } else { // TODO: Raspberry Pi supports more than just the one screen size #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); @@ -261,12 +263,6 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->drawString(x + 1, y, "USB"); } - // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - // if (config.display.heading_bold) - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - uint32_t currentMillis = millis(); uint32_t seconds = currentMillis / 1000; uint32_t minutes = seconds / 60; @@ -396,7 +392,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(nameX, getTextPositions(display)[line++], shortnameble); // === Second Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 9ce35f251..212c85e18 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -26,6 +26,27 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; +void menuHandler::OnboardMessage() +{ + static const char *optionsArray[] = {"OK", "Got it!"}; + enum optionsNumbers { OK, got }; + BannerOverlayOptions bannerOptions; +#if HAS_TFT + bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu."; +#elif defined(BUTTON_PIN) + bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu."; +#else + bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections."; +#endif + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; + screen->runNow(); + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::LoraRegionPicker(uint32_t duration) { static const char *optionsArray[] = {"Back", @@ -318,7 +339,7 @@ void menuHandler::homeBaseMenu() static int optionsEnumArray[enumEnd] = {Back}; int options = 1; -#ifdef PIN_EINK_EN +#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) optionsArray[options] = "Toggle Backlight"; optionsEnumArray[options++] = Backlight; #else @@ -342,12 +363,24 @@ void menuHandler::homeBaseMenu() bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Backlight) { -#ifdef PIN_EINK_EN - if (digitalRead(PIN_EINK_EN) == HIGH) { +#if defined(PIN_EINK_EN) + if (uiconfig.screen_brightness == 1) { + uiconfig.screen_brightness = 0; digitalWrite(PIN_EINK_EN, LOW); } else { + uiconfig.screen_brightness = 1; digitalWrite(PIN_EINK_EN, HIGH); } + saveUIConfig(); +#elif defined(PCA_PIN_EINK_EN) + if (uiconfig.screen_brightness == 1) { + uiconfig.screen_brightness = 0; + io.digitalWrite(PCA_PIN_EINK_EN, LOW); + } else { + uiconfig.screen_brightness = 1; + io.digitalWrite(PCA_PIN_EINK_EN, HIGH); + } + saveUIConfig(); #endif } else if (selected == Sleep) { screen->setOn(false); @@ -401,8 +434,8 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; -#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \ - defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT +#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \ + defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif @@ -696,7 +729,7 @@ void menuHandler::BrightnessPickerMenu() #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) // For HELTEC devices, use analogWrite to control backlight analogWrite(VTFT_LEDA, uiconfig.screen_brightness); -#elif defined(ST7789_CS) +#elif defined(ST7789_CS) || defined(ST7796_CS) static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); @@ -739,7 +772,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 10; bannerOptions.bannerCallback = [display](int selected) -> void { -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_b = 0; @@ -1016,7 +1049,7 @@ void menuHandler::screenOptionsMenu() } // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = ScreenColor; #endif @@ -1124,6 +1157,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case lora_picker: LoraRegionPicker(); break; + case no_timeout_lora_picker: + LoraRegionPicker(0); + break; case TZ_picker: TZPicker(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 87a0b055e..b15cf237d 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -10,6 +10,7 @@ class menuHandler enum screenMenus { menu_none, lora_picker, + no_timeout_lora_picker, TZ_picker, twelve_hour_picker, clock_face_picker, @@ -41,6 +42,7 @@ class menuHandler }; static screenMenus menuQueue; + static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 524f88f9b..117829167 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -137,7 +137,11 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string display->drawString(cursorX + 1, fontY, textChunk.c_str()); } display->drawString(cursorX, fontY, textChunk.c_str()); +#if defined(OLED_UA) || defined(OLED_RU) + cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); +#else cursorX += display->getStringWidth(textChunk.c_str()); +#endif i = nextControl; continue; } @@ -155,7 +159,12 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string display->drawString(cursorX + 1, fontY, remaining.c_str()); } display->drawString(cursorX, fontY, remaining.c_str()); +#if defined(OLED_UA) || defined(OLED_RU) + cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); +#else cursorX += display->getStringWidth(remaining.c_str()); +#endif + break; } } @@ -374,10 +383,16 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS } else { word += ch; std::string test = line + word; - // Keep these lines for diagnostics - // LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch); - // LOG_INFO("Current String: %s", test.c_str()); - if (display->getStringWidth(test.c_str()) > textWidth) { +// Keep these lines for diagnostics +// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch); +// LOG_INFO("Current String: %s", test.c_str()); +// Note: there are boolean comparison uint16 (getStringWidth) with int (textWidth), hope textWidth is always positive :) +#if defined(OLED_UA) || defined(OLED_RU) + uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); +#else + uint16_t strWidth = display->getStringWidth(test.c_str()); +#endif + if (strWidth > textWidth) { if (!line.empty()) lines.push_back(line); line = word; diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index d9cf280ac..3d635e588 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -383,7 +383,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp uint8_t firstOptionToShow = 0; if (alertBannerOptions > 0) { - if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (visibleTotalLines - lineCount == 1) { + firstOptionToShow = curSelected; + } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; else @@ -392,6 +394,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp firstOptionToShow = 0; } } + // Useful log line for troubleshooting: + /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u", + alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */ for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { if (i == curSelected) { diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 71d92616f..049722df8 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -194,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) if (isHighResolution) { diff --git a/src/graphics/fonts/EinkDisplayFonts.cpp b/src/graphics/fonts/EinkDisplayFonts.cpp index cfe2c931f..497b3b389 100644 --- a/src/graphics/fonts/EinkDisplayFonts.cpp +++ b/src/graphics/fonts/EinkDisplayFonts.cpp @@ -1,3 +1,5 @@ +#ifdef USE_EINK + #include "EinkDisplayFonts.h" // Created by https://oleddisplay.squix.ch/ Consider a donation @@ -1182,3 +1184,5 @@ const uint8_t Monospaced_plain_30[] PROGMEM = { 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F, 0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255 }; + +#endif // USE_EINK diff --git a/src/graphics/fonts/EinkDisplayFonts.h b/src/graphics/fonts/EinkDisplayFonts.h index 342525a19..a4a44ba47 100644 --- a/src/graphics/fonts/EinkDisplayFonts.h +++ b/src/graphics/fonts/EinkDisplayFonts.h @@ -1,6 +1,8 @@ #ifndef EINKDISPLAYFONTS_h #define EINKDISPLAYFONTS_h +#ifdef USE_EINK + #ifdef ARDUINO #include #elif __MBED__ @@ -11,4 +13,7 @@ * Monospaced Plain 30 */ extern const uint8_t Monospaced_plain_30[] PROGMEM; + +#endif // USE_EINK + #endif diff --git a/src/graphics/fonts/OLEDDisplayFontsCS.cpp b/src/graphics/fonts/OLEDDisplayFontsCS.cpp index 67208b4d9..c8045285e 100644 --- a/src/graphics/fonts/OLEDDisplayFontsCS.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsCS.cpp @@ -1,3 +1,5 @@ +#ifdef OLED_CS + #include "OLEDDisplayFontsCS.h" // Font generated or edited with the glyphEditor @@ -1860,4 +1862,6 @@ const uint8_t ArialMT_Plain_24_CS[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 -}; \ No newline at end of file +}; + +#endif // OLED_CS \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsPL.cpp b/src/graphics/fonts/OLEDDisplayFontsPL.cpp index 0767e24e7..00f0913fe 100644 --- a/src/graphics/fonts/OLEDDisplayFontsPL.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsPL.cpp @@ -1,4 +1,5 @@ // trunk-ignore-all(clang-format): Preserve long lines +#ifdef OLED_PL #include "OLEDDisplayFontsPL.h" const uint8_t ArialMT_Plain_10_PL[] PROGMEM = { @@ -1310,4 +1311,6 @@ const uint8_t ArialMT_Plain_24_PL[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 -}; \ No newline at end of file +}; + +#endif // OLED_PL \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp index fa055d8b5..3a1159511 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -1,3 +1,5 @@ +#ifdef OLED_RU + #include "OLEDDisplayFontsRU.h" // Font generated or edited with the glyphEditor @@ -423,4 +425,1345 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { 0x00, 0x00, 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 0x00, 0x00, 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0xE0, 0x03, // 255 -}; \ No newline at end of file +}; + +// Font generated or edited with the glyphEditor (@mrekin) +const uint8_t ArialMT_Plain_16_RU[] PROGMEM = { + 0x10, // Width: 16 + 0x13, // Height: 19 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + // Jump Table: + 0xFF, 0xFF, 0x00, 0x04, // 32 + 0x00, 0x00, 0x08, 0x05, // 33 + 0x00, 0x08, 0x0D, 0x06, // 34 + 0x00, 0x15, 0x1A, 0x09, // 35 + 0x00, 0x2F, 0x17, 0x09, // 36 + 0x00, 0x46, 0x26, 0x0E, // 37 + 0x00, 0x6C, 0x1D, 0x0B, // 38 + 0x00, 0x89, 0x04, 0x03, // 39 + 0x00, 0x8D, 0x0C, 0x05, // 40 + 0x00, 0x99, 0x0B, 0x05, // 41 + 0x00, 0xA4, 0x0D, 0x06, // 42 + 0x00, 0xB1, 0x17, 0x09, // 43 + 0x00, 0xC8, 0x09, 0x04, // 44 + 0x00, 0xD1, 0x0B, 0x05, // 45 + 0x00, 0xDC, 0x08, 0x04, // 46 + 0x00, 0xE4, 0x0A, 0x04, // 47 + 0x00, 0xEE, 0x17, 0x09, // 48 + 0x01, 0x05, 0x11, 0x09, // 49 + 0x01, 0x16, 0x17, 0x09, // 50 + 0x01, 0x2D, 0x17, 0x09, // 51 + 0x01, 0x44, 0x17, 0x09, // 52 + 0x01, 0x5B, 0x17, 0x09, // 53 + 0x01, 0x72, 0x17, 0x09, // 54 + 0x01, 0x89, 0x16, 0x09, // 55 + 0x01, 0x9F, 0x17, 0x09, // 56 + 0x01, 0xB6, 0x17, 0x09, // 57 + 0x01, 0xCD, 0x05, 0x04, // 58 + 0x01, 0xD2, 0x06, 0x04, // 59 + 0x01, 0xD8, 0x17, 0x09, // 60 + 0x01, 0xEF, 0x17, 0x09, // 61 + 0x02, 0x06, 0x17, 0x09, // 62 + 0x02, 0x1D, 0x16, 0x09, // 63 + 0x02, 0x33, 0x2F, 0x10, // 64 + 0x02, 0x62, 0x1D, 0x0B, // 65 + 0x02, 0x7F, 0x1D, 0x0B, // 66 + 0x02, 0x9C, 0x20, 0x0C, // 67 + 0x02, 0xBC, 0x20, 0x0C, // 68 + 0x02, 0xDC, 0x1D, 0x0B, // 69 + 0x02, 0xF9, 0x19, 0x0A, // 70 + 0x03, 0x12, 0x20, 0x0C, // 71 + 0x03, 0x32, 0x1D, 0x0B, // 72 + 0x03, 0x4F, 0x05, 0x03, // 73 + 0x03, 0x54, 0x14, 0x08, // 74 + 0x03, 0x68, 0x1D, 0x0B, // 75 + 0x03, 0x85, 0x17, 0x09, // 76 + 0x03, 0x9C, 0x23, 0x0D, // 77 + 0x03, 0xBF, 0x1D, 0x0B, // 78 + 0x03, 0xDC, 0x20, 0x0C, // 79 + 0x03, 0xFC, 0x1C, 0x0B, // 80 + 0x04, 0x18, 0x20, 0x0C, // 81 + 0x04, 0x38, 0x1D, 0x0B, // 82 + 0x04, 0x55, 0x1D, 0x0B, // 83 + 0x04, 0x72, 0x19, 0x09, // 84 + 0x04, 0x8B, 0x1D, 0x0B, // 85 + 0x04, 0xA8, 0x1C, 0x0B, // 86 + 0x04, 0xC4, 0x2B, 0x0F, // 87 + 0x04, 0xEF, 0x20, 0x0B, // 88 + 0x05, 0x0F, 0x19, 0x09, // 89 + 0x05, 0x28, 0x1A, 0x09, // 90 + 0x05, 0x42, 0x0C, 0x04, // 91 + 0x05, 0x4E, 0x0B, 0x04, // 92 + 0x05, 0x59, 0x09, 0x04, // 93 + 0x05, 0x62, 0x14, 0x07, // 94 + 0x05, 0x76, 0x1B, 0x09, // 95 + 0x05, 0x91, 0x07, 0x05, // 96 + 0x05, 0x98, 0x17, 0x09, // 97 + 0x05, 0xAF, 0x17, 0x09, // 98 + 0x05, 0xC6, 0x14, 0x08, // 99 + 0x05, 0xDA, 0x17, 0x09, // 100 + 0x05, 0xF1, 0x17, 0x09, // 101 + 0x06, 0x08, 0x0A, 0x04, // 102 + 0x06, 0x12, 0x17, 0x09, // 103 + 0x06, 0x29, 0x14, 0x08, // 104 + 0x06, 0x3D, 0x05, 0x04, // 105 + 0x06, 0x42, 0x06, 0x03, // 106 + 0x06, 0x48, 0x17, 0x08, // 107 + 0x06, 0x5F, 0x05, 0x03, // 108 + 0x06, 0x64, 0x23, 0x0D, // 109 + 0x06, 0x87, 0x14, 0x08, // 110 + 0x06, 0x9B, 0x17, 0x09, // 111 + 0x06, 0xB2, 0x17, 0x09, // 112 + 0x06, 0xC9, 0x18, 0x09, // 113 + 0x06, 0xE1, 0x0D, 0x05, // 114 + 0x06, 0xEE, 0x14, 0x08, // 115 + 0x07, 0x02, 0x0B, 0x04, // 116 + 0x07, 0x0D, 0x14, 0x08, // 117 + 0x07, 0x21, 0x13, 0x07, // 118 + 0x07, 0x34, 0x1F, 0x0B, // 119 + 0x07, 0x53, 0x14, 0x07, // 120 + 0x07, 0x67, 0x13, 0x07, // 121 + 0x07, 0x7A, 0x14, 0x07, // 122 + 0x07, 0x8E, 0x0F, 0x05, // 123 + 0x07, 0x9D, 0x06, 0x03, // 124 + 0x07, 0xA3, 0x0E, 0x05, // 125 + 0x07, 0xB1, 0x17, 0x09, // 126 + 0xFF, 0xFF, 0x00, 0x10, // 127 + 0xFF, 0xFF, 0x00, 0x10, // 128 + 0xFF, 0xFF, 0x00, 0x10, // 129 + 0xFF, 0xFF, 0x00, 0x10, // 130 + 0xFF, 0xFF, 0x00, 0x10, // 131 + 0xFF, 0xFF, 0x00, 0x10, // 132 + 0xFF, 0xFF, 0x00, 0x10, // 133 + 0xFF, 0xFF, 0x00, 0x10, // 134 + 0xFF, 0xFF, 0x00, 0x10, // 135 + 0xFF, 0xFF, 0x00, 0x10, // 136 + 0xFF, 0xFF, 0x00, 0x10, // 137 + 0xFF, 0xFF, 0x00, 0x10, // 138 + 0xFF, 0xFF, 0x00, 0x10, // 139 + 0xFF, 0xFF, 0x00, 0x10, // 140 + 0xFF, 0xFF, 0x00, 0x10, // 141 + 0xFF, 0xFF, 0x00, 0x10, // 142 + 0xFF, 0xFF, 0x00, 0x10, // 143 + 0xFF, 0xFF, 0x00, 0x10, // 144 + 0xFF, 0xFF, 0x00, 0x10, // 145 + 0xFF, 0xFF, 0x00, 0x10, // 146 + 0xFF, 0xFF, 0x00, 0x10, // 147 + 0xFF, 0xFF, 0x00, 0x10, // 148 + 0xFF, 0xFF, 0x00, 0x10, // 149 + 0xFF, 0xFF, 0x00, 0x10, // 150 + 0xFF, 0xFF, 0x00, 0x10, // 151 + 0xFF, 0xFF, 0x00, 0x10, // 152 + 0xFF, 0xFF, 0x00, 0x10, // 153 + 0xFF, 0xFF, 0x00, 0x10, // 154 + 0xFF, 0xFF, 0x00, 0x10, // 155 + 0xFF, 0xFF, 0x00, 0x10, // 156 + 0xFF, 0xFF, 0x00, 0x10, // 157 + 0xFF, 0xFF, 0x00, 0x10, // 158 + 0xFF, 0xFF, 0x00, 0x10, // 159 + 0xFF, 0xFF, 0x00, 0x10, // 160 + 0x07, 0xC8, 0x09, 0x05, // 161 + 0x07, 0xD1, 0x17, 0x09, // 162 + 0x07, 0xE8, 0x17, 0x09, // 163 + 0x07, 0xFF, 0x14, 0x09, // 164 + 0x08, 0x13, 0x1A, 0x09, // 165 + 0x08, 0x2D, 0x06, 0x03, // 166 + 0x08, 0x33, 0x17, 0x09, // 167 + 0x08, 0x4A, 0x1D, 0x0B, // 168 + 0x08, 0x67, 0x23, 0x0C, // 169 + 0x08, 0x8A, 0x0E, 0x05, // 170 + 0x08, 0x98, 0x14, 0x09, // 171 + 0x08, 0xAC, 0x17, 0x09, // 172 + 0x08, 0xC3, 0x0B, 0x05, // 173 + 0x08, 0xCE, 0x23, 0x0C, // 174 + 0x08, 0xF1, 0x19, 0x09, // 175 + 0x09, 0x0A, 0x0D, 0x06, // 176 + 0x09, 0x17, 0x17, 0x09, // 177 + 0x09, 0x2E, 0x0E, 0x05, // 178 + 0x09, 0x3C, 0x0D, 0x05, // 179 + 0x09, 0x49, 0x0A, 0x05, // 180 + 0x09, 0x53, 0x17, 0x09, // 181 + 0x09, 0x6A, 0x19, 0x09, // 182 + 0x09, 0x83, 0x08, 0x05, // 183 + 0x09, 0x8B, 0x17, 0x09, // 184 + 0x09, 0xA2, 0x0B, 0x05, // 185 + 0x09, 0xAD, 0x0D, 0x05, // 186 + 0x09, 0xBA, 0x17, 0x09, // 187 + 0x09, 0xD1, 0x26, 0x0D, // 188 + 0x09, 0xF7, 0x26, 0x0D, // 189 + 0x0A, 0x1D, 0x26, 0x0D, // 190 + 0x0A, 0x43, 0x1B, 0x0C, // 191 + 0x0A, 0x5E, 0x1D, 0x0B, // 192 + 0x0A, 0x7B, 0x1A, 0x0B, // 193 + 0x0A, 0x95, 0x1D, 0x0B, // 194 + 0x0A, 0xB2, 0x19, 0x09, // 195 + 0x0A, 0xCB, 0x1E, 0x0B, // 196 + 0x0A, 0xE9, 0x1D, 0x0B, // 197 + 0x0B, 0x06, 0x2C, 0x0F, // 198 + 0x0B, 0x32, 0x1A, 0x0A, // 199 + 0x0B, 0x4C, 0x20, 0x0C, // 200 + 0x0B, 0x6C, 0x20, 0x0C, // 201 + 0x0B, 0x8C, 0x1A, 0x09, // 202 + 0x0B, 0xA6, 0x1A, 0x0B, // 203 + 0x0B, 0xC0, 0x23, 0x0D, // 204 + 0x0B, 0xE3, 0x1D, 0x0B, // 205 + 0x0C, 0x00, 0x20, 0x0C, // 206 + 0x0C, 0x20, 0x1D, 0x0C, // 207 + 0x0C, 0x3D, 0x1C, 0x0B, // 208 + 0x0C, 0x59, 0x20, 0x0C, // 209 + 0x0C, 0x79, 0x19, 0x09, // 210 + 0x0C, 0x92, 0x1C, 0x0A, // 211 + 0x0C, 0xAE, 0x23, 0x0D, // 212 + 0x0C, 0xD1, 0x20, 0x0B, // 213 + 0x0C, 0xF1, 0x21, 0x0C, // 214 + 0x0D, 0x12, 0x1A, 0x0B, // 215 + 0x0D, 0x2C, 0x26, 0x0F, // 216 + 0x0D, 0x52, 0x2A, 0x0F, // 217 + 0x0D, 0x7C, 0x23, 0x0D, // 218 + 0x0D, 0x9F, 0x23, 0x0E, // 219 + 0x0D, 0xC2, 0x1A, 0x0A, // 220 + 0x0D, 0xDC, 0x1D, 0x0C, // 221 + 0x0D, 0xF9, 0x2C, 0x10, // 222 + 0x0E, 0x25, 0x20, 0x0C, // 223 + 0x0E, 0x45, 0x17, 0x09, // 224 + 0x0E, 0x5C, 0x1A, 0x09, // 225 + 0x0E, 0x76, 0x14, 0x09, // 226 + 0x0E, 0x8A, 0x10, 0x06, // 227 + 0x0E, 0x9A, 0x1B, 0x09, // 228 + 0x0E, 0xB5, 0x17, 0x09, // 229 + 0x0E, 0xCC, 0x20, 0x0B, // 230 + 0x0E, 0xEC, 0x11, 0x07, // 231 + 0x0E, 0xFD, 0x17, 0x09, // 232 + 0x0F, 0x14, 0x17, 0x09, // 233 + 0x0F, 0x2B, 0x14, 0x07, // 234 + 0x0F, 0x3F, 0x17, 0x09, // 235 + 0x0F, 0x56, 0x1D, 0x0B, // 236 + 0x0F, 0x73, 0x17, 0x09, // 237 + 0x0F, 0x8A, 0x17, 0x09, // 238 + 0x0F, 0xA1, 0x17, 0x09, // 239 + 0x0F, 0xB8, 0x17, 0x09, // 240 + 0x0F, 0xCF, 0x14, 0x08, // 241 + 0x0F, 0xE3, 0x13, 0x07, // 242 + 0x0F, 0xF6, 0x13, 0x07, // 243 + 0x10, 0x09, 0x23, 0x0D, // 244 + 0x10, 0x2C, 0x17, 0x09, // 245 + 0x10, 0x43, 0x1A, 0x0A, // 246 + 0x10, 0x5D, 0x14, 0x08, // 247 + 0x10, 0x71, 0x23, 0x0D, // 248 + 0x10, 0x94, 0x23, 0x0D, // 249 + 0x10, 0xB7, 0x1A, 0x0A, // 250 + 0x10, 0xD1, 0x1D, 0x0C, // 251 + 0x10, 0xEE, 0x17, 0x09, // 252 + 0x11, 0x05, 0x14, 0x08, // 253 + 0x11, 0x19, 0x20, 0x0C, // 254 + 0x11, 0x39, 0x17, 0x09, // 255 + // Font Data: + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 + 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, + 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00, + 0x30, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, + 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, + 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00, + 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, + 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, + 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, + 0x20, 0x1E, // 54 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, + 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, + 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x02, // 62 + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, + 0xE0, // 63 + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, + 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, + 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, + 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x5F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, + 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x18, // 87 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, + 0x18, 0x40, 0x00, 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, + 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, + 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xC0, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, + 0x00, 0x11, // 162 + 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, + 0x20, 0x20, // 163 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 + 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, + 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, + 0x00, 0x0C, // 167 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, + 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 168 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, + 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x04, // 175 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, // 177 + 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, + 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0xF8, 0xFF, 0x03, 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00, + 0x00, 0x17, // 184 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, + 0x00, 0x04, // 187 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, + 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02, + 0x00, 0x00, 0x03, 0x00, 0x80, 0x01, // 191 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x66, 0x00, 0x08, 0x3C, // 193 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 194 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x18, // 195 + 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 197 + 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00, + 0x08, 0x40, // 198 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0xE0, 0x1D, // 199 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 200 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00, + 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 201 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00, + 0x08, 0x20, 0x00, 0x08, 0x40, // 202 + 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0xF8, 0x7F, // 203 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 206 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 208 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 209 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 210 + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00, + 0xC0, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 211 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, + 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212 + 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, 0x40, // 213 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 214 + 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0xF8, 0x7F, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, + 0x01, // 217 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x63, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x63, 0x00, 0x00, 0x3E, // 220 + 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x10, 0x61, 0x00, 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00, + 0x30, 0x30, 0x00, 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00, + 0x80, 0x0F, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 224 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00, + 0x18, 0x3F, 0x00, 0x00, 0x0C, // 225 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, 0x3B, // 226 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227 + 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, + 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 228 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 229 + 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, // 230 + 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, + 0xC0, 0x7F, // 232 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00, + 0xC0, 0x7F, // 233 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, 0x40, // 234 + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0xC0, 0x7F, // 235 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, + 0xC0, 0x07, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0xC0, 0x7F, // 237 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 238 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0xC0, 0x7F, // 239 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 240 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 241 + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 242 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 243 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03, + 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244 + 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, + 0x40, 0x40, // 245 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 246 + 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 247 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249 + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x64, 0x00, 0x00, 0x38, // 250 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, + 0x00, 0x38, // 252 + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, 0x1F, // 253 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00, + 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, 0x1F, // 254 + 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00, + 0xC0, 0x7F, // 255 +}; + +// Font generated or edited with the glyphEditor (@mrekin) +const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { + 0x18, // Width: 24 + 0x1C, // Height: 28 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + // Jump Table: + 0xFF, 0xFF, 0x00, 0x07, // 32 + 0x00, 0x00, 0x13, 0x08, // 33 + 0x00, 0x13, 0x1A, 0x09, // 34 + 0x00, 0x2D, 0x33, 0x0D, // 35 + 0x00, 0x60, 0x2F, 0x0D, // 36 + 0x00, 0x8F, 0x4F, 0x15, // 37 + 0x00, 0xDE, 0x3B, 0x10, // 38 + 0x01, 0x19, 0x0A, 0x05, // 39 + 0x01, 0x23, 0x1C, 0x08, // 40 + 0x01, 0x3F, 0x1B, 0x08, // 41 + 0x01, 0x5A, 0x21, 0x09, // 42 + 0x01, 0x7B, 0x32, 0x0E, // 43 + 0x01, 0xAD, 0x10, 0x07, // 44 + 0x01, 0xBD, 0x1B, 0x08, // 45 + 0x01, 0xD8, 0x0F, 0x07, // 46 + 0x01, 0xE7, 0x19, 0x07, // 47 + 0x02, 0x00, 0x2F, 0x0D, // 48 + 0x02, 0x2F, 0x23, 0x0D, // 49 + 0x02, 0x52, 0x2F, 0x0D, // 50 + 0x02, 0x81, 0x2F, 0x0D, // 51 + 0x02, 0xB0, 0x2F, 0x0D, // 52 + 0x02, 0xDF, 0x2F, 0x0D, // 53 + 0x03, 0x0E, 0x2F, 0x0D, // 54 + 0x03, 0x3D, 0x2D, 0x0D, // 55 + 0x03, 0x6A, 0x2F, 0x0D, // 56 + 0x03, 0x99, 0x2F, 0x0D, // 57 + 0x03, 0xC8, 0x0F, 0x07, // 58 + 0x03, 0xD7, 0x10, 0x07, // 59 + 0x03, 0xE7, 0x2F, 0x0E, // 60 + 0x04, 0x16, 0x2F, 0x0E, // 61 + 0x04, 0x45, 0x2E, 0x0E, // 62 + 0x04, 0x73, 0x2E, 0x0D, // 63 + 0x04, 0xA1, 0x5B, 0x18, // 64 + 0x04, 0xFC, 0x3B, 0x0F, // 65 + 0x05, 0x37, 0x3B, 0x10, // 66 + 0x05, 0x72, 0x3F, 0x11, // 67 + 0x05, 0xB1, 0x3F, 0x11, // 68 + 0x05, 0xF0, 0x3B, 0x10, // 69 + 0x06, 0x2B, 0x35, 0x0F, // 70 + 0x06, 0x60, 0x43, 0x13, // 71 + 0x06, 0xA3, 0x3B, 0x11, // 72 + 0x06, 0xDE, 0x0F, 0x06, // 73 + 0x06, 0xED, 0x27, 0x0C, // 74 + 0x07, 0x14, 0x3F, 0x10, // 75 + 0x07, 0x53, 0x2F, 0x0D, // 76 + 0x07, 0x82, 0x43, 0x13, // 77 + 0x07, 0xC5, 0x3B, 0x11, // 78 + 0x08, 0x00, 0x47, 0x13, // 79 + 0x08, 0x47, 0x3A, 0x10, // 80 + 0x08, 0x81, 0x47, 0x13, // 81 + 0x08, 0xC8, 0x3F, 0x11, // 82 + 0x09, 0x07, 0x3B, 0x10, // 83 + 0x09, 0x42, 0x35, 0x0E, // 84 + 0x09, 0x77, 0x3B, 0x11, // 85 + 0x09, 0xB2, 0x39, 0x0F, // 86 + 0x09, 0xEB, 0x59, 0x17, // 87 + 0x0A, 0x44, 0x3B, 0x0F, // 88 + 0x0A, 0x7F, 0x3D, 0x10, // 89 + 0x0A, 0xBC, 0x37, 0x0F, // 90 + 0x0A, 0xF3, 0x14, 0x07, // 91 + 0x0B, 0x07, 0x1B, 0x07, // 92 + 0x0B, 0x22, 0x18, 0x07, // 93 + 0x0B, 0x3A, 0x2A, 0x0C, // 94 + 0x0B, 0x64, 0x34, 0x0D, // 95 + 0x0B, 0x98, 0x11, 0x08, // 96 + 0x0B, 0xA9, 0x2F, 0x0D, // 97 + 0x0B, 0xD8, 0x33, 0x0E, // 98 + 0x0C, 0x0B, 0x2B, 0x0C, // 99 + 0x0C, 0x36, 0x2F, 0x0E, // 100 + 0x0C, 0x65, 0x2F, 0x0D, // 101 + 0x0C, 0x94, 0x1A, 0x07, // 102 + 0x0C, 0xAE, 0x2F, 0x0E, // 103 + 0x0C, 0xDD, 0x2F, 0x0E, // 104 + 0x0D, 0x0C, 0x0F, 0x05, // 105 + 0x0D, 0x1B, 0x10, 0x06, // 106 + 0x0D, 0x2B, 0x2F, 0x0C, // 107 + 0x0D, 0x5A, 0x0F, 0x06, // 108 + 0x0D, 0x69, 0x47, 0x14, // 109 + 0x0D, 0xB0, 0x2F, 0x0E, // 110 + 0x0D, 0xDF, 0x2F, 0x0D, // 111 + 0x0E, 0x0E, 0x33, 0x0E, // 112 + 0x0E, 0x41, 0x30, 0x0E, // 113 + 0x0E, 0x71, 0x1E, 0x08, // 114 + 0x0E, 0x8F, 0x2B, 0x0C, // 115 + 0x0E, 0xBA, 0x1B, 0x07, // 116 + 0x0E, 0xD5, 0x2F, 0x0E, // 117 + 0x0F, 0x04, 0x2A, 0x0B, // 118 + 0x0F, 0x2E, 0x42, 0x11, // 119 + 0x0F, 0x70, 0x2B, 0x0B, // 120 + 0x0F, 0x9B, 0x2A, 0x0C, // 121 + 0x0F, 0xC5, 0x2B, 0x0C, // 122 + 0x0F, 0xF0, 0x1C, 0x08, // 123 + 0x10, 0x0C, 0x10, 0x06, // 124 + 0x10, 0x1C, 0x1B, 0x08, // 125 + 0x10, 0x37, 0x32, 0x0E, // 126 + 0xFF, 0xFF, 0x00, 0x18, // 127 + 0xFF, 0xFF, 0x00, 0x18, // 128 + 0xFF, 0xFF, 0x00, 0x18, // 129 + 0xFF, 0xFF, 0x00, 0x18, // 130 + 0xFF, 0xFF, 0x00, 0x18, // 131 + 0xFF, 0xFF, 0x00, 0x18, // 132 + 0xFF, 0xFF, 0x00, 0x18, // 133 + 0xFF, 0xFF, 0x00, 0x18, // 134 + 0xFF, 0xFF, 0x00, 0x18, // 135 + 0xFF, 0xFF, 0x00, 0x18, // 136 + 0xFF, 0xFF, 0x00, 0x18, // 137 + 0xFF, 0xFF, 0x00, 0x18, // 138 + 0xFF, 0xFF, 0x00, 0x18, // 139 + 0xFF, 0xFF, 0x00, 0x18, // 140 + 0xFF, 0xFF, 0x00, 0x18, // 141 + 0xFF, 0xFF, 0x00, 0x18, // 142 + 0xFF, 0xFF, 0x00, 0x18, // 143 + 0xFF, 0xFF, 0x00, 0x18, // 144 + 0xFF, 0xFF, 0x00, 0x18, // 145 + 0xFF, 0xFF, 0x00, 0x18, // 146 + 0xFF, 0xFF, 0x00, 0x18, // 147 + 0xFF, 0xFF, 0x00, 0x18, // 148 + 0xFF, 0xFF, 0x00, 0x18, // 149 + 0xFF, 0xFF, 0x00, 0x18, // 150 + 0xFF, 0xFF, 0x00, 0x18, // 151 + 0xFF, 0xFF, 0x00, 0x18, // 152 + 0xFF, 0xFF, 0x00, 0x18, // 153 + 0xFF, 0xFF, 0x00, 0x18, // 154 + 0xFF, 0xFF, 0x00, 0x18, // 155 + 0xFF, 0xFF, 0x00, 0x18, // 156 + 0xFF, 0xFF, 0x00, 0x18, // 157 + 0xFF, 0xFF, 0x00, 0x18, // 158 + 0xFF, 0xFF, 0x00, 0x18, // 159 + 0xFF, 0xFF, 0x00, 0x07, // 160 + 0x10, 0x69, 0x14, 0x08, // 161 + 0x10, 0x7D, 0x2B, 0x0D, // 162 + 0x10, 0xA8, 0x2F, 0x0D, // 163 + 0x10, 0xD7, 0x33, 0x0D, // 164 + 0x11, 0x0A, 0x31, 0x0D, // 165 + 0x11, 0x3B, 0x10, 0x06, // 166 + 0x11, 0x4B, 0x2F, 0x0D, // 167 + 0x11, 0x7A, 0x3B, 0x10, // 168 + 0x11, 0xB5, 0x46, 0x12, // 169 + 0x11, 0xFB, 0x1A, 0x09, // 170 + 0x12, 0x15, 0x27, 0x0D, // 171 + 0x12, 0x3C, 0x2F, 0x0E, // 172 + 0x12, 0x6B, 0x1B, 0x08, // 173 + 0x12, 0x86, 0x46, 0x12, // 174 + 0x12, 0xCC, 0x31, 0x0D, // 175 + 0x12, 0xFD, 0x1E, 0x0A, // 176 + 0x13, 0x1B, 0x33, 0x0D, // 177 + 0x13, 0x4E, 0x1A, 0x08, // 178 + 0x13, 0x68, 0x1A, 0x08, // 179 + 0x13, 0x82, 0x19, 0x08, // 180 + 0x13, 0x9B, 0x2F, 0x0E, // 181 + 0x13, 0xCA, 0x31, 0x0D, // 182 + 0x13, 0xFB, 0x12, 0x08, // 183 + 0x14, 0x0D, 0x2F, 0x0D, // 184 + 0x14, 0x3C, 0x16, 0x08, // 185 + 0x14, 0x52, 0x1E, 0x09, // 186 + 0x14, 0x70, 0x2E, 0x0D, // 187 + 0x14, 0x9E, 0x4F, 0x14, // 188 + 0x14, 0xED, 0x4B, 0x14, // 189 + 0x15, 0x38, 0x4B, 0x14, // 190 + 0x15, 0x83, 0x3B, 0x12, // 191 + 0x15, 0xBE, 0x3B, 0x0F, // 192 + 0x15, 0xF9, 0x3B, 0x10, // 193 + 0x16, 0x34, 0x3B, 0x10, // 194 + 0x16, 0x6F, 0x31, 0x0D, // 195 + 0x16, 0xA0, 0x3C, 0x10, // 196 + 0x16, 0xDC, 0x3B, 0x10, // 197 + 0x17, 0x17, 0x57, 0x16, // 198 + 0x17, 0x6E, 0x33, 0x0F, // 199 + 0x17, 0xA1, 0x3B, 0x11, // 200 + 0x17, 0xDC, 0x3B, 0x11, // 201 + 0x18, 0x17, 0x37, 0x0E, // 202 + 0x18, 0x4E, 0x37, 0x10, // 203 + 0x18, 0x85, 0x43, 0x13, // 204 + 0x18, 0xC8, 0x3B, 0x11, // 205 + 0x19, 0x03, 0x47, 0x13, // 206 + 0x19, 0x4A, 0x3B, 0x11, // 207 + 0x19, 0x85, 0x3A, 0x10, // 208 + 0x19, 0xBF, 0x3F, 0x11, // 209 + 0x19, 0xFE, 0x35, 0x0E, // 210 + 0x1A, 0x33, 0x39, 0x0F, // 211 + 0x1A, 0x6C, 0x42, 0x12, // 212 + 0x1A, 0xAE, 0x3B, 0x0F, // 213 + 0x1A, 0xE9, 0x43, 0x12, // 214 + 0x1B, 0x2C, 0x37, 0x10, // 215 + 0x1B, 0x63, 0x4F, 0x16, // 216 + 0x1B, 0xB2, 0x58, 0x17, // 217 + 0x1C, 0x0A, 0x47, 0x13, // 218 + 0x1C, 0x51, 0x4B, 0x15, // 219 + 0x1C, 0x9C, 0x3B, 0x10, // 220 + 0x1C, 0xD7, 0x3F, 0x11, // 221 + 0x1D, 0x16, 0x5B, 0x18, // 222 + 0x1D, 0x71, 0x3B, 0x11, // 223 + 0x1D, 0xAC, 0x2F, 0x0D, // 224 + 0x1D, 0xDB, 0x33, 0x0E, // 225 + 0x1E, 0x0E, 0x2F, 0x0D, // 226 + 0x1E, 0x3D, 0x22, 0x09, // 227 + 0x1E, 0x5F, 0x33, 0x0E, // 228 + 0x1E, 0x92, 0x2F, 0x0D, // 229 + 0x1E, 0xC1, 0x3F, 0x10, // 230 + 0x1F, 0x00, 0x27, 0x0B, // 231 + 0x1F, 0x27, 0x2F, 0x0D, // 232 + 0x1F, 0x56, 0x2F, 0x0D, // 233 + 0x1F, 0x85, 0x27, 0x0B, // 234 + 0x1F, 0xAC, 0x2F, 0x0E, // 235 + 0x1F, 0xDB, 0x3B, 0x11, // 236 + 0x20, 0x16, 0x2F, 0x0D, // 237 + 0x20, 0x45, 0x2F, 0x0D, // 238 + 0x20, 0x74, 0x2B, 0x0D, // 239 + 0x20, 0x9F, 0x33, 0x0E, // 240 + 0x20, 0xD2, 0x2B, 0x0C, // 241 + 0x20, 0xFD, 0x2A, 0x0B, // 242 + 0x21, 0x27, 0x2A, 0x0C, // 243 + 0x21, 0x51, 0x4B, 0x14, // 244 + 0x21, 0x9C, 0x2B, 0x0B, // 245 + 0x21, 0xC7, 0x33, 0x0E, // 246 + 0x21, 0xFA, 0x2B, 0x0D, // 247 + 0x22, 0x25, 0x47, 0x13, // 248 + 0x22, 0x6C, 0x4B, 0x14, // 249 + 0x22, 0xB7, 0x37, 0x0F, // 250 + 0x22, 0xEE, 0x3B, 0x11, // 251 + 0x23, 0x29, 0x2F, 0x0D, // 252 + 0x23, 0x58, 0x2B, 0x0C, // 253 + 0x23, 0x83, 0x43, 0x12, // 254 + 0x23, 0xC6, 0x2B, 0x0D, // 255 + // Font Data: + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 + 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, + 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, + 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, + 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, + 0x1F, 0x00, 0x80, 0x81, 0x07, // 36 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, + 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0, + 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, + 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, + 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, + 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x60, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, + 0x00, 0x33, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, + 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, + 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, + 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x03, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, + 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, + 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, + 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, + 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, + 0x80, 0x3F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, + 0x00, 0x00, 0x60, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7, + 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, + 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, + 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, + 0x03, 0x00, 0x00, 0x03, 0x06, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, + 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, + 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x20, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, + 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, + 0x00, 0x00, 0x00, 0x07, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, + 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, + 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, + 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, + 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, + 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x03, // 67 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x01, // 68 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, + 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, + 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, + 0x00, 0x00, 0xE2, 0x0F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, + 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x20, // 75 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, + 0x00, 0xE0, 0xFF, 0x3F, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, + 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, + 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, + 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F, + 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, + 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x20, // 82 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, + 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70, + 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85 + 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, + 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, + 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, + 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, + 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, + 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, + 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, + 0xFF, 0xFF, 0x07, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x20, // 94 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, + 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x0C, // 99 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, + 0x06, 0x00, 0x00, 0x60, 0x06, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, + 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, + 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, + 0x30, 0x00, 0x00, 0x00, 0x20, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, + 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x0F, // 115 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x06, // 118 + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0x0E, // 119 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x20, // 120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, + 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x06, // 121 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, + 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x30, // 122 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, + 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, + 0x06, 0x3F, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, + 0x06, // 162 + 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, + 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, + 0x38, 0x00, 0x00, 0x00, 0x10, // 163 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, + 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, + 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 + 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, + 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, + 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, + 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, + 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, + 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, + 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0, + 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, + 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, + 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, + 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, + 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, + 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, + 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, + 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, + 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 + 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, + 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, + 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x20, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 + 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, + 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, + 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00, + 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8, + 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, + 0x3F, // 185 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, + 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, // 187 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, + 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, + 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, + 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, + 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, + 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09, + 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, + 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 195 + 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197 + 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, + 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00, + 0x60, 0x00, 0x20, // 198 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C, + 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, 0x00, 0xC7, 0x0F, // 199 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06, + 0x00, 0x0F, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E, + 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00, + 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, + 0x00, 0xE0, 0xFF, 0x3F, // 204 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, + 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, + 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 208 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x03, // 209 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 210 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, + 0x78, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x20, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, + 0x01, 0x06, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01, + 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01, + 0x00, 0x00, 0xF8, // 212 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, + 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0xF0, // 214 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, + 0x00, 0x00, 0xF0, 0x01, // 217 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, + 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, + 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, + 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, + 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, + 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, + 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x03, // 221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03, + 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, + 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0, + 0xB8, 0x0F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, + 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, + 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60, + 0x0C, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78, + 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, 0x10, 0xC0, 0x03, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, + 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78, + 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227 + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00, + 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0xF0, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229 + 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, + 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04, + 0x20, // 230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, + 0x86, 0x20, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, 0x1F, // 231 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80, + 0x00, 0x0F, 0x00, 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, 0x30, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, // 239 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x0C, // 241 + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, + 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, // 242 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, + 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x06, // 243 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF, + 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, + 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x20, // 245 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, // 247 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, + 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, + 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, + 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x07, // 253 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, + 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F, + 0x00, 0x00, 0xE0, 0x07, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00, + 0x84, 0x03, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, // 255 +}; + +#endif // OLED_RU \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.h b/src/graphics/fonts/OLEDDisplayFontsRU.h index 7510dcdfc..0437517dd 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.h +++ b/src/graphics/fonts/OLEDDisplayFontsRU.h @@ -8,4 +8,6 @@ #endif extern const uint8_t ArialMT_Plain_10_RU[] PROGMEM; +extern const uint8_t ArialMT_Plain_16_RU[] PROGMEM; +extern const uint8_t ArialMT_Plain_24_RU[] PROGMEM; #endif \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp index 2a97526ef..8bc56ea94 100644 --- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -1,3 +1,5 @@ +#ifdef OLED_UA + #include "OLEDDisplayFontsUA.h" // Font generated or edited with the glyphEditor @@ -1920,4 +1922,6 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1103 -}; \ No newline at end of file +}; + +#endif // OLED_UA \ No newline at end of file diff --git a/src/graphics/images.h b/src/graphics/images.h index beef3a1b2..c66e4b992 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -27,7 +27,8 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp new file mode 100644 index 000000000..e83588905 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp @@ -0,0 +1,68 @@ +#include "./ZJY122250_0213BAAMFGN.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void ZJY122250_0213BAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from +// the controller IC's OTP memory, when the update procedure begins. +void ZJY122250_0213BAAMFGN::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VCOM + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void ZJY122250_0213BAAMFGN::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } +} + +// Once the refresh operation has been started, +// begin periodically polling the display to check for completion, using the normal Meshtastic threading code +// Only used when refresh is "async" +void ZJY122250_0213BAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h new file mode 100644 index 000000000..82c4ec107 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h @@ -0,0 +1,42 @@ +/* + +E-Ink display driver + - ZJY122250_0213BAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 2.13 inch + - Resolution: 250px x 122px + - Flex connector marking (not a unique identifier): FPC-A002 + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class ZJY122250_0213BAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} + + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index 7e1accafd..e8849b72e 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -7,12 +7,7 @@ using namespace NicheGraphics; // Timing for "maintenance" // Paying off full-refresh debt with unprovoked updates, if the display is not very active - -#ifdef SEEED_WIO_TRACKER_L1 -static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL; -#else static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; -#endif static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 233bbefe0..32882f7ae 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -92,8 +92,11 @@ bool ButtonThread::initButton(const ButtonConfig &config) if (config.shortLong != INPUT_BROKER_NONE) { _shortLong = config.shortLong; } - +#ifdef USE_EINK + userButton.setDebounceMs(0); +#else userButton.setDebounceMs(1); +#endif userButton.setPressMs(_longPressTime); if (screen) { @@ -137,8 +140,7 @@ int32_t ButtonThread::runOnce() } // Progressive lead-up sound system - if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS && - (millis() - buttonPressStartTime) < _longLongPressTime) { + if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { // Start the progressive sequence if not already active if (!leadUpSequenceActive) { @@ -150,13 +152,14 @@ int32_t ButtonThread::runOnce() else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes if (playNextLeadUpNote()) { lastLeadUpNoteTime = millis(); + } else { + leadUpPlayed = true; } } } // Reset when button is released if (!buttonCurrentlyPressed && buttonWasPressed) { - leadUpPlayed = false; leadUpSequenceActive = false; resetLeadUpSequence(); } @@ -253,12 +256,13 @@ int32_t ButtonThread::runOnce() LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && - (millis() - buttonPressStartTime) >= _longLongPressTime) { + (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { evt.inputEvent = _longLongPress; this->notifyObservers(&evt); } // Reset combination tracking waitingForLongPress = false; + leadUpPlayed = false; break; } diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index bbc8da2a7..c6d6557e2 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -92,7 +92,7 @@ class ButtonThread : public Observable, public concurrency:: voidFuncPtr _intRoutine = nullptr; uint16_t _longPressTime = 500; - uint16_t _longLongPressTime = 5000; + uint16_t _longLongPressTime = 3900; int _pinNum = 0; bool _activeLow = true; bool _touchQuirk = false; diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp new file mode 100644 index 000000000..e00c1cc6f --- /dev/null +++ b/src/input/RotaryEncoderImpl.cpp @@ -0,0 +1,76 @@ +#ifdef T_LORA_PAGER + +#include "RotaryEncoderImpl.h" +#include "InputBroker.h" +#include "RotaryEncoder.h" + +#define ORIGIN_NAME "RotaryEncoder" + +RotaryEncoderImpl *rotaryEncoderImpl; + +RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) +{ + rotary = nullptr; +} + +bool RotaryEncoderImpl::init() +{ + if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || + moduleConfig.canned_message.inputbroker_pin_b == 0) { + // Input device is disabled. + disable(); + return false; + } + + eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + rotary->resetButton(); + + inputBroker->registerSource(this); + + LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, + moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, + eventPressed); + return true; +} + +int32_t RotaryEncoderImpl::runOnce() +{ + InputEvent e; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->originName; + + static uint32_t lastPressed = millis(); + if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { + if (lastPressed + 200 < millis()) { + LOG_DEBUG("Rotary event Press"); + lastPressed = millis(); + e.inputEvent = this->eventPressed; + } + } else { + switch (rotary->process()) { + case RotaryEncoder::DIRECTION_CW: + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->eventCw; + break; + case RotaryEncoder::DIRECTION_CCW: + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->eventCcw; + break; + default: + break; + } + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + + return 10; +} + +#endif \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h new file mode 100644 index 000000000..ae2a7c6fd --- /dev/null +++ b/src/input/RotaryEncoderImpl.h @@ -0,0 +1,28 @@ +#pragma once + +// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library) + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" + +class RotaryEncoder; + +class RotaryEncoderImpl : public Observable, public concurrency::OSThread +{ + public: + RotaryEncoderImpl(); + bool init(void); + + protected: + virtual int32_t runOnce() override; + + input_broker_event eventCw = INPUT_BROKER_NONE; + input_broker_event eventCcw = INPUT_BROKER_NONE; + input_broker_event eventPressed = INPUT_BROKER_NONE; + + RotaryEncoder *rotary; + const char *originName; +}; + +extern RotaryEncoderImpl *rotaryEncoderImpl; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 0557bc180..88b07a389 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -18,14 +18,23 @@ void RotaryEncoderInterruptBase::init( this->_eventCcw = eventCcw; this->_eventPressed = eventPressed; - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinA, INPUT_PULLUP); - pinMode(this->_pinB, INPUT_PULLUP); + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - // attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinA, onIntA, CHANGE); - attachInterrupt(this->_pinB, onIntB, CHANGE); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinA, INPUT_PULLUP); + attachInterrupt(this->_pinA, onIntA, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinB, INPUT_PULLUP); + attachInterrupt(this->_pinB, onIntB, CHANGE); + } this->rotaryLevelA = digitalRead(this->_pinA); this->rotaryLevelB = digitalRead(this->_pinB); diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp new file mode 100644 index 000000000..b3f62a36c --- /dev/null +++ b/src/input/TLoraPagerKeyboard.cpp @@ -0,0 +1,230 @@ +#if defined(T_LORA_PAGER) + +#include "TLoraPagerKeyboard.h" +#include "main.h" + +#ifndef LEDC_BACKLIGHT_CHANNEL +#define LEDC_BACKLIGHT_CHANNEL 4 +#endif + +#ifndef LEDC_BACKLIGHT_BIT_WIDTH +#define LEDC_BACKLIGHT_BIT_WIDTH 8 +#endif + +#ifndef LEDC_BACKLIGHT_FREQ +#define LEDC_BACKLIGHT_FREQ 1000 // Hz +#endif + +#define _TCA8418_COLS 10 +#define _TCA8418_ROWS 4 +#define _TCA8418_NUM_KEYS 31 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1 +constexpr uint8_t modifierRightShift = 0b0001; +constexpr uint8_t modifierSymKey = 21 - 1; +constexpr uint8_t modifierSym = 0b0010; + +// Num chars per key, Modulus for rotating through characters +static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; + +static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, + {'w', 'W', '2'}, + {'e', 'E', '3'}, + {'r', 'R', '4'}, + {'t', 'T', '5'}, + {'y', 'Y', '6'}, + {'u', 'U', '7'}, + {'i', 'I', '8'}, + {'o', 'O', '9'}, + {'p', 'P', '0'}, + {'a', 'A', '*'}, + {'s', 'S', '/'}, + {'d', 'D', '+'}, + {'f', 'F', '-'}, + {'g', 'G', '='}, + {'h', 'H', ':'}, + {'j', 'J', '\''}, + {'k', 'K', '"'}, + {'l', 'L', '@'}, + {Key::SELECT, 0x00, Key::TAB}, + {0x00, 0x00, 0x00}, + {'z', 'Z', '_'}, + {'x', 'X', '$'}, + {'c', 'C', ';'}, + {'v', 'V', '?'}, + {'b', 'B', '!'}, + {'n', 'N', ','}, + {'m', 'M', '.'}, + {0x00, 0x00, 0x00}, + {Key::BSP, 0x00, Key::ESC}, + {' ', 0x00, Key::BL_TOGGLE}}; + +TLoraPagerKeyboard::TLoraPagerKeyboard() + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); +#else + ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); +#endif + reset(); +} + +void TLoraPagerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + digitalWrite(KB_BL_PIN, LOW); + setBacklight(false); +} + +// handle multi-key presses (shift and alt) +void TLoraPagerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } + } +} + +void TLoraPagerKeyboard::setBacklight(bool on) +{ + toggleBacklight(!on); +} + +void TLoraPagerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + hapticFeedback(); + } + + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } + + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; +} + +void TLoraPagerKeyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } + + queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void TLoraPagerKeyboard::hapticFeedback() +{ + drv.setWaveform(0, 14); // strong buzz 100% + drv.setWaveform(1, 0); // end waveform + drv.go(); +} + +// toggle brightness of the backlight in three steps +void TLoraPagerKeyboard::toggleBacklight(bool off) +{ + static uint32_t brightness = 0; + if (off) { + brightness = 0; + } else { + if (brightness == 0) { + brightness = 40; + } else if (brightness == 40) { + brightness = 127; + } else if (brightness >= 127) { + brightness = 0; + } + } + LOG_DEBUG("Toggle backlight: %d", brightness); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcWrite(KB_BL_PIN, brightness); +#else + ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness); +#endif +} + +void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } +} + +bool TLoraPagerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierSymKey); +} + +#endif \ No newline at end of file diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index d31b05978..4dabbac64 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -4,9 +4,26 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase { public: TLoraPagerKeyboard(); - void setBacklight(bool on) override{}; + void reset(void); + void trigger(void) override; + void setBacklight(bool on) override; + virtual ~TLoraPagerKeyboard() {} protected: - void pressed(uint8_t key) override{}; - void released(void) override{}; + void pressed(uint8_t key) override; + void released(void) override; + void hapticFeedback(void); + + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(bool off = false); + + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index c66eb13d0..26b281aaf 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -15,14 +15,23 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventPressed = eventPressed; + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinDown, INPUT_PULLUP); - pinMode(this->_pinUp, INPUT_PULLUP); - - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinDown, onIntDown, RISING); - attachInterrupt(this->_pinUp, onIntUp, RISING); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinDown != 0) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, RISING); + } + if (!isRAK || this->_pinUp != 0) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, RISING); + } LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index fcbdd0a3f..cb03eb4ff 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -12,8 +12,12 @@ void CardKbI2cImpl::init() #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) if (cardkb_found.address == 0x00) { LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, XPOWERS_AXP192_AXP2101_ADDRESS}; + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; +#if defined(T_LORA_PAGER) + uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); +#else uint8_t i2caddr_asize = 5; +#endif auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 diff --git a/src/main.cpp b/src/main.cpp index 9e46021c9..401ea7592 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,8 +135,9 @@ AccelerometerThread *accelerometerThread = nullptr; AudioThread *audioThread = nullptr; #endif -#ifdef USE_PCA9557 -PCA9557 IOEXP; +#ifdef USE_XL9555 +#include "ExtensionIOXL9555.hpp" +ExtensionIOXL9555 io; #endif #if HAS_TFT @@ -201,7 +202,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) Adafruit_DRV2605 drv; #endif @@ -304,7 +305,6 @@ void setup() Wire.begin(48, 47); io.pinMode(PCA_PIN_EINK_EN, OUTPUT); io.pinMode(PCA_PIN_POWER_EN, OUTPUT); - io.digitalWrite(PCA_PIN_EINK_EN, HIGH); io.digitalWrite(PCA_PIN_POWER_EN, HIGH); // io.pinMode(C2_PIN, OUTPUT); #endif @@ -326,8 +326,12 @@ void setup() #ifdef BLE_LED pinMode(BLE_LED, OUTPUT); +#ifdef BLE_LED_INVERTED + digitalWrite(BLE_LED, HIGH); +#else digitalWrite(BLE_LED, LOW); #endif +#endif #if defined(T_DECK) // GPIO10 manages all peripheral power supplies @@ -356,6 +360,30 @@ void setup() digitalWrite(SDCARD_CS, HIGH); pinMode(PIN_EINK_CS, OUTPUT); digitalWrite(PIN_EINK_CS, HIGH); +#elif defined(T_LORA_PAGER) + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); #endif concurrency::hasBeenSetup = true; @@ -391,7 +419,7 @@ void setup() struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + perhapsSetRTC(RTCQualityDevice, &tv); #endif powerMonInit(); @@ -401,6 +429,16 @@ void setup() initDeepSleep(); +#if defined(MODEM_POWER_EN) + pinMode(MODEM_POWER_EN, OUTPUT); + digitalWrite(MODEM_POWER_EN, LOW); +#endif + +#if defined(MODEM_PWRKEY) + pinMode(MODEM_PWRKEY, OUTPUT); + digitalWrite(MODEM_PWRKEY, LOW); +#endif + #if defined(LORA_TCXO_GPIO) pinMode(LORA_TCXO_GPIO, OUTPUT); digitalWrite(LORA_TCXO_GPIO, HIGH); @@ -703,6 +741,7 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::TSL2561, meshtastic_TelemetrySensorType_TSL2561); i2cScanner.reset(); #endif @@ -792,7 +831,7 @@ void setup() #endif #endif -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) drv.begin(); drv.selectLibrary(1); // I2C trigger by sending 'go' command @@ -838,7 +877,7 @@ void setup() if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && @@ -1101,7 +1140,7 @@ void setup() // Don't call screen setup until after nodedb is setup (because we need // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) @@ -1487,7 +1526,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif -#if NO_EXT_GPIO +#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts @@ -1559,7 +1598,13 @@ void loop() #endif service->loop(); - +#if defined(LGFX_SDL) + if (screen) { + auto dispdev = screen->getDisplayDevice(); + if (dispdev) + static_cast(dispdev)->sdlLoop(); + } +#endif long delayMsec = mainController.runOrDelay(); // We want to sleep as long as possible here - because it saves power diff --git a/src/main.h b/src/main.h index 3568daad2..ef1f241ef 100644 --- a/src/main.h +++ b/src/main.h @@ -41,7 +41,7 @@ extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #include extern Adafruit_DRV2605 drv; #endif diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 70e4127d8..4ef41ddfb 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -368,7 +368,7 @@ const char *Channels::getName(size_t chIndex) // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case // the app effed up and forgot to set channelSettings.name if (config.lora.use_preset) { - channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); } else { channelName = "Custom"; } @@ -382,7 +382,8 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex) const auto &ch = getByIndex(chIndex); if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { const char *name = getName(chIndex); - const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); // Check if the name is the default derived from the modem preset if (strcmp(name, presetName) == 0) return true; diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 142ada806..dbd458b61 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -47,8 +47,10 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && + p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! + // But only LoRa packets should be able to trigger this. if (Router::cancelSending(p->from, p->id)) txRelayCanceled++; } diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index a20db808e..a0d992c42 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -6,6 +6,10 @@ #include "mesh/NodeDB.h" #ifdef LR11X0_DIO_AS_RF_SWITCH #include "rfswitch.h" +#elif ARCH_PORTDUINO +#include "PortduinoGlue.h" +#define rfswitch_dio_pins portduino_config.rfswitch_dio_pins +#define rfswitch_table portduino_config.rfswitch_table #else static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { @@ -14,10 +18,6 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { }; #endif -#ifdef ARCH_PORTDUINO -#include "PortduinoGlue.h" -#endif - // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and LR11x0 power config forgotten) #if ARCH_PORTDUINO @@ -117,17 +117,14 @@ template bool LR11x0Interface::init() #ifdef LR11X0_DIO_AS_RF_SWITCH bool dioAsRfSwitch = true; #elif defined(ARCH_PORTDUINO) - bool dioAsRfSwitch = false; - if (settingsMap[dio2_as_rf_switch]) { - dioAsRfSwitch = true; - } + bool dioAsRfSwitch = portduino_config.has_rfswitch_table; #else bool dioAsRfSwitch = false; #endif if (dioAsRfSwitch) { lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); - LOG_DEBUG("Set DIO RF switch", res); + LOG_DEBUG("Set DIO RF switch"); } if (res == RADIOLIB_ERR_NONE) { diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index c5748a560..22fcec663 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -100,6 +100,7 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets auto ourNodeNum = nodeDB->getNodeNum(); bool toUs = isBroadcast(mp.to) || isToUs(&mp); + bool fromUs = mp.from == ourNodeNum; for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 860250f75..7ceca2195 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -34,8 +34,11 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) 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 (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + rxDupe++; + stopRetransmission(p->from, p->id); + } // If it was a fallback to flooding, try to relay again if (wasFallback) { @@ -71,10 +74,11 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast 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 + // Either relayer of ACK was also a relayer of the packet, or we were the *only* 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)) { + (p->hop_start != 0 && p->hop_start == p->hop_limit && + wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) { 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; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index b54cdae86..c8eba1b2e 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -225,7 +225,11 @@ NodeDB::NodeDB() memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); myNodeInfo.device_id.size = 16; // Uncomment below to print the device id - +#elif ARCH_PORTDUINO + if (portduino_config.has_device_id) { + memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); + myNodeInfo.device_id.size = 16; + } #else // FIXME - implement for other platforms #endif @@ -659,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); @@ -826,6 +830,15 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; +#endif +#ifdef T_LORA_PAGER + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; + moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; + moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; + moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); + moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); + moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT @@ -1631,24 +1644,33 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); // Alert the user if a remote node is advertising public key that matches our own - if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0 && !duplicateWarned) { - duplicateWarned = true; - char warning[] = "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " - "to regenerate your public keys."; - LOG_WARN(warning, p.long_name); - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, warning, p.long_name); - service->sendClientNotification(cn); + if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) { + if (!duplicateWarned) { + duplicateWarned = true; + char warning[] = + "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " + "to regenerate your public keys."; + LOG_WARN(warning, p.long_name); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, warning, p.long_name); + service->sendClientNotification(cn); + } + return false; } } - if (info->user.public_key.size > 0) { // if we have a key for this user already, don't overwrite with a new one + if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one + // if the key doesn't match, don't update nodeDB at all. + if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { + LOG_WARN("Public Key mismatch, dropping NodeInfo"); + return false; + } LOG_INFO("Public Key set for node, not updating!"); // we copy the key into the incoming packet, to prevent overwrite p.public_key.size = 32; memcpy(p.public_key.bytes, info->user.public_key.bytes, 32); - } else if (p.public_key.size > 0) { + } else if (p.public_key.size == 32) { LOG_INFO("Update Node Pubkey!"); } #endif @@ -1689,10 +1711,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { - // if (mp.from == getNodeNum()) { - // LOG_DEBUG("Ignore update from self"); - // return; - // } + if (mp.from == getNodeNum()) { + LOG_DEBUG("Ignore update from self"); + return; + } if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); @@ -1867,7 +1889,7 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub uint8_t keyHash[32] = {0}; memcpy(keyHash, keyToTest.bytes, keyToTest.size); crypto->hash(keyHash, 32); - for (int i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { + for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) { return true; } diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 3902c1057..735386d79 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -294,7 +294,7 @@ void PacketHistory::insert(const PacketRecord &r) /* 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) +bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) { if (!initOk()) { LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); @@ -322,27 +322,42 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif - return wasRelayer(relayer, *found); + return wasRelayer(relayer, *found, wasSole); } /* 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, const PacketRecord &r) +bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) { - for (uint8_t i = 0; i < NUM_RELAYERS; i++) { + bool found = false; + bool other_present = false; + + for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { if (r.relayed_by[i] == relayer) { -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id, - r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); -#endif - return true; + found = true; + } else if (r.relayed_by[i] != 0) { + other_present = true; } } + + if (wasSole) { + *wasSole = (found && !other_present); + } + #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); #endif - return false; + + return found; +} + +// Check if a certain node was the *only* relayer of a packet in the history given an ID and sender +bool PacketHistory::wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +{ + bool wasSole = false; + wasRelayer(relayer, id, sender, &wasSole); + return wasSole; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 9f14a4cf0..4b53c8f6a 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -34,8 +34,9 @@ class PacketHistory void insert(const PacketRecord &r); // Insert or replace a packet record in the history /* Check if a certain node was a relayer of a packet in the history given iterator + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const PacketRecord &r); + bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); PacketHistory(const PacketHistory &); // non construction-copyable PacketHistory &operator=(const PacketHistory &); // non copyable @@ -54,8 +55,12 @@ class PacketHistory bool *weWereNextHop = nullptr); /* Check if a certain node was a relayer of a packet in the history given an ID and sender + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); + bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); + + // Check if a certain node was the *only* relayer of a packet in the history given an ID and sender + bool wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); // 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); diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 305689fff..a3a8a2087 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -192,12 +192,6 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) size_t PhoneAPI::getFromRadio(uint8_t *buf) { - if (!available()) { - return 0; - } - // In case we send a FromRadio packet - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - // Respond to heartbeat by sending queue status if (heartbeatReceived) { memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); @@ -209,6 +203,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) return numbytes; } + if (!available()) { + return 0; + } + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + // Advance states as needed switch (state) { case STATE_SEND_NOTHING: diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 7590ac34d..a5c293868 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -170,11 +170,10 @@ const RegionInfo regions[] = { */ RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, true), - /* Nepal - 865β€―MHz to 868β€―MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use, specifically in non-cellular mode. - https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf + 865β€―MHz to 868β€―MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use, + specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf */ RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false), @@ -336,8 +335,9 @@ uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) void printPacket(const char *prefix, const meshtastic_MeshPacket *p) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - std::string out = DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, - p->from, p->to, p->want_ack, p->hop_limit, p->channel); + std::string out = + DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, + p->from, p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel); if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { auto &s = p->decoded; @@ -586,7 +586,8 @@ void RadioInterface::applyModemConfig() // Check if we use the default frequency slot RadioInterface::uses_default_frequency_slot = - channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels; + channel_num == + hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; // Old frequency selection formula // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); @@ -666,8 +667,10 @@ void RadioInterface::limitPower(int8_t loraMaxPower) void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) { - if (router) + if (router) { + p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; router->enqueueReceivedMessage(p); + } } /*** diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 6e5c6231b..e9ceeaef1 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -58,7 +58,10 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) // marked as wantAck sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); - stopRetransmission(key); + // Only stop retransmissions if the rebroadcast came via LoRa + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + stopRetransmission(key); + } } else { LOG_DEBUG("Didn't find pending packet"); } diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 48205cc0f..c7e32c4a1 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -523,12 +523,15 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // is not in the local nodedb // First, only PKC encrypt packets we are originating if (isFromUs(p) && - // Don't use PKC with simulator - radioType != SIM_RADIO && +#if ARCH_PORTDUINO + // Sim radio via the cli flag skips PKC + !portduino_config.force_simradio && +#endif // Don't use PKC with Ham mode !owner.is_licensed && - // Don't use PKC if it's not explicitly requested and a non-primary channel is requested - !(p->pki_encrypted != true && p->channel > 0) && + // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested + !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || + strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && // Check for a known public key for the destination @@ -559,7 +562,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -575,7 +578,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -652,7 +655,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) } // call modules here - if (!skipHandle) { + // If this could be a spoofed packet, don't let the modules see it. + if (!skipHandle && p->from != nodeDB->getNodeNum()) { MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT @@ -666,6 +670,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) !isFromUs(p) && mqtt) mqtt->onSend(*p_encrypted, *p, p->channel); #endif + } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { + MeshModule::callModules(*p, src); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index d7bc37466..f6e4b3512 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -1,13 +1,13 @@ -#include "STM32WLE5JCInterface.h" #include "configuration.h" + +#ifdef ARCH_STM32WL +#include "STM32WLE5JCInterface.h" #include "error.h" #ifndef STM32WLx_MAX_POWER #define STM32WLx_MAX_POWER 22 #endif -#ifdef ARCH_STM32WL - STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) : SX126xInterface(hal, cs, irq, rst, busy) diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h index 0c8140290..ee935375e 100644 --- a/src/mesh/STM32WLE5JCInterface.h +++ b/src/mesh/STM32WLE5JCInterface.h @@ -1,8 +1,8 @@ #pragma once -#include "SX126xInterface.h" - #ifdef ARCH_STM32WL +#include "SX126xInterface.h" +#include "rfswitch.h" /** * Our adapter for STM32WLE5JC radios @@ -16,13 +16,4 @@ class STM32WLE5JCInterface : public SX126xInterface virtual bool init() override; }; -/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/ - * Wio-E5 module ONLY transmits through RFO_HP - * Receive: PA4=1, PA5=0 - * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */ -static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; - -static const Module::RfSwitchMode_t rfswitch_table[4] = { - {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; - #endif // ARCH_STM32WL \ No newline at end of file diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 4a42e5197..20026767e 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -16,6 +16,95 @@ int32_t StreamAPI::runOncePart() return result; } +int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) +{ + auto result = readStream(buf, bufLen); + writeStream(); + checkConnectionTimeout(); + return result; +} + +/** + * Read any rx chars from the link and call handleRecStream + */ +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } +} + +/** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ +void StreamAPI::writeStream() +{ + if (canWrite) { + uint32_t len; + do { + // Send every packet we can + len = getFromRadio(txBuf + HEADER_LEN); + emitTxBuffer(len); + } while (len); + } +} + +int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + while (bufLen > index) { // Currently we never want to block + int cInt = buf[index++]; + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino + + uint8_t c = (uint8_t)cInt; + + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; + + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) + + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + + // console->printf("len %d\n", len); + + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } + } + return 0; +} + /** * Read any rx chars from the link and call handleToRadio */ @@ -76,21 +165,6 @@ int32_t StreamAPI::readStream() } } -/** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ -void StreamAPI::writeStream() -{ - if (canWrite) { - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - emitTxBuffer(len); - } while (len); - } -} - /** * Send the current txBuffer over our stream */ diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 6e0364bc1..547dd0175 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -50,12 +50,15 @@ class StreamAPI : public PhoneAPI * phone. */ virtual int32_t runOncePart(); + virtual int32_t runOncePart(char *buf,uint16_t bufLen); private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); + int32_t readStream(char *buf,uint16_t bufLen); + int32_t handleRecStream(char *buf,uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream diff --git a/src/mesh/api/PacketAPI.cpp b/src/mesh/api/PacketAPI.cpp index 1d6df855f..ab380d696 100644 --- a/src/mesh/api/PacketAPI.cpp +++ b/src/mesh/api/PacketAPI.cpp @@ -59,6 +59,7 @@ bool PacketAPI::receivePacket(void) switch (mr->which_payload_variant) { case meshtastic_ToRadio_packet_tag: { meshtastic_MeshPacket *mp = &mr->packet; + mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API; printPacket("PACKET FROM QUEUE", mp); service->handleToRadio(*mp); break; diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 8a68197f0..59e55db3f 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -64,7 +64,12 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { in areas not already covered by other routers, or to bridge around problematic terrain, but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11 + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, + /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + Technical Details: Used for stronger attic/roof nodes to distribute messages more widely + from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes + where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12 } meshtastic_Config_DeviceConfig_Role; /* Defines the device's behavior for how messages are rebroadcast */ @@ -207,10 +212,10 @@ typedef enum _meshtastic_Config_DisplayConfig_OledType { meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306 = 1, /* Default / Autodetect */ meshtastic_Config_DisplayConfig_OledType_OLED_SH1106 = 2, - /* Can not be auto detected but set by proto. Used for 128x128 screens */ - meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3, /* Can not be auto detected but set by proto. Used for 128x64 screens */ - meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64 = 4 + meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3, + /* Can not be auto detected but set by proto. Used for 128x128 screens */ + meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4 } meshtastic_Config_DisplayConfig_OledType; typedef enum _meshtastic_Config_DisplayConfig_DisplayMode { @@ -646,8 +651,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT -#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE -#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1)) +#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE +#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1)) #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY @@ -682,8 +687,8 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_DisplayUnits_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayUnits)(meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL+1)) #define _meshtastic_Config_DisplayConfig_OledType_MIN meshtastic_Config_DisplayConfig_OledType_OLED_AUTO -#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64 -#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64+1)) +#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 +#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128+1)) #define _meshtastic_Config_DisplayConfig_DisplayMode_MIN meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT #define _meshtastic_Config_DisplayConfig_DisplayMode_MAX meshtastic_Config_DisplayConfig_DisplayMode_COLOR diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index f47091384..9b6330596 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2271 +#define meshtastic_BackupPreferences_size 2273 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index ca8dcd5fb..da224fb94 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 747 -#define meshtastic_LocalModuleConfig_size 669 +#define meshtastic_LocalModuleConfig_size 671 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 1d1ff47e0..2a4e77870 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -270,6 +270,10 @@ typedef enum _meshtastic_HardwareModel { /* MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices. https://heltec.org/project/meshsolar/ */ meshtastic_HardwareModel_HELTEC_MESH_SOLAR = 108, + /* Lilygo T-Echo Lite */ + meshtastic_HardwareModel_T_ECHO_LITE = 109, + /* New Heltec LoRA32 with ESP32-S3 CPU */ + meshtastic_HardwareModel_HELTEC_V4 = 110, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index b27f5f515..16c4c230c 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -317,6 +317,9 @@ typedef struct _meshtastic_ModuleConfig_RangeTestConfig { /* Bool value indicating that this node should save a RangeTest.csv file. ESP32 Only */ bool save; + /* Bool indicating that the node should cleanup / destroy it's RangeTest.csv file. + ESP32 Only */ + bool clear_on_reboot; } meshtastic_ModuleConfig_RangeTestConfig; /* Configuration for both device and environment metrics */ @@ -519,7 +522,7 @@ extern "C" { #define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} -#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} @@ -535,7 +538,7 @@ extern "C" { #define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} -#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} @@ -610,6 +613,7 @@ extern "C" { #define meshtastic_ModuleConfig_RangeTestConfig_enabled_tag 1 #define meshtastic_ModuleConfig_RangeTestConfig_sender_tag 2 #define meshtastic_ModuleConfig_RangeTestConfig_save_tag 3 +#define meshtastic_ModuleConfig_RangeTestConfig_clear_on_reboot_tag 4 #define meshtastic_ModuleConfig_TelemetryConfig_device_update_interval_tag 1 #define meshtastic_ModuleConfig_TelemetryConfig_environment_update_interval_tag 2 #define meshtastic_ModuleConfig_TelemetryConfig_environment_measurement_enabled_tag 3 @@ -803,7 +807,8 @@ X(a, STATIC, SINGULAR, BOOL, is_server, 6) #define meshtastic_ModuleConfig_RangeTestConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ X(a, STATIC, SINGULAR, UINT32, sender, 2) \ -X(a, STATIC, SINGULAR, BOOL, save, 3) +X(a, STATIC, SINGULAR, BOOL, save, 3) \ +X(a, STATIC, SINGULAR, BOOL, clear_on_reboot, 4) #define meshtastic_ModuleConfig_RangeTestConfig_CALLBACK NULL #define meshtastic_ModuleConfig_RangeTestConfig_DEFAULT NULL @@ -901,7 +906,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_MapReportSettings_size 14 #define meshtastic_ModuleConfig_NeighborInfoConfig_size 10 #define meshtastic_ModuleConfig_PaxcounterConfig_size 30 -#define meshtastic_ModuleConfig_RangeTestConfig_size 10 +#define meshtastic_ModuleConfig_RangeTestConfig_size 12 #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index f758995c2..9af095e78 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -99,7 +99,9 @@ typedef enum _meshtastic_TelemetrySensorType { /* Sensirion SFA30 Formaldehyde sensor */ meshtastic_TelemetrySensorType_SFA30 = 42, /* SEN5X PM SENSORS */ - meshtastic_TelemetrySensorType_SEN5X = 43 + meshtastic_TelemetrySensorType_SEN5X = 43, + /* TSL2561 light sensor */ + meshtastic_TelemetrySensorType_TSL2561 = 44 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -434,8 +436,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SEN5X -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SEN5X+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_TSL2561 +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_TSL2561+1)) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 42ebb8417..74953d8fc 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -342,6 +342,11 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) res->print(value->Stringify().c_str()); delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -610,33 +615,38 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) res->println("
");
     }
 
+    // Helper lambda to create JSON array and clean up memory properly
+    auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
+        JSONArray tempArray;
+        for (int i = 0; i < count; i++) {
+            tempArray.push_back(new JSONValue((int)logArray[i]));
+        }
+        JSONValue *result = new JSONValue(tempArray);
+        // Clean up original array to prevent memory leak
+        for (auto *val : tempArray) {
+            delete val;
+        }
+        return result;
+    };
+
     // data->airtime->tx_log
-    JSONArray txLogValues;
     uint32_t *logArray;
     logArray = airTime->airtimeReport(TX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        txLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_log
-    JSONArray rxLogValues;
     logArray = airTime->airtimeReport(RX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_all_log
-    JSONArray rxAllLogValues;
     logArray = airTime->airtimeReport(RX_ALL_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxAllLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime
     JSONObject jsonObjAirtime;
-    jsonObjAirtime["tx_log"] = new JSONValue(txLogValues);
-    jsonObjAirtime["rx_log"] = new JSONValue(rxLogValues);
-    jsonObjAirtime["rx_all_log"] = new JSONValue(rxAllLogValues);
+    jsonObjAirtime["tx_log"] = txLogJsonValue;
+    jsonObjAirtime["rx_log"] = rxLogJsonValue;
+    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
     jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
     jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
     jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
@@ -765,6 +775,11 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the nodesArray to prevent memory leak
+    for (auto *val : nodesArray) {
+        delete val;
+    }
 }
 
 /*
@@ -955,5 +970,10 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the networkObjs to prevent memory leak
+    for (auto *val : networkObjs) {
+        delete val;
+    }
 }
 #endif
\ No newline at end of file
diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h
index d4e0eaa8c..9650668a8 100644
--- a/src/mesh/udp/UdpMulticastHandler.h
+++ b/src/mesh/udp/UdpMulticastHandler.h
@@ -50,6 +50,7 @@ class UdpMulticastHandler final
         LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength);
 #endif
         meshtastic_MeshPacket mp;
+        mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
         LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength);
         bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp);
         if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
@@ -78,6 +79,9 @@ class UdpMulticastHandler final
             return false;
         }
 #endif
+        if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) {
+            LOG_ERROR("Attempt to send UDP sourced packet over UDP");
+        }
         LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id);
         uint8_t buffer[meshtastic_MeshPacket_size];
         size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp);
diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index 4014e1c36..407003f7e 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -505,7 +505,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
     if (mp.decoded.want_response && !myReply) {
         myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
     }
-
+    if (mp.pki_encrypted && myReply) {
+        myReply->pki_encrypted = true;
+    }
     return handled;
 }
 
@@ -718,6 +720,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
             requiresReboot = false;
         }
 
+#if defined(ARCH_PORTDUINO)
+        // If running on portduino and using SimRadio, do not require reboot
+        if (SimRadio::instance) {
+            requiresReboot = false;
+        }
+#endif
+
 #ifdef RF95_FAN_EN
         // Turn PA off if disabled by config
         if (c.payload_variant.lora.pa_fan_disabled) {
@@ -934,6 +943,9 @@ void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req)
         res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag;
         setPassKey(&res);
         myReply = allocDataProtobuf(res);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1005,6 +1017,9 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
         res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag;
         setPassKey(&res);
         myReply = allocDataProtobuf(res);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1092,6 +1107,9 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
         res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag;
         setPassKey(&res);
         myReply = allocDataProtobuf(res);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1116,6 +1134,9 @@ void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &r
     }
     setPassKey(&r);
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
@@ -1125,6 +1146,9 @@ void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
     r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag;
     setPassKey(&r);
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req)
@@ -1193,6 +1217,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
     r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag;
     setPassKey(&r);
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex)
@@ -1204,6 +1231,9 @@ void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t ch
         r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag;
         setPassKey(&r);
         myReply = allocDataProtobuf(r);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1213,6 +1243,9 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req)
     r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag;
     r.get_ui_config_response = uiconfig;
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::reboot(int32_t seconds)
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index b6cb1b0e3..e9165e57c 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -78,16 +78,15 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan
     lastDestSet = true;
 
     // Rest of function unchanged...
-    // Always select the first real canned message on activation
-    int firstRealMsgIdx = 0;
+    // Upon activation, highlight "[Select Destination]"
+    int selectDestination = 0;
     for (int i = 0; i < messagesCount; ++i) {
-        if (strcmp(messages[i], "[Select Destination]") != 0 && strcmp(messages[i], "[Exit]") != 0 &&
-            strcmp(messages[i], "[---- Free Text ----]") != 0) {
-            firstRealMsgIdx = i;
+        if (strcmp(messages[i], "[Select Destination]") == 0) {
+            selectDestination = i;
             break;
         }
     }
-    currentMessageIndex = firstRealMsgIdx;
+    currentMessageIndex = selectDestination;
 
     // This triggers the canned message list
     runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
@@ -633,10 +632,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
         // Normal canned message selection
         if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
         } else {
+#if CANNED_MESSAGE_ADD_CONFIRMATION
             // Show confirmation dialog before sending canned message
             NodeNum destNode = dest;
             ChannelIndex chan = channel;
-#if CANNED_MESSAGE_ADD_CONFIRMATION
             graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() {
                 this->sendText(destNode, chan, current, false);
                 payload = runState;
@@ -992,24 +991,22 @@ int32_t CannedMessageModule::runOnce()
                 this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
             }
         }
-        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
         this->currentMessageIndex = -1;
         this->freetext = "";
         this->cursor = 0;
         this->notifyObservers(&e);
         return 2000;
     }
-    // Always highlight the first real canned message when entering the message list
+    // Highlight [Select Destination] initially when entering the message list
     else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) {
-        int firstRealMsgIdx = 0;
+        int selectDestination = 0;
         for (int i = 0; i < this->messagesCount; ++i) {
-            if (strcmp(this->messages[i], "[Select Destination]") != 0 && strcmp(this->messages[i], "[Exit]") != 0 &&
-                strcmp(this->messages[i], "[---- Free Text ----]") != 0) {
-                firstRealMsgIdx = i;
+            if (strcmp(this->messages[i], "[Select Destination]") == 0) {
+                selectDestination = i;
                 break;
             }
         }
-        this->currentMessageIndex = firstRealMsgIdx;
+        this->currentMessageIndex = selectDestination;
         e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
         this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
     } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) {
diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp
index 1f871f87e..2f2934984 100644
--- a/src/modules/ExternalNotificationModule.cpp
+++ b/src/modules/ExternalNotificationModule.cpp
@@ -364,9 +364,10 @@ ExternalNotificationModule::ExternalNotificationModule()
     // moduleConfig.external_notification.alert_message_buzzer = true;
 
     if (moduleConfig.external_notification.enabled) {
+#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER)
         if (inputBroker) // put our callback in the inputObserver list
             inputObserver.observe(inputBroker);
-
+#endif
         if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig),
                               &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) {
             memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone));
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 2f3da0fdd..f2c34a7c8 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -3,6 +3,7 @@
 #include "buzz/BuzzerFeedbackThread.h"
 #include "input/ExpressLRSFiveWay.h"
 #include "input/InputBroker.h"
+#include "input/RotaryEncoderImpl.h"
 #include "input/RotaryEncoderInterruptImpl1.h"
 #include "input/SerialKeyboardImpl.h"
 #include "input/TrackballInterruptImpl1.h"
@@ -87,7 +88,7 @@
 #include "modules/StoreForwardModule.h"
 #endif
 #endif
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)
+
 #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
 #include "modules/ExternalNotificationModule.h"
 #endif
@@ -97,7 +98,6 @@
 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_SERIAL
 #include "modules/SerialModule.h"
 #endif
-#endif
 
 #if !MESHTASTIC_EXCLUDE_DROPZONE
 #include "modules/DropzoneModule.h"
@@ -170,11 +170,20 @@ void setupModules()
                 delete rotaryEncoderInterruptImpl1;
                 rotaryEncoderInterruptImpl1 = nullptr;
             }
+#ifdef T_LORA_PAGER
+            // use a special FSM based rotary encoder version for T-LoRa Pager
+            rotaryEncoderImpl = new RotaryEncoderImpl();
+            if (!rotaryEncoderImpl->init()) {
+                delete rotaryEncoderImpl;
+                rotaryEncoderImpl = nullptr;
+            }
+#else
             upDownInterruptImpl1 = new UpDownInterruptImpl1();
             if (!upDownInterruptImpl1->init()) {
                 delete upDownInterruptImpl1;
                 upDownInterruptImpl1 = nullptr;
             }
+#endif
             cardKbI2cImpl = new CardKbI2cImpl();
             cardKbI2cImpl->init();
 #ifdef INPUTBROKER_MATRIX_TYPE
@@ -236,8 +245,8 @@ void setupModules()
 #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
         new PowerTelemetryModule();
 #endif
-#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) &&               \
-    !defined(CONFIG_IDF_TARGET_ESP32C3)
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
+    !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 #if !MESHTASTIC_EXCLUDE_SERIAL
         if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
             new SerialModule();
@@ -258,13 +267,11 @@ void setupModules()
         storeForwardModule = new StoreForwardModule();
 #endif
 #endif
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)
 #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
         externalNotificationModule = new ExternalNotificationModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
         new RangeTestModule();
-#endif
 #endif
     } else {
 #if !MESHTASTIC_EXCLUDE_ADMIN
diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp
index eebf428a4..97dc17001 100644
--- a/src/modules/NeighborInfoModule.cpp
+++ b/src/modules/NeighborInfoModule.cpp
@@ -105,14 +105,15 @@ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies)
 {
     meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
     collectNeighborInfo(&neighborInfo);
-    meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
-    // send regardless of whether or not we have neighbors in our DB,
-    // because we want to get neighbors for the next cycle
-    p->to = dest;
-    p->decoded.want_response = wantReplies;
-    p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
-    printNeighborInfo("SENDING", &neighborInfo);
-    service->sendToMesh(p, RX_SRC_LOCAL, true);
+    // only send neighbours if we have some to send
+    if (neighborInfo.neighbors_count > 0) {
+        meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
+        p->to = dest;
+        p->decoded.want_response = wantReplies;
+        p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
+        printNeighborInfo("SENDING", &neighborInfo);
+        service->sendToMesh(p, RX_SRC_LOCAL, true);
+    }
 }
 
 /*
@@ -214,4 +215,4 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen
         neighbors.push_back(new_nbr);
     }
     return &neighbors.back();
-}
\ No newline at end of file
+}
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index b6fee7703..0060e99fa 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -14,6 +14,10 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
 {
     auto p = *pptr;
 
+    if (mp.from == nodeDB->getNodeNum()) {
+        LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
+        return false;
+    }
     if (p.is_licensed != owner.is_licensed) {
         LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
         return true;
diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp
index 6f3d69acf..d1d2d9ead 100644
--- a/src/modules/RangeTestModule.cpp
+++ b/src/modules/RangeTestModule.cpp
@@ -31,7 +31,7 @@ uint32_t packetSequence = 0;
 
 int32_t RangeTestModule::runOnce()
 {
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
+#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO)
 
     /*
         Uncomment the preferences below if you want to use the module
@@ -130,7 +130,7 @@ void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
 
 ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp)
 {
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
+#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO)
 
     if (moduleConfig.range_test.enabled) {
 
diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp
index 39b297965..7485f1c2d 100644
--- a/src/modules/SerialModule.cpp
+++ b/src/modules/SerialModule.cpp
@@ -45,9 +45,12 @@
 
 
 */
+#ifdef HELTEC_MESH_SOLAR
+#include "meshSolarApp.h"
+#endif
 
-#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) &&               \
-    !defined(CONFIG_IDF_TARGET_ESP32C3)
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
+    !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 
 #define RX_BUFFER 256
 #define TIMEOUT 250
@@ -61,10 +64,10 @@ SerialModule *serialModule;
 SerialModuleRadio *serialModuleRadio;
 
 #if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) ||                          \
-    defined(ELECROW_ThinkNode_M5)
+    defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE)
 SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {}
 static Print *serialPrint = &Serial;
-#elif defined(CONFIG_IDF_TARGET_ESP32C6)
+#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172)
 SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") {}
 static Print *serialPrint = &Serial1;
 #else
@@ -78,7 +81,8 @@ size_t serialPayloadSize;
 bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config)
 {
     if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA,
-                                                          meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) {
+                                                          meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO,
+                                                          meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
         const char *warning =
             "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes.";
         LOG_ERROR(warning);
@@ -169,7 +173,18 @@ int32_t SerialModule::runOnce()
                 Serial.begin(baud);
                 Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
             }
-
+#elif defined(ARCH_STM32WL)
+#ifndef RAK3172
+            HardwareSerial *serialInstance = &Serial2;
+#else
+            HardwareSerial *serialInstance = &Serial1;
+#endif
+            if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
+                serialInstance->setTx(moduleConfig.serial.txd);
+                serialInstance->setRx(moduleConfig.serial.rxd);
+            }
+            serialInstance->begin(baud);
+            serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
 #elif defined(ARCH_ESP32)
 
             if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
@@ -179,8 +194,8 @@ int32_t SerialModule::runOnce()
                 Serial.begin(baud);
                 Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
             }
-#elif !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) &&                    \
-    !defined(ELECROW_ThinkNode_M5)
+#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) &&                             \
+    !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
             if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
 #ifdef ARCH_RP2040
                 Serial2.setFIFOSize(RX_BUFFER);
@@ -236,18 +251,33 @@ int32_t SerialModule::runOnce()
                 }
             }
 
-#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) &&                      \
-    !defined(ELECROW_ThinkNode_M5)
+#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) &&                               \
+    !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
             else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
                 processWXSerial();
 
-            } else {
+            }
+#if defined(HELTEC_MESH_SOLAR)
+            else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
+                serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1);
+                // If the parsing fails, the following parsing will be performed.
+                if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) {
+                    return runOncePart(serialBytes, serialPayloadSize);
+                }
+            }
+#endif
+            else {
 #if defined(CONFIG_IDF_TARGET_ESP32C6)
                 while (Serial1.available()) {
                     serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
 #else
-                while (Serial2.available()) {
-                    serialPayloadSize = Serial2.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
+#ifndef RAK3172
+                HardwareSerial *serialInstance = &Serial2;
+#else
+                HardwareSerial *serialInstance = &Serial1;
+#endif
+                while (serialInstance->available()) {
+                    serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
 #endif
                     serialModuleRadio->sendPayload();
                 }
@@ -496,8 +526,8 @@ ParsedLine parseLine(const char *line)
  */
 void SerialModule::processWXSerial()
 {
-#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) &&                 \
-    !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
+#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) &&              \
+    !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL)
     static unsigned int lastAveraged = 0;
     static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
     static double dir_sum_sin = 0;
diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h
index 1c74c927c..dbe4f75db 100644
--- a/src/modules/SerialModule.h
+++ b/src/modules/SerialModule.h
@@ -8,8 +8,8 @@
 #include 
 #include 
 
-#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) &&               \
-    !defined(CONFIG_IDF_TARGET_ESP32C3)
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
+    !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 
 class SerialModule : public StreamAPI, private concurrency::OSThread
 {
diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp
index 8926b171c..c90d9250f 100644
--- a/src/modules/Telemetry/EnvironmentTelemetry.cpp
+++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp
@@ -198,6 +198,13 @@ T1000xSensor t1000xSensor;
 IndicatorSensor indicatorSensor;
 #endif
 
+#if __has_include()
+#include "Sensor/TSL2561Sensor.h"
+TSL2561Sensor tsl2561Sensor;
+#else
+NullSensor tsl2561Sensor;
+#endif
+
 #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
 
@@ -296,6 +303,8 @@ int32_t EnvironmentTelemetryModule::runOnce()
                 result = max17048Sensor.runOnce();
             if (cgRadSens.hasSensor())
                 result = cgRadSens.runOnce();
+            if (tsl2561Sensor.hasSensor())
+                result = tsl2561Sensor.runOnce();
             if (pct2075Sensor.hasSensor())
                 result = pct2075Sensor.runOnce();
                 // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the
@@ -642,6 +651,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
         valid = valid && nau7802Sensor.getMetrics(m);
         hasSensor = true;
     }
+    if (tsl2561Sensor.hasSensor()) {
+        valid = valid && tsl2561Sensor.getMetrics(m);
+        hasSensor = true;
+    }
     if (aht10Sensor.hasSensor()) {
         if (!bmp280Sensor.hasSensor() && !bmp3xxSensor.hasSensor()) {
             valid = valid && aht10Sensor.getMetrics(m);
diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp
new file mode 100644
index 000000000..9f3b7e460
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp
@@ -0,0 +1,41 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TSL2561Sensor.h"
+#include "TelemetrySensor.h"
+#include 
+
+TSL2561Sensor::TSL2561Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL2561, "TSL2561") {}
+
+int32_t TSL2561Sensor::runOnce()
+{
+    LOG_INFO("Init sensor: %s", sensorName);
+    if (!hasSensor()) {
+        return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+    }
+
+    status = tsl.begin(nodeTelemetrySensorsMap[sensorType].second);
+
+    return initI2CSensor();
+}
+
+void TSL2561Sensor::setup()
+{
+    tsl.setGain(TSL2561_GAIN_1X);
+    tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS);
+}
+
+bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+    measurement->variant.environment_metrics.has_lux = true;
+    sensors_event_t event;
+    tsl.getEvent(&event);
+    measurement->variant.environment_metrics.lux = event.light;
+    LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux);
+
+    return true;
+}
+
+#endif
diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.h b/src/modules/Telemetry/Sensor/TSL2561Sensor.h
new file mode 100644
index 000000000..0329becd8
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.h
@@ -0,0 +1,23 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include 
+
+class TSL2561Sensor : public TelemetrySensor
+{
+  private:
+    // The magic number is a sensor id, the actual value doesn't matter
+    Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345);
+
+  protected:
+    virtual void setup() override;
+
+  public:
+    TSL2561Sensor();
+    virtual int32_t runOnce() override;
+    virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+};
+#endif
diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp
index f4eccd667..d7df90bb5 100644
--- a/src/modules/TraceRouteModule.cpp
+++ b/src/modules/TraceRouteModule.cpp
@@ -602,7 +602,7 @@ void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state
             int start = 0;
             int newlinePos = resultText.indexOf('\n', start);
 
-            while (newlinePos != -1 || start < resultText.length()) {
+            while (newlinePos != -1 || start < static_cast(resultText.length())) {
                 String segment;
                 if (newlinePos != -1) {
                     segment = resultText.substring(start, newlinePos);
@@ -624,7 +624,7 @@ void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state
                         int lastGoodBreak = -1;
                         bool lineComplete = false;
 
-                        for (int i = 0; i < remaining.length(); i++) {
+                        for (int i = 0; i < static_cast(remaining.length()); i++) {
                             char ch = remaining.charAt(i);
                             String testLine = tempLine + ch;
 
diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h
index d0efa5ae6..ddca5767c 100755
--- a/src/motion/BMX160Sensor.h
+++ b/src/motion/BMX160Sensor.h
@@ -7,7 +7,7 @@
 
 #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
 
-#if defined(RAK_4631) && !defined(RAK2560) && __has_include()
+#if !defined(RAK2560) && __has_include()
 
 #include "Fusion/Fusion.h"
 #include 
diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp
index 21d4a8fa0..7f7a9d511 100644
--- a/src/mqtt/MQTT.cpp
+++ b/src/mqtt/MQTT.cpp
@@ -95,6 +95,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
     p->hop_start = e.packet->hop_start;
     p->want_ack = e.packet->want_ack;
     p->via_mqtt = true; // Mark that the packet was received via MQTT
+    p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
     p->which_payload_variant = e.packet->which_payload_variant;
     memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted)));
 
@@ -278,6 +279,8 @@ struct PubSubConfig {
 
     // Defaults
     static constexpr uint16_t defaultPort = 1883;
+    static constexpr uint16_t defaultPortTls = 8883;
+
     uint16_t serverPort = defaultPort;
     String serverAddr = default_mqtt_address;
     const char *mqttUsername = default_mqtt_username;
@@ -640,7 +643,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
     }
 
     const bool defaultServer = isDefaultServer(parsed.serverAddr);
-    if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) {
+    if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) {
         const char *warning = "Invalid MQTT config: default server address must not have a port specified";
         LOG_ERROR(warning);
 #if !IS_RUNNING_TESTS
diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index 834184292..95e191c8e 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -223,9 +223,12 @@ void NimbleBluetooth::deinit()
     LOG_INFO("Disable bluetooth until reboot");
 
 #ifdef BLE_LED
+#ifdef BLE_LED_INVERTED
+    digitalWrite(BLE_LED, HIGH);
+#else
     digitalWrite(BLE_LED, LOW);
 #endif
-
+#endif
     NimBLEDevice::deinit();
 #endif
 }
diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h
index 522e862ac..cb0f0dab3 100644
--- a/src/platform/esp32/architecture.h
+++ b/src/platform/esp32/architecture.h
@@ -192,6 +192,8 @@
 #define HW_VENDOR meshtastic_HardwareModel_LINK_32
 #elif defined(T_DECK_PRO)
 #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
+#elif defined(T_LORA_PAGER)
+#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
 #endif
 
 // -----------------------------------------------------------------------------
diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp
new file mode 100644
index 000000000..ea5773d30
--- /dev/null
+++ b/src/platform/extra_variants/t_lora_pager/variant.cpp
@@ -0,0 +1,27 @@
+#include "configuration.h"
+
+#ifdef T_LORA_PAGER
+
+#include "AudioBoard.h"
+
+DriverPins PinsAudioBoardES8311;
+AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
+
+// TLora Pager specific init
+void lateInitVariant()
+{
+    // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug);
+    // I2C: function, scl, sda
+    PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire);
+    // I2S: function, mclk, bck, ws, data_out, data_in
+    PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN);
+
+    // configure codec
+    CodecConfig cfg;
+    cfg.input_device = ADC_INPUT_LINE1;
+    cfg.output_device = DAC_OUTPUT_ALL;
+    cfg.i2s.bits = BIT_LENGTH_16BITS;
+    cfg.i2s.rate = RATE_44K;
+    board.begin(cfg);
+}
+#endif
\ No newline at end of file
diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h
index 1bbdd77e0..c9938062e 100644
--- a/src/platform/nrf52/architecture.h
+++ b/src/platform/nrf52/architecture.h
@@ -53,10 +53,15 @@
 #define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAG
 #elif defined(GAT562_MESH_TRIAL_TRACKER)
 #define HW_VENDOR meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER
+#elif defined(NOMADSTAR_METEOR_PRO)
+#define HW_VENDOR meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO
+// MAke sure all custom RAK4630 boards are defined before the generic RAK4630
 #elif defined(RAK4630)
 #define HW_VENDOR meshtastic_HardwareModel_RAK4631
 #elif defined(TTGO_T_ECHO)
 #define HW_VENDOR meshtastic_HardwareModel_T_ECHO
+#elif defined(T_ECHO_LITE)
+#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE
 #elif defined(ELECROW_ThinkNode_M1)
 #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1
 #elif defined(NANO_G2_ULTRA)
@@ -89,12 +94,12 @@
 #define HW_VENDOR meshtastic_HardwareModel_SEEED_SOLAR_NODE
 #elif defined(HELTEC_MESH_POCKET)
 #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_POCKET
-#elif defined(NOMADSTAR_METEOR_PRO)
-#define HW_VENDOR meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO
 #elif defined(SEEED_WIO_TRACKER_L1_EINK)
 #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK
 #elif defined(SEEED_WIO_TRACKER_L1)
 #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1
+#elif defined(HELTEC_MESH_SOLAR)
+#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR
 #else
 #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN
 #endif
diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp
index 1bf9a39fd..8ce74d5f7 100644
--- a/src/platform/nrf52/main-nrf52.cpp
+++ b/src/platform/nrf52/main-nrf52.cpp
@@ -282,10 +282,14 @@ void cpuDeepSleep(uint32_t msecToWake)
 #if SPI_INTERFACES_COUNT > 1
     SPI1.end();
 #endif
-    // This may cause crashes as debug messages continue to flow.
-    Serial.end();
+    if (Serial)       // Another check in case of disabled default serial, does nothing bad
+        Serial.end(); // This may cause crashes as debug messages continue to flow.
+
+        // This causes troubles with waking up on nrf52 (on pro-micro in particular):
+        // we have no Serial1 in use on nrf52, check Serial and GPS modules.
 #ifdef PIN_SERIAL1_RX
-    Serial1.end();
+    if (Serial1) // A straightforward solution to the wake from deepsleep problem
+        Serial1.end();
 #endif
     setBluetoothEnable(false);
 
@@ -319,7 +323,7 @@ void cpuDeepSleep(uint32_t msecToWake)
 #endif
 #endif
 
-#ifdef HELTEC_MESH_NODE_T114
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR)
     nrf_gpio_cfg_default(PIN_GPS_PPS);
     detachInterrupt(PIN_GPS_PPS);
     detachInterrupt(PIN_BUTTON1);
@@ -362,6 +366,7 @@ void cpuDeepSleep(uint32_t msecToWake)
         // Resume on user button press
         // https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738
         constexpr uint32_t DFU_MAGIC_SKIP = 0x6d;
+        sd_power_gpregret_clr(0, 0xFF);           // Clear the register before setting a new values in it for stability reasons
         sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP
 
         // FIXME, use system off mode with ram retention for key state?
@@ -378,6 +383,12 @@ void cpuDeepSleep(uint32_t msecToWake)
         nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1);
 #endif
 
+#ifdef PROMICRO_DIY_TCXO
+        nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin
+        nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge
+        nrf_gpio_cfg_sense_set(BUTTON_PIN, sense);           // Apply SENSE to wake up the device from the deep sleep
+#endif
+
         auto ok = sd_power_system_off();
         if (ok != NRF_SUCCESS) {
             LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!");
diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp
index 5f99ec2c3..929a45d09 100644
--- a/src/platform/portduino/PortduinoGlue.cpp
+++ b/src/platform/portduino/PortduinoGlue.cpp
@@ -10,6 +10,7 @@
 #include "linux/gpio/LinuxGPIOPin.h"
 #include "meshUtils.h"
 #include "yaml-cpp/yaml.h"
+#include 
 #include 
 #include 
 #include 
@@ -29,11 +30,11 @@
 
 std::map settingsMap;
 std::map settingsStrings;
+portduino_config_struct portduino_config;
 std::ofstream traceFile;
 Ch341Hal *ch341Hal = nullptr;
 char *configPath = nullptr;
 char *optionMac = nullptr;
-bool forceSimulated = false;
 bool verboseEnabled = false;
 
 const char *argp_program_version = optstr(APP_VERSION);
@@ -66,7 +67,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
         configPath = arg;
         break;
     case 's':
-        forceSimulated = true;
+        portduino_config.force_simradio = true;
         break;
     case 'h':
         optionMac = arg;
@@ -189,7 +190,7 @@ void portduinoSetup()
 
     YAML::Node yamlConfig;
 
-    if (forceSimulated == true) {
+    if (portduino_config.force_simradio == true) {
         settingsMap[use_simradio] = true;
     } else if (configPath != nullptr) {
         if (loadConfig(configPath)) {
@@ -253,16 +254,95 @@ void portduinoSetup()
             std::cout << "autoconf: Could not locate CH341 device" << std::endl;
         }
         // Try Pi HAT+
-        std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
-        if (access("/proc/device-tree/hat/product", R_OK) == 0) {
-            std::ifstream hatProductFile("/proc/device-tree/hat/product");
-            if (hatProductFile.is_open()) {
-                hatProductFile.read(autoconf_product, 95);
-                hatProductFile.close();
+        if (strlen(autoconf_product) < 6) {
+            std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
+            if (access("/proc/device-tree/hat/product", R_OK) == 0) {
+                std::ifstream hatProductFile("/proc/device-tree/hat/product");
+                if (hatProductFile.is_open()) {
+                    hatProductFile.read(autoconf_product, 95);
+                    hatProductFile.close();
+                }
+                std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
+            } else {
+                std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
+            }
+        }
+        // attempt to load autoconf data from an EEPROM on 0x50
+        //      RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8
+        // :mac address :<16 random unique bytes in hexidecimal> : crc32
+        // crc32 is calculated on the eeprom string up to but not including the final colon
+        if (strlen(autoconf_product) < 6) {
+            try {
+                char *mac_start = nullptr;
+                char *devID_start = nullptr;
+                char *crc32_start = nullptr;
+                Wire.begin();
+                Wire.beginTransmission(0x50);
+                Wire.write(0x0);
+                Wire.write(0x0);
+                Wire.endTransmission();
+                Wire.requestFrom((uint8_t)0x50, (uint8_t)75);
+                uint8_t i = 0;
+                delay(100);
+                std::string autoconf_raw;
+                while (Wire.available() && i < sizeof(autoconf_product)) {
+                    autoconf_product[i] = Wire.read();
+                    if (autoconf_product[i] == 0xff) {
+                        autoconf_product[i] = 0x0;
+                        break;
+                    }
+                    autoconf_raw += autoconf_product[i];
+                    if (autoconf_product[i] == ':') {
+                        autoconf_product[i] = 0x0;
+                        if (mac_start == nullptr) {
+                            mac_start = autoconf_product + i + 1;
+                        } else if (devID_start == nullptr) {
+                            devID_start = autoconf_product + i + 1;
+                        } else if (crc32_start == nullptr) {
+                            crc32_start = autoconf_product + i + 1;
+                        }
+                    }
+                    i++;
+                }
+                if (crc32_start != nullptr && strlen(crc32_start) == 8) {
+                    std::string crc32_str(crc32_start);
+                    uint32_t crc32_value = 0;
+
+                    // convert crc32 ascii to raw uint32
+                    for (int j = 0; j < 4; j++) {
+                        crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8;
+                    }
+                    std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl;
+
+                    // set the autoconf string to blank and short circuit
+                    if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) {
+                        std::cout << "autoconf: crc32 mismatch, dropping " << std::endl;
+                        autoconf_product[0] = 0x0;
+                    } else {
+                        std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl;
+                        if (mac_start != nullptr) {
+                            std::cout << "autoconf: Found mac data " << mac_start << std::endl;
+                            if (strlen(mac_start) == 12)
+                                settingsStrings[mac_address] = std::string(mac_start);
+                        }
+                        if (devID_start != nullptr) {
+                            std::cout << "autoconf: Found deviceid data " << devID_start << std::endl;
+                            if (strlen(devID_start) == 32) {
+                                std::string devID_str(devID_start);
+                                for (int j = 0; j < 16; j++) {
+                                    portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16);
+                                }
+                                portduino_config.has_device_id = true;
+                            }
+                        }
+                    }
+                } else {
+                    std::cout << "autoconf: crc32 missing " << std::endl;
+                    autoconf_product[0] = 0x0;
+                }
+            } catch (...) {
+                std::cout << "autoconf: Could not locate EEPROM" << std::endl;
             }
-            std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
-        } else {
-            std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
         }
         // Load the config file based on the product string
         if (strlen(autoconf_product) > 0) {
@@ -553,6 +633,48 @@ bool loadConfig(const char *configPath)
                     }
                 }
             }
+            if (yamlConfig["Lora"]["rfswitch_table"]) {
+                portduino_config.has_rfswitch_table = true;
+                portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY;
+                portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX;
+                portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX;
+                portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP;
+                portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF;
+                portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS;
+                portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI;
+                portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE;
+
+                for (int i = 0; i < 5; i++) {
+
+                    // set up the pin array first
+                    if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5")
+                        portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6")
+                        portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7")
+                        portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8")
+                        portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10")
+                        portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10;
+
+                    // now fill in the table
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[0].values[i] = HIGH;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[1].values[i] = HIGH;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[2].values[i] = HIGH;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[3].values[i] = HIGH;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[4].values[i] = HIGH;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[5].values[i] = HIGH;
+                    if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH")
+                        portduino_config.rfswitch_table[6].values[i] = HIGH;
+                }
+            }
         }
         if (yamlConfig["GPIO"]) {
             settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC);
diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h
index 288870eef..8c36a1180 100644
--- a/src/platform/portduino/PortduinoGlue.h
+++ b/src/platform/portduino/PortduinoGlue.h
@@ -3,16 +3,21 @@
 #include 
 #include 
 
+#include "LR11x0Interface.h"
+#include "Module.h"
 #include "platform/portduino/USBHal.h"
 
 // Product strings for auto-configuration
 // {"PRODUCT_STRING", "CONFIG.YAML"}
 // YAML paths are relative to `meshtastic/available.d`
-inline const std::unordered_map configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"},
-                                                                            {"MESHSTICK", "lora-meshstick-1262.yaml"},
-                                                                            {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"},
-                                                                            {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"},
-                                                                            {"POWERPI", "lora-MeshAdv-900M30S.yaml"}};
+inline const std::unordered_map configProducts = {
+    {"MESHTOAD", "lora-usb-meshtoad-e22.yaml"},
+    {"MESHSTICK", "lora-meshstick-1262.yaml"},
+    {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"},
+    {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"},
+    {"POWERPI", "lora-MeshAdv-900M30S.yaml"},
+    {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"},
+    {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}};
 
 enum configNames {
     default_gpiochip,
@@ -126,4 +131,13 @@ bool loadConfig(const char *configPath);
 static bool ends_with(std::string_view str, std::string_view suffix);
 void getMacAddr(uint8_t *dmac);
 bool MAC_from_string(std::string mac_str, uint8_t *dmac);
-std::string exec(const char *cmd);
\ No newline at end of file
+std::string exec(const char *cmd);
+
+extern struct portduino_config_struct {
+    bool has_rfswitch_table = false;
+    uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+    Module::RfSwitchMode_t rfswitch_table[8];
+    bool force_simradio = false;
+    bool has_device_id = false;
+    uint8_t device_id[16] = {0};
+} portduino_config;
\ No newline at end of file
diff --git a/src/power.h b/src/power.h
index 1c078c06d..e96f5b022 100644
--- a/src/power.h
+++ b/src/power.h
@@ -128,6 +128,8 @@ class Power : private concurrency::OSThread
     bool lipoInit();
     /// Setup a Lipo charger
     bool lipoChargerInit();
+    /// Setup a meshSolar battery sensor
+    bool meshSolarInit();
 
   private:
     void shutdown();
diff --git a/src/sleep.cpp b/src/sleep.cpp
index 1a5f246c5..83597e349 100644
--- a/src/sleep.cpp
+++ b/src/sleep.cpp
@@ -32,6 +32,16 @@ esp_sleep_source_t wakeCause; // the reason we booted this time
 #endif
 #include "Throttle.h"
 
+#ifdef USE_XL9555
+#include "ExtensionIOXL9555.hpp"
+extern ExtensionIOXL9555 io;
+#endif
+
+#ifdef HAS_PPM
+#include 
+extern XPowersPPM *PPM;
+#endif
+
 #ifndef INCLUDE_vTaskSuspend
 #define INCLUDE_vTaskSuspend 0
 #endif
@@ -297,6 +307,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
 #endif
 #endif
 
+#ifdef HAS_PPM
+    if (PPM) {
+        LOG_INFO("PMM shutdown");
+        console->flush();
+        PPM->shutdown();
+    }
+#endif
+
 #ifdef HAS_PMU
     if (pmu_found && PMU) {
         // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep.
@@ -412,6 +430,7 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
     if (pmu_found)
         gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
 #endif
+
     auto res = esp_sleep_enable_gpio_wakeup();
     if (res != ESP_OK) {
         LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
index 557ee7a49..37cfc1626 100644
--- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp
+++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
@@ -1,20 +1,9 @@
 #include "../test_helpers.h"
 
-// Test encrypted packet serialization
-void test_encrypted_packet_serialization()
+// Helper function for all encrypted packet assertions
+void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
 {
-    meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
-    packet.from = 0x11223344;
-    packet.to = 0x55667788;
-    packet.id = 0x9999;
-    packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
-
-    // Add some dummy encrypted data
-    const char *encrypted_data = "encrypted_payload_data";
-    packet.encrypted.size = strlen(encrypted_data);
-    memcpy(packet.encrypted.bytes, encrypted_data, packet.encrypted.size);
-
-    std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
+    // Parse and validate JSON
     TEST_ASSERT_TRUE(json.length() > 0);
 
     JSONValue *root = JSON::Parse(json.c_str());
@@ -23,28 +12,48 @@ void test_encrypted_packet_serialization()
 
     JSONObject jsonObj = root->AsObject();
 
-    // Check basic packet fields
+    // Assert basic packet fields
     TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber());
+    TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber());
 
     TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber());
+    TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber());
 
     TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber());
+    TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
 
-    // Check that it has encrypted data fields (not "payload" but "bytes" and "size")
+    // Assert encrypted data fields
     TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
-    TEST_ASSERT_TRUE(jsonObj["bytes"]->IsString());
+    TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
 
     TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
-    TEST_ASSERT_EQUAL(22, (int)jsonObj["size"]->AsNumber()); // strlen("encrypted_payload_data") = 22
+    TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber());
 
-    // The encrypted data should be hex-encoded
+    // Assert hex encoding
     std::string encrypted_hex = jsonObj["bytes"]->AsString();
-    TEST_ASSERT_TRUE(encrypted_hex.length() > 0);
-    // Should be twice the size of the original data (hex encoding)
-    TEST_ASSERT_EQUAL(44, encrypted_hex.length()); // 22 * 2 = 44
+    TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length());
 
     delete root;
 }
+
+// Test encrypted packet serialization
+void test_encrypted_packet_serialization()
+{
+    const char *data = "encrypted_payload_data";
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data),
+                           meshtastic_MeshPacket_encrypted_tag);
+    std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
+
+    assert_encrypted_packet(json, packet);
+}
+
+// Test empty encrypted packet
+void test_empty_encrypted_packet()
+{
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag);
+    std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
+
+    assert_encrypted_packet(json, packet);
+}
diff --git a/test/test_meshpacket_serializer/test_helpers.h b/test/test_meshpacket_serializer/test_helpers.h
index 630e059bc..12245b85d 100644
--- a/test/test_meshpacket_serializer/test_helpers.h
+++ b/test/test_meshpacket_serializer/test_helpers.h
@@ -11,7 +11,8 @@
 #include 
 
 // Helper function to create a test packet with the given port and payload
-static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size)
+static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size,
+                                                int payload_variant = meshtastic_MeshPacket_decoded_tag)
 {
     meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
 
@@ -29,8 +30,12 @@ static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const u
     packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY;
 
     // Set decoded variant
-    packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
+    packet.which_payload_variant = payload_variant;
     packet.decoded.portnum = port;
+    if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) {
+        packet.encrypted.size = payload_size;
+        memcpy(packet.encrypted.bytes, payload, packet.encrypted.size);
+    }
     memcpy(packet.decoded.payload.bytes, payload, payload_size);
     packet.decoded.payload.size = payload_size;
     packet.decoded.want_response = false;
diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp
index d74031fa4..484db8d74 100644
--- a/test/test_meshpacket_serializer/test_serializer.cpp
+++ b/test/test_meshpacket_serializer/test_serializer.cpp
@@ -4,6 +4,10 @@
 
 // Forward declarations for test functions
 void test_text_message_serialization();
+void test_text_message_serialization_null();
+void test_text_message_serialization_long_text();
+void test_text_message_serialization_oversized();
+void test_text_message_serialization_invalid_utf8();
 void test_position_serialization();
 void test_nodeinfo_serialization();
 void test_waypoint_serialization();
@@ -14,6 +18,7 @@ void test_telemetry_environment_metrics_missing_fields();
 void test_telemetry_environment_metrics_complete_coverage();
 void test_telemetry_environment_metrics_unset_fields();
 void test_encrypted_packet_serialization();
+void test_empty_encrypted_packet();
 
 void setup()
 {
@@ -21,6 +26,10 @@ void setup()
 
     // Text message tests
     RUN_TEST(test_text_message_serialization);
+    RUN_TEST(test_text_message_serialization_null);
+    RUN_TEST(test_text_message_serialization_long_text);
+    RUN_TEST(test_text_message_serialization_oversized);
+    RUN_TEST(test_text_message_serialization_invalid_utf8);
 
     // Position tests
     RUN_TEST(test_position_serialization);
@@ -41,6 +50,7 @@ void setup()
 
     // Encrypted packet test
     RUN_TEST(test_encrypted_packet_serialization);
+    RUN_TEST(test_empty_encrypted_packet);
 
     UNITY_END();
 }
diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini
new file mode 100644
index 000000000..809599212
--- /dev/null
+++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini
@@ -0,0 +1,12 @@
+; 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S
+; https://shopee.com.my/product/1095224/21692283917
+[env:9m2ibr_aprs_lora_tracker]
+extends = esp32_base
+board = esp32doit-devkit-v1
+board_level = extra
+build_flags =
+  ${esp32_base.build_flags}
+  -D PRIVATE_HW
+  -D EBYTE_E22
+  -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation
+  -I variants/esp32/diy/9m2ibr_aprs_lora_tracker
diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h
new file mode 100644
index 000000000..037933140
--- /dev/null
+++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h
@@ -0,0 +1,74 @@
+/*
+
+  9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S
+  https://shopee.com.my/product/1095224/21692283917
+
+  Originally developed for LoRa_APRS_iGate and GPIO is similar to
+  https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/variants/ESP32_DIY_1W_LoRa_Mesh_V1_2/board_pinout.h
+
+*/
+
+// OLED (may be different controllers depending on screen size)
+#define I2C_SDA 21
+#define I2C_SCL 22
+#define HAS_SCREEN 1 // Generates randomized BLE pin
+
+// GNSS: Ai-Thinker GP-02 BDS/GNSS module
+#define GPS_RX_PIN 16
+#define GPS_TX_PIN 17
+
+// Button
+#define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client
+
+// LEDs
+#define LED_PIN 13 // Tx LED
+#define USER_LED 2 // Rx LED
+
+// Buzzer
+#define PIN_BUZZER 33
+
+// Battery sense
+#define BATTERY_PIN 35
+#define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance
+#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
+#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION
+
+// SPI
+#define LORA_SCK 18
+#define LORA_MISO 19
+#define LORA_MOSI 23
+
+// LoRa
+#define LORA_CS 5
+#define LORA_DIO0 26          // a No connect on the SX1262/SX1268 module
+#define LORA_RESET 27         // RST for SX1276, and for SX1262/SX1268
+#define LORA_DIO1 12          // IRQ for SX1262/SX1268
+#define LORA_DIO2 RADIOLIB_NC // BUSY for SX1262/SX1268
+#define LORA_DIO3             // NC, but used as TCXO supply by E22 module
+#define LORA_RXEN 32          // RF switch RX (and E22 LNA) control by ESP32 GPIO
+#define LORA_TXEN 25          // RF switch TX (and E22 PA) control by ESP32 GPIO
+
+// RX/TX for RFM95/SX127x
+#define RF95_RXEN LORA_RXEN
+#define RF95_TXEN LORA_TXEN
+// #define RF95_TCXO 
+
+// common pinouts for SX126X modules
+#define SX126X_CS 5
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_DIO2
+#define SX126X_RESET LORA_RESET
+#define SX126X_RXEN LORA_RXEN
+#define SX126X_TXEN LORA_TXEN
+
+// Support alternative modules if soldered in place of E22
+#define USE_RF95 // RFM95/SX127x
+#define USE_SX1262
+#define USE_SX1268
+#define USE_LLCC68
+
+// E22 TCXO support
+#ifdef EBYTE_E22
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL
+#endif
diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini
index 60e686f9e..93c3e3394 100644
--- a/variants/esp32/heltec_wireless_bridge/platformio.ini
+++ b/variants/esp32/heltec_wireless_bridge/platformio.ini
@@ -1,6 +1,7 @@
 [env:heltec-wireless-bridge]
 ;build_type = debug ; to make it possible to step through our jtag debugger 
 extends = esp32_base
+board_level = extra
 board = heltec_wifi_lora_32
 build_flags = 
   ${esp32_base.build_flags}
diff --git a/variants/esp32/trackerd/platformio.ini b/variants/esp32/trackerd/platformio.ini
index 3c2726a3c..00c14fad2 100644
--- a/variants/esp32/trackerd/platformio.ini
+++ b/variants/esp32/trackerd/platformio.ini
@@ -1,5 +1,6 @@
 [env:trackerd]
 extends = esp32_base
+board_level = extra
 board = pico32
 board_build.f_flash = 80000000L
 
diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h
index 61d6149d2..a55808170 100644
--- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h
+++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h
@@ -61,7 +61,7 @@
 #define LORA_DIO1 SX126X_DIO1
 
 #define USE_EINK
-#define PIN_EINK_EN -1    // Note: this is really just backlight power
+// Note: this is really just backlight power
 #define PCA_PIN_EINK_EN 5 // This is the pin number on the GPIO expander
 #define PIN_EINK_CS 39
 #define PIN_EINK_BUSY 42
diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini
index 59bc26000..065f22538 100644
--- a/variants/esp32s3/elecrow_panel/platformio.ini
+++ b/variants/esp32s3/elecrow_panel/platformio.ini
@@ -19,8 +19,6 @@ build_flags = ${esp32s3_base.build_flags} -Os
   -D MESHTASTIC_EXCLUDE_SERIAL=1
   -D MESHTASTIC_EXCLUDE_SOCKETAPI=1
   -D MESHTASTIC_EXCLUDE_SCREEN=1
-  -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
-  -D HAS_TELEMETRY=0
   -D CONFIG_DISABLE_HAL_LOCKS=1
   -D USE_PIN_BUZZER
   -D HAS_SCREEN=0
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h
new file mode 100644
index 000000000..15a26e991
--- /dev/null
+++ b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h
@@ -0,0 +1,28 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include "variant.h"
+#include 
+
+#define USB_VID 0x303a
+#define USB_PID 0x1001
+
+// The default Wire will be mapped to PMU and RTC
+static const uint8_t SDA = 9;
+static const uint8_t SCL = 40;
+
+// Default SPI will be mapped to Radio
+static const uint8_t SS = 12;
+static const uint8_t MOSI = 11;
+static const uint8_t MISO = 10;
+static const uint8_t SCK = 13;
+
+#define SPI_MOSI (11)
+#define SPI_SCK (13)
+#define SPI_MISO (10)
+#define SPI_CS (12)
+
+// LEDs
+#define LED_BUILTIN LED_GREEN
+
+#endif /* Pins_Arduino_h */
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
new file mode 100644
index 000000000..8b86e0217
--- /dev/null
+++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
@@ -0,0 +1,87 @@
+; rak_wismeshtap2 rak3112
+
+[rak_wismeshtap_s3]
+extends = esp32s3_base
+board = wiscore_rak3312
+board_check = true
+upload_protocol = esptool
+board_build.partitions = default_8MB.csv
+
+build_flags = 
+  ${esp32_base.build_flags} 
+  -D RAK3312
+  -D RAK_WISMESH_TAP_V2 
+  -I variants/esp32s3/rak_wismesh_tap_v2
+
+lib_deps =
+  ${esp32s3_base.lib_deps}
+  lovyan03/LovyanGFX@^1.2.0
+
+[ft5x06]
+extends = mesh_tab_base
+build_flags = 
+  -D LGFX_TOUCH=FT5x06
+  -D LGFX_TOUCH_I2C_FREQ=100000
+  -D LGFX_TOUCH_I2C_PORT=0
+  -D LGFX_TOUCH_I2C_ADDR=0x38
+  -D LGFX_TOUCH_I2C_SDA=9
+  -D LGFX_TOUCH_I2C_SCL=40
+  -D LGFX_TOUCH_RST=-1
+  -D LGFX_TOUCH_INT=39
+
+[env:rak_wismesh_tap_v2-tft]
+extends = rak_wismeshtap_s3
+
+build_flags =
+  ${rak_wismeshtap_s3.build_flags}
+  -D CONFIG_ARDUHAL_ESP_LOG
+  -D CONFIG_ARDUHAL_LOG_COLORS=1
+  -D CONFIG_DISABLE_HAL_LOCKS=1
+  -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 LV_BUILD_TEST=0
+  -D USE_LOG_DEBUG
+  -D LOG_DEBUG_INC=\"DebugConfiguration.h\"
+  -D RADIOLIB_SPI_PARANOID=0
+  -D INPUTDRIVER_BUTTON_TYPE=0
+  -D HAS_SDCARD
+  -D HAS_SCREEN=0
+  -D HAS_TFT=1
+  -D USE_PIN_BUZZER=PIN_BUZZER
+  -D RAM_SIZE=5120
+  -D LGFX_DRIVER_TEMPLATE
+  -D LGFX_DRIVER=LGFX_GENERIC
+  -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\"
+  -D LGFX_PIN_SCK=13
+  -D LGFX_PIN_MOSI=11
+  -D LGFX_PIN_MISO=10
+  -D LGFX_PIN_DC=42
+  -D LGFX_PIN_CS=12
+  -D LGFX_PIN_RST=-1
+  -D LGFX_PIN_BL=41
+  -D VIEW_320x240
+  -D USE_PACKET_API
+  ${ft5x06.build_flags}
+  -D LGFX_SCREEN_WIDTH=240
+  -D LGFX_SCREEN_HEIGHT=320
+  -D LGFX_PANEL=ST7789
+  -D LGFX_ROTATION=1
+  -D LGFX_TOUCH_X_MIN=0
+  -D LGFX_TOUCH_X_MAX=239
+  -D LGFX_TOUCH_Y_MIN=0
+  -D LGFX_TOUCH_Y_MAX=319
+  -D LGFX_TOUCH_ROTATION=2
+  -D LGFX_CFG_HOST=SPI3_HOST
+  -D MAP_FULL_REDRAW=1
+
+lib_deps =
+  ${rak_wismeshtap_s3.lib_deps}
+  ${device-ui_base.lib_deps}
+
+
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h
new file mode 100644
index 000000000..2fc056557
--- /dev/null
+++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h
@@ -0,0 +1,71 @@
+#ifndef _VARIANT_RAK_WISMESHTAP_V2_H
+#define _VARIANT_RAK_WISMESHTAP_V2_H
+
+#define I2C_SDA 9
+#define I2C_SCL 40
+
+#define USE_SX1262
+
+#define LORA_SCK 5
+#define LORA_MISO 3
+#define LORA_MOSI 6
+#define LORA_CS 7
+#define LORA_RESET 8
+
+#ifdef USE_SX1262
+#define SX126X_CS LORA_CS
+#define SX126X_DIO1 47
+#define SX126X_BUSY 48
+#define SX126X_RESET LORA_RESET
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+#endif
+
+#define SX126X_POWER_EN (4)
+
+#define PIN_POWER_EN PIN_3V3_EN
+#define PIN_3V3_EN (14)
+
+#define LED_GREEN 46
+#define LED_BLUE 45
+
+#define PIN_LED1 LED_GREEN
+#define PIN_LED2 LED_BLUE
+
+#define LED_CONN LED_BLUE
+#define LED_PIN LED_GREEN
+#define ledOff(pin) pinMode(pin, INPUT)
+
+#define LED_STATE_ON 1 // State when LED is litted
+
+#define HAS_GPS 1
+#define GPS_TX_PIN 43
+#define GPS_RX_PIN 44
+
+#define SPI_MOSI (11)
+#define SPI_SCK (13)
+#define SPI_MISO (10)
+#define SPI_CS (12)
+
+#define HAS_BUTTON 1
+#define BUTTON_PIN 0
+
+#define CANNED_MESSAGE_MODULE_ENABLE 1
+#define USE_VIRTUAL_KEYBOARD 1
+
+#define BATTERY_PIN 1
+#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
+#define ADC_MULTIPLIER 1.667
+
+#define PIN_BUZZER 38
+
+#define HAS_SDCARD 1
+#define SDCARD_USE_SPI1 1
+#define SDCARD_CS 2
+
+#define SPI_FREQUENCY 40000000
+#define SPI_READ_FREQUENCY 16000000
+
+#define SD_SPI_FREQUENCY 50000000
+
+#endif
\ No newline at end of file
diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h
index b08d3f65f..abe0a772a 100644
--- a/variants/esp32s3/t-deck-pro/variant.h
+++ b/variants/esp32s3/t-deck-pro/variant.h
@@ -92,3 +92,12 @@
 #define SX126X_DIO3_TCXO_VOLTAGE 2.4
 // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface
 // code)
+
+#define MODEM_POWER_EN  41
+#define MODEM_PWRKEY    40
+#define MODEM_RST  9
+#define MODEM_RI  7
+#define MODEM_DTR  8
+#define MODEM_RX  10
+#define MODEM_TX  11
+
diff --git a/variants/esp32s3/tlora-pager/pins_arduino.h b/variants/esp32s3/tlora-pager/pins_arduino.h
new file mode 100644
index 000000000..a6321f510
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/pins_arduino.h
@@ -0,0 +1,19 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include 
+
+#define USB_VID 0x303a
+#define USB_PID 0x1001
+
+// used for keyboard, battery gauge, charger and haptic driver
+static const uint8_t SDA = 3;
+static const uint8_t SCL = 2;
+
+// Default SPI will be mapped to Radio
+static const uint8_t SS = 36;
+static const uint8_t MOSI = 34;
+static const uint8_t MISO = 33;
+static const uint8_t SCK = 35;
+
+#endif /* Pins_Arduino_h */
diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini
new file mode 100644
index 000000000..312d46259
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/platformio.ini
@@ -0,0 +1,71 @@
+; LilyGo T-Lora-Pager
+[env:tlora-pager]
+extends = esp32s3_base
+board = t-deck-pro ; same as T-Deck Pro
+board_check = true
+board_build.partitions = default_16MB.csv
+upload_protocol = esptool
+
+build_flags = ${esp32s3_base.build_flags} 
+  -I variants/esp32s3/tlora-pager
+  -D T_LORA_PAGER 
+  -D BOARD_HAS_PSRAM
+  -D GPS_POWER_TOGGLE
+  -D HAS_SDCARD
+  -D SDCARD_USE_SPI1
+  -D ENABLE_ROTARY_PULLUP
+  -D ENABLE_BUTTON_PULLUP
+  -D HALF_STEP
+
+lib_deps = ${esp32s3_base.lib_deps}
+  lovyan03/LovyanGFX@1.2.7
+  earlephilhower/ESP8266Audio@1.9.9
+  earlephilhower/ESP8266SAM@1.0.1
+  adafruit/Adafruit DRV2605 Library@1.2.4
+  lewisxhe/PCF8563_Library@1.0.1
+  lewisxhe/SensorLib@0.3.1
+  https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip
+  https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
+  https://github.com/mverch67/RotaryEncoder/archive/25a59d5745a6645536f921427d80b08e78f886d4.zip
+
+[env:tlora-pager-tft]
+board_level = extra
+extends = env:tlora-pager
+build_flags =
+  ${env:tlora-pager.build_flags}
+  -D CONFIG_DISABLE_HAL_LOCKS=1
+  -D INPUTDRIVER_ROTARY_TYPE=1
+  -D INPUTDRIVER_ROTARY_UP=40
+  -D INPUTDRIVER_ROTARY_DOWN=41
+  -D INPUTDRIVER_ROTARY_BTN=7
+  -D INPUTDRIVER_BUTTON_TYPE=0
+  -D HAS_SCREEN=1
+  -D HAS_TFT=1
+  -D USE_I2S_BUZZER
+  -D RAM_SIZE=5120
+  -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_SPI_PARANOID=0
+  -D LGFX_SCREEN_WIDTH=222
+  -D LGFX_SCREEN_HEIGHT=480
+  -D DISPLAY_SIZE=480x222 ; landscape mode
+  -D DISPLAY_SET_RESOLUTION
+  -D LGFX_DRIVER=LGFX_TLORA_PAGER
+  -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_LORA_PAGER.h\"
+;  -D LVGL_DRIVER=LVGL_T_LORA_PAGER
+;  -D LV_USE_ST7796=1
+  -D VIEW_480x222
+  -D USE_PACKET_API
+  -D MAP_FULL_REDRAW
+
+lib_deps =
+  ${env:tlora-pager.lib_deps}
+  ${device-ui_base.lib_deps}
diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h
new file mode 100644
index 000000000..ee48088c8
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/variant.h
@@ -0,0 +1,125 @@
+// ST7796 TFT LCD
+#define TFT_CS 38
+#define ST7796_CS TFT_CS
+#define ST7796_RS 37    // DC
+#define ST7796_SDA MOSI // MOSI
+#define ST7796_SCK SCK
+#define ST7796_RESET -1
+#define ST7796_MISO MISO
+#define ST7796_BUSY -1
+#define ST7796_BL 42
+#define ST7796_SPI_HOST SPI2_HOST
+#define TFT_BL 42
+#define SPI_FREQUENCY 75000000
+#define SPI_READ_FREQUENCY 16000000
+#define TFT_HEIGHT 480
+#define TFT_WIDTH 222
+#define TFT_OFFSET_X 49
+#define TFT_OFFSET_Y 0
+#define TFT_OFFSET_ROTATION 3
+#define SCREEN_ROTATE
+#define SCREEN_TRANSITION_FRAMERATE 5
+#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
+
+#define I2C_SDA SDA
+#define I2C_SCL SCL
+
+#define USE_POWERSAVE
+#define SLEEP_TIME 120
+
+// GNNS
+#define HAS_GPS 1
+#define GPS_BAUDRATE 38400
+#define GPS_RX_PIN 4
+#define GPS_TX_PIN 12
+#define PIN_GPS_PPS 13
+
+// PCF8563 RTC Module
+#if __has_include("pcf8563.h")
+#include "pcf8563.h"
+#endif
+#define PCF8563_RTC 0x51
+#define HAS_RTC 1
+
+// Rotary
+#define ROTARY_A (40)
+#define ROTARY_B (41)
+#define ROTARY_PRESS (7)
+
+#define BUTTON_PIN 0
+
+// SPI interface SD card slot
+#define SPI_MOSI MOSI
+#define SPI_SCK SCK
+#define SPI_MISO MISO
+#define SPI_CS 21
+#define SDCARD_CS SPI_CS
+#define SD_SPI_FREQUENCY 75000000U
+
+// TCA8418 keyboard
+#define I2C_NO_RESCAN
+#define KB_BL_PIN 46
+#define KB_INT 6
+#define CANNED_MESSAGE_MODULE_ENABLE 1
+
+// audio codec ES8311
+#define HAS_I2S
+#define DAC_I2S_BCK 11
+#define DAC_I2S_WS 18
+#define DAC_I2S_DOUT 45
+#define DAC_I2S_DIN 17
+#define DAC_I2S_MCLK 10
+
+// gyroscope BHI260AP
+#define HAS_BHI260AP
+
+// battery charger BQ25896
+#define HAS_PPM 1
+#define XPOWERS_CHIP_BQ25896
+
+// battery quality management BQ27220
+#define HAS_BQ27220 1
+#define BQ27220_I2C_SDA SDA
+#define BQ27220_I2C_SCL SCL
+#define BQ27220_DESIGN_CAPACITY 1500
+
+// NFC ST25R3916
+#define NFC_INT 5
+#define NFC_CS 39
+
+// External expansion chip XL9555
+#define USE_XL9555
+#define EXPANDS_DRV_EN (0)
+#define EXPANDS_AMP_EN (1)
+#define EXPANDS_KB_RST (2)
+#define EXPANDS_LORA_EN (3)
+#define EXPANDS_GPS_EN (4)
+#define EXPANDS_NFC_EN (5)
+#define EXPANDS_GPS_RST (7)
+#define EXPANDS_KB_EN (8)
+#define EXPANDS_GPIO_EN (9)
+#define EXPANDS_SD_DET (10)
+#define EXPANDS_SD_PULLEN (11)
+#define EXPANDS_SD_EN (12)
+
+// LoRa
+#define USE_SX1262
+#define USE_SX1268
+
+#define LORA_SCK 35
+#define LORA_MISO 33
+#define LORA_MOSI 34
+#define LORA_CS 36
+
+#define LORA_DIO0 -1 // a No connect on the SX1262 module
+#define LORA_RESET 47
+#define LORA_DIO1 14 // SX1262 IRQ
+#define LORA_DIO2 48 // SX1262 BUSY
+#define LORA_DIO3    // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled
+
+#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 3.0
diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini
index ecb1cbd67..476858ff5 100644
--- a/variants/esp32s3/unphone/platformio.ini
+++ b/variants/esp32s3/unphone/platformio.ini
@@ -32,6 +32,7 @@ lib_deps = ${esp32s3_base.lib_deps}
 
 
 [env:unphone-tft]
+board_level = extra
 extends = env:unphone
 build_flags =
   ${env:unphone.build_flags}
@@ -52,8 +53,6 @@ build_flags =
   -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_SCREEN_WIDTH=320
   -D LGFX_SCREEN_HEIGHT=480
   -D DISPLAY_SIZE=320x480 ; portrait mode
diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini
index 732b2a1d4..c47ab8bf1 100644
--- a/variants/native/portduino/platformio.ini
+++ b/variants/native/portduino/platformio.ini
@@ -1,10 +1,12 @@
 [native_base]
 extends = portduino_base
 build_flags = ${portduino_base.build_flags} -I variants/native/portduino
-  -D ARCH_PORTDUINO
   -I /usr/include
 board = cross_platform
-lib_deps = ${portduino_base.lib_deps}
+lib_deps = 
+  ${portduino_base.lib_deps}
+  melopero/Melopero RV3028@^1.1.0
+
 build_src_filter = ${portduino_base.build_src_filter}
 
 [env:native]
@@ -26,7 +28,6 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
   -D USE_X11=1
   -D HAS_TFT=1
   -D HAS_SCREEN=1
-
   -D LV_CACHE_DEF_SIZE=6291456
   -D LV_BUILD_TEST=0
   -D LV_USE_LIBINPUT=1
@@ -42,6 +43,25 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
 build_src_filter =
   ${native_base.build_src_filter}
 
+[env:native-sdl]
+extends = native_base
+build_type = release
+lib_deps =
+  ${env.lib_deps}
+  ${networking_base.lib_deps}
+  ${radiolib_base.lib_deps}
+  ${environmental_base.lib_deps}
+  # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
+  rweather/Crypto@0.4.0
+  # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
+  https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
+  # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
+	adafruit/Adafruit seesaw Library@1.7.9
+  https://github.com/jp-bennett/LovyanGFX/archive/7458f84a126c1f8fdc7b038074f71be903f6e4c0.zip
+build_flags = ${native_base.build_flags}
+  !pkg-config --cflags --libs sdl2 --silence-errors || :
+  -D LGFX_SDL=1
+
 [env:native-fb]
 extends = native_base
 build_type = release
diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h
index ce7dbd865..a7ca865be 100644
--- a/variants/native/portduino/variant.h
+++ b/variants/native/portduino/variant.h
@@ -4,4 +4,7 @@
 #define CANNED_MESSAGE_MODULE_ENABLE 1
 #define HAS_GPS 1
 #define MAX_RX_TOPHONE settingsMap[maxtophone]
-#define MAX_NUM_NODES settingsMap[maxnodes]
\ No newline at end of file
+#define MAX_NUM_NODES settingsMap[maxnodes]
+
+// RAK12002 RTC Module
+#define RV3028_RTC (uint8_t)0b1010010
\ No newline at end of file
diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
index 72ac6320d..5c1047aae 100644
--- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
+++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
@@ -1,6 +1,7 @@
 ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921
 [env:gat562_mesh_trial_tracker]
 extends = nrf52840_base
+board_level = extra
 board = gat562_mesh_trial_tracker
 board_check = true
 build_flags = ${nrf52840_base.build_flags}
diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h
index f4f0baf13..7e82733aa 100644
--- a/variants/nrf52840/heltec_mesh_node_t114/variant.h
+++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h
@@ -208,7 +208,7 @@ No longer populated on PCB
 #undef AREF_VOLTAGE
 #define AREF_VOLTAGE 3.0
 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0
-#define ADC_MULTIPLIER (4.90F)
+#define ADC_MULTIPLIER (4.916F)
 
 #define HAS_RTC 0
 #ifdef __cplusplus
diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini
new file mode 100644
index 000000000..65d26dc40
--- /dev/null
+++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini
@@ -0,0 +1,19 @@
+; First prototype nrf52840/sx1262 device
+[env:heltec-mesh-solar]
+extends = nrf52840_base
+board = heltec_mesh_solar
+board_level = pr
+debug_tool = jlink
+
+# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling.
+build_flags = ${nrf52840_base.build_flags}
+  -Ivariants/nrf52840/heltec_mesh_solar
+  -DGPS_POWER_TOGGLE
+  -DHELTEC_MESH_SOLAR
+
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar>
+lib_deps = 
+  ${nrf52840_base.lib_deps}
+  https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip
+  lewisxhe/PCF8563_Library@^1.0.1
+  ArduinoJson@6.21.4
diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp
new file mode 100644
index 000000000..8236d7cf4
--- /dev/null
+++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp
@@ -0,0 +1,36 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+#include "nrf.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+    // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
+    0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+
+    // P1
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+
+void initVariant()
+{
+  pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT);
+}
diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h
new file mode 100644
index 000000000..33c2b2556
--- /dev/null
+++ b/variants/nrf52840/heltec_mesh_solar/variant.h
@@ -0,0 +1,157 @@
+/*
+ Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+ Copyright (c) 2016 Sandeep Mistry All right reserved.
+ Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Lesser General Public License for more details.
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef _VARIANT_HELTEC_NRF_
+#define _VARIANT_HELTEC_NRF_
+/** Master clock frequency */
+#define VARIANT_MCK (64000000ul)
+
+#define USE_LFXO // Board uses 32khz crystal for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (1)
+#define NUM_ANALOG_OUTPUTS (0)
+
+
+#define PIN_LED1 (0 + 12) // green (confirmed on 1.0 board)
+#define LED_BLUE PIN_LED1 // fake for bluefruit library
+#define LED_GREEN PIN_LED1
+#define LED_BUILTIN LED_GREEN
+#define LED_STATE_ON 0  // State when LED is lit
+
+#define HAS_NEOPIXEL                         // Enable the use of neopixels
+#define NEOPIXEL_COUNT 1                     // How many neopixels are connected
+#define NEOPIXEL_DATA (32+15)                // gpio pin used to send data to the neopixels
+#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use
+
+/*
+ * Buttons
+ */
+#define PIN_BUTTON1 (32 + 10)
+// #define PIN_BUTTON2 (0 + 18)      // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular
+// GPIO
+
+/*
+No longer populated on PCB
+*/
+#define PIN_SERIAL2_RX (0 + 9)
+#define PIN_SERIAL2_TX (0 + 10)
+//  #define PIN_SERIAL2_EN (0 + 17)
+
+/*
+ * I2C
+ */
+
+#define WIRE_INTERFACES_COUNT 2
+
+// I2C bus 0
+// Routed to footprint for PCF8563TS RTC
+// Not populated on T114 V1, maybe in future?
+#define PIN_WIRE_SDA (0 + 6) // P0.26
+#define PIN_WIRE_SCL (0 + 26) // P0.26
+
+// I2C bus 1
+// Available on header pins, for general use
+#define PIN_WIRE1_SDA (0 + 30) // P0.30
+#define PIN_WIRE1_SCL (0 + 5) // P0.13
+
+/*
+ * Lora radio
+ */
+
+#define USE_SX1262
+// #define USE_SX1268
+#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead
+#define LORA_CS  (0 + 24)
+#define SX126X_DIO1 (0 + 20)
+// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching
+// #define SX1262_DIO3 (0 + 21)
+// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the
+//    main
+// CPU?
+#define SX126X_BUSY  (0 + 17)
+#define SX126X_RESET (0 + 25)
+// Not really an E22 but TTGO seems to be trying to clone that
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+
+/*
+ * GPS pins
+ */
+
+#define GPS_L76K
+
+// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K
+// #define GPS_RESET_MODE LOW
+// #define PIN_GPS_EN (21)
+#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing
+#define VEXT_ON_VALUE HIGH
+// #define GPS_EN_ACTIVE HIGH
+#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake
+#define PIN_GPS_PPS (32 + 4)
+// Seems to be missing on this new board
+// #define PIN_GPS_PPS (32 + 4)  // Pulse per second input from the GPS
+#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU
+#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS
+
+#define GPS_THREAD_INTERVAL 50
+
+#define PIN_SERIAL1_RX GPS_TX_PIN
+#define PIN_SERIAL1_TX GPS_RX_PIN
+
+/*
+ * SPI Interfaces
+ */
+#define SPI_INTERFACES_COUNT 1
+
+// For LORA, spi 0
+#define PIN_SPI_MISO (0 + 23)
+#define PIN_SPI_MOSI (0 + 22)
+#define PIN_SPI_SCK  (0 + 19)
+
+// #define PIN_PWR_EN (0 + 6)
+
+// To debug via the segger JLINK console rather than the CDC-ACM serial device
+// #define USE_SEGGER
+
+#define BQ4050_SDA_PIN                      (32+1) // I2C data line pin
+#define BQ4050_SCL_PIN                      (32+0) // I2C clock line pin
+#define BQ4050_EMERGENCY_SHUTDOWN_PIN       (32+3) // Emergency shutdown pin
+
+#define HAS_RTC 0
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif
diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini
index 8216a704a..466362242 100644
--- a/variants/nrf52840/meshlink/platformio.ini
+++ b/variants/nrf52840/meshlink/platformio.ini
@@ -4,6 +4,7 @@
 [env:meshlink]
 extends = nrf52840_base
 board = meshlink
+board_level = extra
 ;board_check = true
 build_flags = ${nrf52840_base.build_flags}
   -I variants/nrf52840/meshlink
diff --git a/variants/nrf52840/meshlink_eink/platformio.ini b/variants/nrf52840/meshlink_eink/platformio.ini
index a48a9e695..af5a0040e 100644
--- a/variants/nrf52840/meshlink_eink/platformio.ini
+++ b/variants/nrf52840/meshlink_eink/platformio.ini
@@ -4,6 +4,7 @@
 [env:meshlink_eink]
 extends = nrf52840_base
 board = meshlink
+board_level = extra
 ;board_check = true
 build_flags = ${nrf52840_base.build_flags}
   -I variants/nrf52840/meshlink_eink
diff --git a/variants/nrf52840/meshtiny/platformio.ini b/variants/nrf52840/meshtiny/platformio.ini
new file mode 100644
index 000000000..ef744a1c3
--- /dev/null
+++ b/variants/nrf52840/meshtiny/platformio.ini
@@ -0,0 +1,19 @@
+; MeshTiny - Custom device based on GAT562 with encoder and buzzer support
+[env:meshtiny]
+extends = nrf52840_base
+board = meshtiny
+board_level = extra
+build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHTINY
+  -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely.
+  -DRADIOLIB_EXCLUDE_SX128X=1
+  -DRADIOLIB_EXCLUDE_SX127X=1
+  -DRADIOLIB_EXCLUDE_LR11X0=1
+  -D INPUTDRIVER_ENCODER_TYPE=2
+  -D INPUTDRIVER_ENCODER_UP=4
+  -D INPUTDRIVER_ENCODER_DOWN=26
+  -D INPUTDRIVER_ENCODER_BTN=28
+  -D USE_PIN_BUZZER=PIN_BUZZER
+  -D MESHTASTIC_EXCLUDE_GPS=1
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny>
+lib_deps = 
+  ${nrf52840_base.lib_deps}
diff --git a/variants/nrf52840/meshtiny/variant.cpp b/variants/nrf52840/meshtiny/variant.cpp
new file mode 100644
index 000000000..2e8b00e4b
--- /dev/null
+++ b/variants/nrf52840/meshtiny/variant.cpp
@@ -0,0 +1,54 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+#include "nrf.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+    // P0
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+
+    // P1
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+
+void initVariant()
+{
+    // LED1 & LED2
+    pinMode(PIN_LED1, OUTPUT);
+    ledOff(PIN_LED1);
+
+    pinMode(PIN_LED2, OUTPUT);
+    ledOff(PIN_LED2);
+
+    // 3V3 Power Rail
+    pinMode(PIN_3V3_EN, OUTPUT);
+    digitalWrite(PIN_3V3_EN, HIGH);
+
+    // Initialize Encoder pins
+    pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP);
+    pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP);
+    pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP);
+
+    // Initialize Buzzer pin
+    pinMode(PIN_BUZZER, OUTPUT);
+    digitalWrite(PIN_BUZZER, LOW);
+}
diff --git a/variants/nrf52840/meshtiny/variant.h b/variants/nrf52840/meshtiny/variant.h
new file mode 100644
index 000000000..83ad4c5b9
--- /dev/null
+++ b/variants/nrf52840/meshtiny/variant.h
@@ -0,0 +1,199 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef _VARIANT_MESHTINY_
+#define _VARIANT_MESHTINY_
+
+#define MESHTINY
+
+// #define RAK4630
+
+/** Master clock frequency */
+#define VARIANT_MCK (64000000ul)
+
+#define USE_LFXO // Board uses 32khz crystal for LF
+// define USE_LFRC    // Board uses RC for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (6)
+#define NUM_ANALOG_OUTPUTS (0)
+
+// LEDs
+#define PIN_LED1 (35)
+#define PIN_LED2 (36)
+
+#define LED_BUILTIN PIN_LED1
+#define LED_CONN PIN_LED2
+
+#define LED_GREEN PIN_LED1
+#define LED_BLUE PIN_LED2
+
+#define LED_STATE_ON 1 // State when LED is litted
+
+/*
+ * Encoder
+ */
+#define INPUTDRIVER_ENCODER_TYPE 2
+#define INPUTDRIVER_ENCODER_UP 26
+#define INPUTDRIVER_ENCODER_DOWN 4
+#define INPUTDRIVER_ENCODER_BTN 28
+
+#define CANNED_MESSAGE_MODULE_ENABLE 1
+
+/*
+ * Buzzer - PWM
+ */
+#define PIN_BUZZER 30
+
+/*
+ * Buttons
+ */
+
+#define PIN_BUTTON1 9
+#define BUTTON_NEED_PULLUP
+#define PIN_BUTTON2 12
+#define PIN_BUTTON3 24
+#define PIN_BUTTON4 25
+
+/*
+ * Analog pins
+ */
+#define PIN_A0 (5)
+#define PIN_A1 (31)
+#define PIN_A2 (28)
+#define PIN_A3 (29)
+#define PIN_A4 (30)
+#define PIN_A5 (31)
+#define PIN_A6 (0xff)
+#define PIN_A7 (0xff)
+
+static const uint8_t A0 = PIN_A0;
+static const uint8_t A1 = PIN_A1;
+static const uint8_t A2 = PIN_A2;
+static const uint8_t A3 = PIN_A3;
+static const uint8_t A4 = PIN_A4;
+static const uint8_t A5 = PIN_A5;
+static const uint8_t A6 = PIN_A6;
+static const uint8_t A7 = PIN_A7;
+#define ADC_RESOLUTION 14
+
+// Other pins
+#define PIN_AREF (2)
+#define PIN_NFC1 (9)
+#define PIN_NFC2 (10)
+
+static const uint8_t AREF = PIN_AREF;
+
+/*
+ * Serial interfaces
+ */
+#define PIN_SERIAL1_RX (15)
+#define PIN_SERIAL1_TX (16)
+
+// Connected to Jlink CDC
+#define PIN_SERIAL2_RX (8)
+#define PIN_SERIAL2_TX (6)
+
+/*
+ * SPI Interfaces
+ */
+#define SPI_INTERFACES_COUNT 2
+
+#define PIN_SPI_MISO (45)
+#define PIN_SPI_MOSI (44)
+#define PIN_SPI_SCK (43)
+
+#define PIN_SPI1_MISO (29) // (0 + 29)
+#define PIN_SPI1_MOSI (30) // (0 + 30)
+#define PIN_SPI1_SCK (3)   // (0 + 3)
+
+static const uint8_t SS = 42;
+static const uint8_t MOSI = PIN_SPI_MOSI;
+static const uint8_t MISO = PIN_SPI_MISO;
+static const uint8_t SCK = PIN_SPI_SCK;
+
+#define HAS_SCREEN 1
+#define USE_SSD1306
+
+/*
+ * Wire Interfaces
+ */
+#define WIRE_INTERFACES_COUNT 1
+
+#define PIN_WIRE_SDA (13)
+#define PIN_WIRE_SCL (14)
+
+// QSPI Pins
+#define PIN_QSPI_SCK 3
+#define PIN_QSPI_CS 22  // Changed from 26 to avoid conflict with encoder
+#define PIN_QSPI_IO0 27 // Changed from 30 to avoid conflict with buzzer
+#define PIN_QSPI_IO1 29
+#define PIN_QSPI_IO2 21 // Changed from 28 to avoid conflict with encoder button
+#define PIN_QSPI_IO3 2
+
+// On-board QSPI Flash
+#define EXTERNAL_FLASH_DEVICES IS25LP080D
+#define EXTERNAL_FLASH_USE_QSPI
+
+#define USE_SX1262
+#define SX126X_CS (42)
+#define SX126X_DIO1 (47)
+#define SX126X_BUSY (46)
+#define SX126X_RESET (38)
+#define SX126X_POWER_EN (37)
+// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+
+// Testing USB detection
+#define NRF_APM
+
+#define PIN_3V3_EN (34)
+
+// Battery
+// The battery sense is hooked to pin A0 (5)
+#define BATTERY_PIN PIN_A0
+// and has 12 bit resolution
+#define BATTERY_SENSE_RESOLUTION_BITS 12
+#define BATTERY_SENSE_RESOLUTION 4096.0
+#undef AREF_VOLTAGE
+#define AREF_VOLTAGE 3.0
+#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
+#define ADC_MULTIPLIER 1.73
+
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif
diff --git a/variants/nrf52840/rak2560/platformio.ini b/variants/nrf52840/rak2560/platformio.ini
index 2b73aca03..edc648b9b 100644
--- a/variants/nrf52840/rak2560/platformio.ini
+++ b/variants/nrf52840/rak2560/platformio.ini
@@ -11,10 +11,10 @@ build_flags = ${nrf52840_base.build_flags}
   -DRADIOLIB_EXCLUDE_SX127X=1
   -DRADIOLIB_EXCLUDE_LR11X0=1
   -DHAS_RAKPROT=1 ; Define if RAk OneWireSerial is used (disables GPS)
-build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak2560> + +
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak2560> + + +
 lib_deps = 
   ${nrf52840_base.lib_deps}
-  ${networking_base.lib_deps}
+  ${nrf52_networking_base.lib_deps}
   melopero/Melopero RV3028@^1.1.0
   https://github.com/beegee-tokyo/RAK-OneWireSerial/archive/0.0.2.zip
 debug_tool = jlink
diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini
index 199e17570..6bf5f44cb 100644
--- a/variants/nrf52840/rak4631/platformio.ini
+++ b/variants/nrf52840/rak4631/platformio.ini
@@ -17,11 +17,12 @@ build_flags = ${nrf52840_base.build_flags}
 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631> + + +
 lib_deps = 
   ${nrf52840_base.lib_deps}
-  ${networking_base.lib_deps}
+  ${nrf52_networking_base.lib_deps}
   melopero/Melopero RV3028@^1.1.0
   https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
   rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
   beegee-tokyo/RAK12035_SoilMoisture@^1.0.4
+  # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
   https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
 
 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini
index a1c1b4610..4be8843a2 100644
--- a/variants/nrf52840/rak4631_eth_gw/platformio.ini
+++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini
@@ -24,14 +24,15 @@ build_flags = ${nrf52840_base.build_flags}
   -DMESHTASTIC_EXCLUDE_STOREFORWARD=1
   -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
   -DMESHTASTIC_EXCLUDE_WAYPOINT=1
-build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_eth_gw> + + +
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_eth_gw> + + + +
 lib_deps = 
   ${nrf52840_base.lib_deps}
   ${networking_base.lib_deps}
   melopero/Melopero RV3028@^1.1.0
   https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
   rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
-  https://github.com/meshtastic/RAK12034-BMX160/archive/4821355fb10390ba8557dc43ca29a023bcfbb9d9.zip
+  # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
+  https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
   bblanchon/ArduinoJson @ 6.21.4
 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
 ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds
diff --git a/variants/nrf52840/rak_wismeshtap/platformio.ini b/variants/nrf52840/rak_wismeshtap/platformio.ini
index f6ee8fd23..adf301537 100644
--- a/variants/nrf52840/rak_wismeshtap/platformio.ini
+++ b/variants/nrf52840/rak_wismeshtap/platformio.ini
@@ -18,7 +18,7 @@ build_flags = ${nrf52840_base.build_flags}
 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtap> + + +
 lib_deps = 
   ${nrf52840_base.lib_deps}
-  ${networking_base.lib_deps}
+  ${nrf52_networking_base.lib_deps}
   melopero/Melopero RV3028@^1.1.0
   https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
   rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h
index a32753343..7fb890303 100644
--- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h
+++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h
@@ -18,16 +18,9 @@
 
 // Shared NicheGraphics components
 // --------------------------------
-#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
-#include "graphics/niche/Drivers/EInk/GDEY0213B74.h"
+#include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h"
 #include "graphics/niche/Inputs/TwoButton.h"
 
-// Special case - fix T-Echo's touch button
-// ----------------------------------------
-// On a handful of T-Echos, LoRa TX triggers the capacitive touch
-// To avoid this, we lockout the button during TX
-#include "mesh/RadioLibInterface.h"
-
 void setupNicheGraphics()
 {
     using namespace NicheGraphics;
@@ -41,7 +34,7 @@ void setupNicheGraphics()
     // E-Ink Driver
     // -----------------------------
 
-    Drivers::EInk *driver = new Drivers::GDEY0213B74;
+    Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN;
     driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES);
 
     // InkHUD
@@ -53,8 +46,7 @@ void setupNicheGraphics()
     inkhud->setDriver(driver);
 
     // Set how many FAST updates per FULL update
-    // Set how unhealthy additional FAST updates beyond this number are
-    inkhud->setDisplayResilience(7, 1.5);
+    inkhud->setDisplayResilience(15);
 
     // Select fonts
     InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252;
@@ -62,16 +54,10 @@ void setupNicheGraphics()
     InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252;
 
     // Customize default settings
-    inkhud->persistence->settings.userTiles.maxCount = 2;              // Two applets side-by-side
-                                                                       // 270 degrees clockwise
+    inkhud->persistence->settings.rotation = 1;                        // 90 degrees clockwise
     inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
-    inkhud->persistence->settings.optionalMenuItems.backlight = true;  // Until proves capacitive button works by touching it
-    inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
-
-    // Setup backlight controller
-    // Note: AUX button attached further down
-    Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance();
-    backlight->setPin(PIN_EINK_EN);
+    inkhud->persistence->settings.userTiles.count = 1;    // One tile only by default, keep things simple for new users
+    inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side
 
     // Pick applets
     // Note: order of applets determines priority of "auto-show" feature
@@ -83,11 +69,9 @@ void setupNicheGraphics()
     inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);            // -
     inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0);         // Activated, no autoshow, default on tile 0
 
-    inkhud->persistence->settings.rotation = 1;
-    // inkhud->persistence->printSettings(&inkhud->persistence->settings);
     //  Start running InkHUD
     inkhud->begin();
-    // inkhud->persistence->printSettings(&inkhud->persistence->settings);
+
     //  Buttons
     //  --------------------------
 
diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini
index 52ff39d49..7f9eb0e2c 100644
--- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini
+++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini
@@ -1,17 +1,47 @@
 [env:seeed_wio_tracker_L1_eink]
 board = seeed_wio_tracker_L1
-extends = nrf52840_base, inkhud
+extends = nrf52840_base
 ;board_level = extra
 build_flags = ${nrf52840_base.build_flags}
-  ${inkhud.build_flags}
   -I variants/nrf52840/seeed_wio_tracker_L1_eink
   -D SEEED_WIO_TRACKER_L1_EINK
   -D SEEED_WIO_TRACKER_L1
   -I src/platform/nrf52/softdevice
   -I src/platform/nrf52/softdevice/nrf52
+  -DUSE_EINK
+  -DEINK_DISPLAY_MODEL=GxEPD2_213_B74
+  -DEINK_WIDTH=250
+  -DEINK_HEIGHT=122
+  -DUSE_EINK_DYNAMICDISPLAY            ; Enable Dynamic EInk
+  -DEINK_LIMIT_FASTREFRESH=10          ; How many consecutive fast-refreshes are permitted
+  -DEINK_LIMIT_RATE_BACKGROUND_SEC=30  ; Minimum interval between BACKGROUND updates
+  -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1   ; Minimum interval between RESPONSIVE updates
+;   -D EINK_LIMIT_GHOSTING_PX=2000      ; (Optional) How much image ghosting is tolerated
+  -DEINK_BACKGROUND_USES_FAST          ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
+  -DEINK_HASQUIRK_GHOSTING             ; Display model is identified as "prone to ghosting"
+  -DEINK_HASQUIRK_WEAKFASTREFRESH      ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight
 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld
-build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> ${inkhud.build_src_filter}
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink>
 lib_deps =
-  ${inkhud.lib_deps}
   ${nrf52840_base.lib_deps}
+  https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d
 debug_tool = jlink
+
+[env:seeed_wio_tracker_L1_eink-inkhud]
+board = seeed_wio_tracker_L1
+extends = nrf52840_base, inkhud
+build_flags = 
+  ${nrf52840_base.build_flags}
+  ${inkhud.build_flags}
+  -I variants/nrf52840/seeed_wio_tracker_L1_eink
+  -D SEEED_WIO_TRACKER_L1
+  -D BUTTON_PIN=D13
+board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld
+build_src_filter = 
+  ${nrf52_base.build_src_filter}
+  ${inkhud.build_src_filter}
+  +<../variants/nrf52840/seeed_wio_tracker_L1_eink>
+lib_deps =
+  ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space)
+  ${nrf52840_base.lib_deps}
+debug_tool = jlink
\ No newline at end of file
diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h
index 98a7b2c39..f33d200b1 100644
--- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h
+++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h
@@ -33,17 +33,10 @@
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 //  Button Configuration
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
-#ifdef BUTTON_PIN
-#undef BUTTON_PIN
-#endif
-
-#define BUTTON_PIN D13 // This is the Program Button
+#define CANCEL_BUTTON_PIN D13 // This is the Program Button
 // #define BUTTON_NEED_PULLUP   1
-#define BUTTON_ACTIVE_LOW true
-#define BUTTON_ACTIVE_PULLUP false
-
-#define BUTTON_PIN_TOUCH 13 // Touch button
+#define CANCEL_BUTTON_ACTIVE_LOW true
+#define CANCEL_BUTTON_ACTIVE_PULLUP false
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 //  Digital Pin Mapping (D0-D10)
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -116,7 +109,7 @@ static const uint8_t SCL = PIN_WIRE_SCL;
 #define PIN_EINK_SCLK 31
 #define PIN_EINK_MOSI 33
 #define PIN_EINK_EN 14   // unused
-#define PIN_SPI1_MISO 15 // unused
+#define PIN_SPI1_MISO -1 // 15 unused
 #define PIN_SPI1_MOSI PIN_EINK_MOSI
 #define PIN_SPI1_SCK PIN_EINK_SCLK
 
@@ -175,7 +168,17 @@ static const uint8_t SCL = PIN_WIRE_SCL;
 //  joystick
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
+// trackball
+#define HAS_TRACKBALL 1
+#define TB_UP 25
+#define TB_DOWN 26
+#define TB_LEFT 27
+#define TB_RIGHT 28
+#define TB_PRESS 29
+#define TB_DIRECTION FALLING
+
 #define CANNED_MESSAGE_MODULE_ENABLE 1
+#define CANNED_MESSAGE_ADD_CONFIRMATION 1
 
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 //  Compatibility Definitions
diff --git a/variants/nrf52840/t-echo-lite/platformio.ini b/variants/nrf52840/t-echo-lite/platformio.ini
new file mode 100644
index 000000000..68ae59dcb
--- /dev/null
+++ b/variants/nrf52840/t-echo-lite/platformio.ini
@@ -0,0 +1,25 @@
+; Using original screen class
+[env:t-echo-lite]
+extends = nrf52840_base
+board = t-echo
+board_check = true
+debug_tool = jlink
+
+# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling.
+build_flags = ${nrf52840_base.build_flags}
+  -Ivariants/nrf52840/t-echo-lite
+  -D T_ECHO_LITE
+  -D GPS_POWER_TOGGLE
+  -D EINK_DISPLAY_MODEL=GxEPD2_122_T61
+  -D EINK_WIDTH=192
+  -D EINK_HEIGHT=176
+  -D USE_EINK
+  -D USE_EINK_DYNAMICDISPLAY            ; Enable Dynamic EInk
+  -D EINK_LIMIT_FASTREFRESH=20          ; 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.
+
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-lite>
+lib_deps = 
+  ${nrf52840_base.lib_deps}
+  https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip
+;upload_protocol = fs
diff --git a/variants/nrf52840/t-echo-lite/variant.cpp b/variants/nrf52840/t-echo-lite/variant.cpp
new file mode 100644
index 000000000..cae079b74
--- /dev/null
+++ b/variants/nrf52840/t-echo-lite/variant.cpp
@@ -0,0 +1,44 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+#include "nrf.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+    // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
+    0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+
+    // P1
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+
+void initVariant()
+{
+    // LED1 & LED2
+    pinMode(PIN_LED1, OUTPUT);
+    ledOff(PIN_LED1);
+
+    pinMode(PIN_LED2, OUTPUT);
+    ledOff(PIN_LED2);
+
+    pinMode(PIN_LED3, OUTPUT);
+    ledOff(PIN_LED3);
+}
diff --git a/variants/nrf52840/t-echo-lite/variant.h b/variants/nrf52840/t-echo-lite/variant.h
new file mode 100644
index 000000000..2e2cdce72
--- /dev/null
+++ b/variants/nrf52840/t-echo-lite/variant.h
@@ -0,0 +1,207 @@
+/*
+ Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+ Copyright (c) 2016 Sandeep Mistry All right reserved.
+ Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Lesser General Public License for more details.
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef _VARIANT_TTGO_EINK_V1_0_
+#define _VARIANT_TTGO_EINK_V1_0_
+
+/** Master clock frequency */
+#define VARIANT_MCK (64000000ul)
+
+#define USE_LFXO // Board uses 32khz crystal for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (1)
+#define NUM_ANALOG_OUTPUTS (0)
+
+// LEDs
+#define PIN_LED1 (32 + 7) // Green LED
+#define PIN_LED2 (32 + 5) // Blue LED
+// Unused(by firmware) LEDs:
+#define PIN_LED3 (32 + 14) // Red LED inside, under the display.
+
+#define LED_RED PIN_LED3
+#define LED_BLUE PIN_LED2
+#define LED_GREEN PIN_LED1
+
+#define BLE_LED LED_BLUE
+#define BLE_LED_INVERTED 1
+#define LED_BUILTIN LED_GREEN
+#define LED_CONN LED_GREEN
+#define LED_STATE_ON 0 // State when LED is lit
+
+// Buttons
+#define PIN_BUTTON1 (0 + 24)
+#define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular GPIO
+
+#define BUTTON_CLICK_MS 400
+
+// Analog pins
+#define PIN_A0 (0 + 2) // Battery ADC
+
+#define BATTERY_PIN PIN_A0
+
+static const uint8_t A0 = PIN_A0;
+
+#define ADC_RESOLUTION 14
+
+#define ADC_CTRL (0 + 31)
+#define ADC_CTRL_ENABLED HIGH
+
+// NFC
+#define PIN_NFC1 (9)
+#define PIN_NFC2 (10)
+
+// Wire Interfaces
+#define WIRE_INTERFACES_COUNT 1
+
+#define PIN_WIRE_SDA (32 + 4)
+#define PIN_WIRE_SCL (32 + 2)
+
+/*
+ Internal, PCB PAD interrupt PIN. Currently not used. (Not built in my device)
+*/
+// #define PIN_IMU_INT (0 + 16) // Interrupt from the IMU, macro name correct?!
+
+// External serial flash ZD25WQ32CEIGR
+// QSPI Pins
+#define PIN_QSPI_SCK (0 + 4)
+#define PIN_QSPI_CS (0 + 12)
+#define PIN_QSPI_IO0 (0 + 6)  // MOSI if using two bit interface
+#define PIN_QSPI_IO1 (0 + 8)  // MISO if using two bit interface
+#define PIN_QSPI_IO2 (32 + 9) // WP if using two bit interface (i.e. not used)
+#define PIN_QSPI_IO3 (0 + 26) // HOLD if using two bit interface (i.e. not used)
+
+// On-board QSPI Flash
+#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR
+#define EXTERNAL_FLASH_USE_QSPI
+
+// Lora radio
+
+#define USE_SX1262
+// #define USE_SX1268 // currently only available with XS1262.
+#define SX126X_CS (0 + 11)
+#define SX126X_DIO1 (32 + 8)
+#define SX126X_DIO2 (0 + 5)
+#define SX126X_BUSY (0 + 14)
+#define SX126X_RESET (0 + 7)
+#define SX126X_RXEN (32 + 1)
+#define SX126X_TXEN (0 + 27)
+// #define TCXO_OPTIONAL
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+
+// eink display pins
+#define VEXT_ENABLE (32 + 12)
+#define VEXT_ON_VALUE LOW
+#define PIN_EINK_CS (0 + 22)
+#define PIN_EINK_BUSY (0 + 3)
+#define PIN_EINK_DC (0 + 21)
+#define PIN_EINK_RES (0 + 28)
+#define PIN_EINK_SCLK (0 + 19)
+#define PIN_EINK_MOSI (0 + 20)
+
+// Controls power 3V3 for all peripherals (eink + GPS + LoRa + Sensor)
+#define PIN_POWER_EN (0 + 30) // 3V3 POWER Enable
+
+#define PIN_SPI1_MISO (-1) // The display does not use MISO.
+#define PIN_SPI1_MOSI PIN_EINK_MOSI
+#define PIN_SPI1_SCK PIN_EINK_SCLK
+
+// GPS pins
+// #define GPS_DEBUG
+#define GPS_L76K
+#define GPS_BAUDRATE 9600
+#define HAS_GPS 1
+// #define PIN_GPS_REINIT (32 + 5) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K
+
+#define PIN_GPS_STANDBY (32 + 10) // An output to wake GPS, low means allow sleep, high means force wake
+// Seems to be missing on this new board
+#define PIN_GPS_PPS (0 + 29) // Pulse per second input from the GPS
+#define GPS_TX_PIN (32 + 15) // This is for bits going TOWARDS the CPU
+#define GPS_RX_PIN (32 + 13) // This is for bits going TOWARDS the GPS
+
+#define GPS_THREAD_INTERVAL 50
+
+#define PIN_SERIAL1_RX GPS_TX_PIN
+#define PIN_SERIAL1_TX GPS_RX_PIN
+
+// SPI Interfaces
+#define SPI_INTERFACES_COUNT 2
+
+// For LORA, SPI 0
+#define PIN_SPI_MISO (0 + 17)
+#define PIN_SPI_MOSI (0 + 15)
+#define PIN_SPI_SCK (0 + 13)
+
+// Battery
+// The battery sense is hooked to pin A0 (2)
+// it is defined in the analogue pin section of this file
+// and has 12 bit resolution
+#define BATTERY_SENSE_RESOLUTION_BITS 12
+#define BATTERY_SENSE_RESOLUTION 4096.0
+#undef AREF_VOLTAGE
+#define AREF_VOLTAGE 3.0
+#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
+#define ADC_MULTIPLIER (2.0F)
+
+// #define NO_EXT_GPIO 1
+// PINs back side
+// Batt & solar connector left up corner
+/*
+-------------------------------
+| VDDH, VBAT, 0.23, SCL , 1.06 |
+| GND , SDA , 0.09, 0.10, 0.25 |
+-------------------------------
+                        --------
+                        | VDDH |
+                        | GND  |
+                        | 1.13 | - Wake Up/standby
+                        | 1.15 | - PPS
+                        | 0.29 | - TX
+                        | 1.10 | - RX
+                        | 1.11 | - EN
+                        --------
+-------------------------------
+| 3V3 , GND , 0.16, 1.03, G_WU |  0.16 internal solder pad interrupt PIN,
+| G_EN, G_RX, G_TX, GND , PPS  |
+-------------------------------
+*/
+
+// To debug via the segger JLINK console rather than the CDC-ACM serial device
+// #define USE_SEGGER
+
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif
\ No newline at end of file
diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h
index 81b4ef3fb..403552ec0 100644
--- a/variants/nrf52840/tracker-t1000-e/variant.h
+++ b/variants/nrf52840/tracker-t1000-e/variant.h
@@ -124,8 +124,7 @@ extern "C" {
 #define GPS_RTC_INT (0 + 15)     // P0.15, normal is LOW, wake by HIGH
 #define GPS_RESETB_OUT (32 + 14) // P1.14, always input pull_up
 
-#define GPS_FIX_HOLD_TIME 15000 // ms
-#define BATTERY_PIN 2           // P0.02/AIN0, BAT_ADC
+#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC
 #define BATTERY_IMMUTABLE
 #define ADC_MULTIPLIER (2.0F)
 // P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE
diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h
index eb6a34d6c..02f8a20b2 100644
--- a/variants/nrf52840/wio-t1000-s/variant.h
+++ b/variants/nrf52840/wio-t1000-s/variant.h
@@ -123,7 +123,6 @@ extern "C" {
 #define GPS_RESETB_OUT (32 + 14) // P1.14, awlays input pull_up
 
 // #define GPS_THREAD_INTERVAL 50
-#define GPS_FIX_HOLD_TIME 15000 // ms
 
 #define BATTERY_PIN 2
 // #define ADC_CHANNEL ADC1_GPIO2_CHANNEL
@@ -157,4 +156,4 @@ extern "C" {
  *        Arduino objects - C++ only
  *----------------------------------------------------------------------------*/
 
-#endif // _VARIANT_WIO_SDK_WM1110_
\ No newline at end of file
+#endif // _VARIANT_WIO_SDK_WM1110_
diff --git a/variants/rp2040/challenger_2040_lora/pins_arduino.h b/variants/rp2040/challenger_2040_lora/pins_arduino.h
new file mode 100644
index 000000000..ac472c07e
--- /dev/null
+++ b/variants/rp2040/challenger_2040_lora/pins_arduino.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#define PINS_COUNT (25u)
+#define NUM_DIGITAL_PINS (25u)
+#define NUM_ANALOG_INPUTS (4u)
+#define NUM_ANALOG_OUTPUTS (0u)
+#define ADC_RESOLUTION (12u)
+
+// LEDs
+#define PIN_LED (24u)
+
+// Serial
+#define PIN_SERIAL1_TX (16u)
+#define PIN_SERIAL1_RX (17u)
+
+// SPI
+#define PIN_SPI0_MISO (20u)
+#define PIN_SPI0_MOSI (23u)
+#define PIN_SPI0_SCK (22u)
+#define PIN_SPI0_SS (21u)
+
+// Connected to LoRa module
+#define PIN_SPI1_MISO (12u)
+#define PIN_SPI1_MOSI (11u)
+#define PIN_SPI1_SCK (10u)
+#define PIN_SPI1_SS (9u)
+#define RFM95W_SS (9u)
+#define RFM95W_DIO0 (14u)
+#define RFM95W_DIO1 (15u)
+#define RFM95W_DIO2 (18u)
+#define RFM95W_RST (13u)
+#define RFM95W_SPI SPI1
+
+// Wire
+#define PIN_WIRE0_SDA (0u)
+#define PIN_WIRE0_SCL (1u)
+
+// Not pinned out
+#define PIN_WIRE1_SDA (31u)
+#define PIN_WIRE1_SCL (31u)
+#define PIN_SERIAL2_RX (31u)
+#define PIN_SERIAL2_TX (31u)
+
+#define SERIAL_HOWMANY (1u)
+#define SPI_HOWMANY (2u)
+#define WIRE_HOWMANY (1u)
+
+#define LED_BUILTIN PIN_LED
+
+static const uint8_t D0 = (16u);
+static const uint8_t D1 = (17u);
+static const uint8_t D2 = (20u);
+static const uint8_t D3 = (23u);
+static const uint8_t D4 = (22u);
+static const uint8_t D5 = (2u);
+static const uint8_t D6 = (3u);
+static const uint8_t D7 = (0u);
+static const uint8_t D8 = (1u);
+static const uint8_t D9 = (4u);
+static const uint8_t D10 = (5u);
+static const uint8_t D11 = (6u);
+static const uint8_t D12 = (7u);
+static const uint8_t D13 = (8u);
+static const uint8_t D14 = (13u);
+static const uint8_t D15 = (14u);
+static const uint8_t D16 = (15u);
+static const uint8_t D17 = (18u);
+static const uint8_t D18 = (24u);
+
+static const uint8_t A0 = (26u);
+static const uint8_t A1 = (27u);
+static const uint8_t A2 = (28u);
+static const uint8_t A3 = (29u);
+static const uint8_t A4 = (19u);
+static const uint8_t A5 = (21u);
+
+#ifndef SS
+#define SS PIN_SPI1_SS
+#endif
\ No newline at end of file
diff --git a/variants/rp2040/challenger_2040_lora/platformio.ini b/variants/rp2040/challenger_2040_lora/platformio.ini
new file mode 100644
index 000000000..4a709d650
--- /dev/null
+++ b/variants/rp2040/challenger_2040_lora/platformio.ini
@@ -0,0 +1,16 @@
+[env:challenger_2040_lora]
+extends = rp2040_base
+board = challenger_2040_lora
+board_level = extra
+upload_protocol = picotool
+# add our variants files to the include and src paths
+build_flags = 
+  ${rp2040_base.build_flags} 
+  -D PRIVATE_HW
+  -I variants/rp2040/challenger_2040_lora
+  -D DEBUG_RP2040_PORT=Serial
+  -D HW_SPI1_DEVICE
+lib_deps =
+  ${rp2040_base.lib_deps}
+debug_build_flags = ${rp2040_base.build_flags}
+debug_tool = cmsis-dap ; for e.g. Picotool
diff --git a/variants/rp2040/challenger_2040_lora/variant.h b/variants/rp2040/challenger_2040_lora/variant.h
new file mode 100644
index 000000000..552f90720
--- /dev/null
+++ b/variants/rp2040/challenger_2040_lora/variant.h
@@ -0,0 +1,39 @@
+// Define SS for compatibility with libraries expecting a default SPI chip select pin
+
+#define ARDUINO_ARCH_AVR
+
+#define EXT_NOTIFY_OUT 0xFFFFFFFF
+#define BUTTON_PIN 0xFFFFFFFF
+
+#define LED_PIN PIN_LED
+
+#define USE_RF95 // RFM95/SX127x
+
+#undef LORA_SCK
+#undef LORA_MISO
+#undef LORA_MOSI
+#undef LORA_CS
+
+// https://gitlab.com/invectorlabs/hw/challenger_rp2040_lora
+#define LORA_SCK 10  // Clock
+#define LORA_CS 9    // Chip Select
+#define LORA_MOSI 11 // Serial Data Out
+#define LORA_MISO 12 // Serial Data In
+
+#define LORA_RESET 13 // Reset
+
+#define LORA_DIO0 14         // DIO0
+#define LORA_DIO1 15         // DIO1
+#define LORA_DIO2 18         // DIO2
+#define LORA_DIO3 0xFFFFFFFF // Not connected
+#define LORA_DIO4 0xFFFFFFFF // Not connected
+#define LORA_DIO5 0xFFFFFFFF // Not connected
+
+#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/rp2040/ec_catsniffer/platformio.ini b/variants/rp2040/ec_catsniffer/platformio.ini
index acf19d757..b70eff6d7 100644
--- a/variants/rp2040/ec_catsniffer/platformio.ini
+++ b/variants/rp2040/ec_catsniffer/platformio.ini
@@ -1,6 +1,7 @@
 [env:catsniffer]
 extends = rp2040_base
 board = rpipico
+board_level = extra
 upload_protocol = picotool
 build_flags = 
   ${rp2040_base.build_flags}
diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini
index c011f62c9..290982405 100644
--- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini
+++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini
@@ -12,7 +12,5 @@ build_flags =
   -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
   -DMESHTASTIC_EXCLUDE_I2C=1
   -DMESHTASTIC_EXCLUDE_GPS=1
-  ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF
-  ;-DCFG_DEBUG
 
 upload_port = stlink
\ No newline at end of file
diff --git a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h
new file mode 100644
index 000000000..daf4aaaf9
--- /dev/null
+++ b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h
@@ -0,0 +1,9 @@
+// From E77-900M22S Product Specification
+// https://www.cdebyte.com/pdf-down.aspx?id=2963
+// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, RF_TXEN=1
+// RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel
+
+static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA7, PA6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[4] = {
+    {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
\ No newline at end of file
diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h
index 52801dac7..317f44489 100644
--- a/variants/stm32/CDEBYTE_E77-MBL/variant.h
+++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h
@@ -18,5 +18,4 @@ Do not expect a working Meshtastic device with this target.
 #define LED_PIN PB4 // LED1
 // #define LED_PIN PB3 // LED2
 #define LED_STATE_ON 1
-
 #endif
diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini
index 4f9edbb92..7fc6c7cba 100644
--- a/variants/stm32/rak3172/platformio.ini
+++ b/variants/stm32/rak3172/platformio.ini
@@ -6,10 +6,14 @@ board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem
 build_flags =
   ${stm32_base.build_flags}
   -Ivariants/stm32/rak3172
+  -DRAK3172
+  -DENABLE_HWSERIAL1
+  -DPIN_SERIAL1_RX=PB7
+  -DPIN_SERIAL1_TX=PB6
   -DPIN_WIRE_SDA=PA11
   -DPIN_WIRE_SCL=PA12
   -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
   -DMESHTASTIC_EXCLUDE_I2C=1
   -DMESHTASTIC_EXCLUDE_GPS=1
-  ;-DCFG_DEBUG
+
 upload_port = stlink
diff --git a/variants/stm32/rak3172/rfswitch.h b/variants/stm32/rak3172/rfswitch.h
new file mode 100644
index 000000000..2dced3c7c
--- /dev/null
+++ b/variants/stm32/rak3172/rfswitch.h
@@ -0,0 +1,7 @@
+// Pins from https://forum.rakwireless.com/t/rak3172-internal-schematic/4557/2
+// PB8, PC13
+
+static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[4] = {
+    {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
\ No newline at end of file
diff --git a/variants/stm32/wio-e5/rfswitch.h b/variants/stm32/wio-e5/rfswitch.h
new file mode 100644
index 000000000..3eadd9b5c
--- /dev/null
+++ b/variants/stm32/wio-e5/rfswitch.h
@@ -0,0 +1,8 @@
+/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/
+ * Wio-E5 module ONLY transmits through RFO_HP
+ * Receive: PA4=1, PA5=0
+ * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */
+static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[4] = {
+    {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
diff --git a/version.properties b/version.properties
index 7764a56c9..cbf8265d9 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
 [VERSION]  
 major = 2
 minor = 7
-build = 5
+build = 9