mirror of
https://github.com/meshtastic/firmware.git
synced 2025-04-24 09:26:52 +00:00
Merge branch 'master' into portexpander-keyboard
This commit is contained in:
commit
1b616e653a
@ -1,9 +1,10 @@
|
|||||||
|
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
|
||||||
|
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||||
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12
|
FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
# trunk-ignore(terrascan/AC_DOCKER_0002): Known terrascan issue
|
|
||||||
# trunk-ignore(hadolint/DL3008): Use latest version of packages
|
|
||||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
&& apt-get -y install --no-install-recommends \
|
&& apt-get -y install --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@ -27,9 +28,11 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|||||||
hwdata \
|
hwdata \
|
||||||
gpg \
|
gpg \
|
||||||
gnupg2 \
|
gnupg2 \
|
||||||
|
libusb-1.0-0-dev \
|
||||||
|
libi2c-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN pipx install platformio==6.1.15
|
RUN pipx install platformio
|
||||||
|
|
||||||
COPY 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules
|
COPY 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules
|
||||||
|
|
||||||
|
2
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
2
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: File a bug report
|
description: File a bug report
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug", "triage"]
|
labels: [bug, triage]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
2
.github/ISSUE_TEMPLATE/New Board.yml
vendored
2
.github/ISSUE_TEMPLATE/New Board.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: New Board
|
name: New Board
|
||||||
description: Request us to support new hardware
|
description: Request us to support new hardware
|
||||||
title: "[Board]: "
|
title: "[Board]: "
|
||||||
labels: ["enhancement", "triage"]
|
labels: [enhancement, triage]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Request a new feature
|
description: Request a new feature
|
||||||
title: "[Feature Request]: "
|
title: "[Feature Request]: "
|
||||||
labels: ["enhancement"]
|
labels: [enhancement]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
5
.github/actionlint.yaml
vendored
Normal file
5
.github/actionlint.yaml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Configuration related to self-hosted runner.
|
||||||
|
self-hosted-runner:
|
||||||
|
# Labels of self-hosted runner in array of strings.
|
||||||
|
labels:
|
||||||
|
- test-runner
|
2
.github/actions/build-variant/action.yml
vendored
2
.github/actions/build-variant/action.yml
vendored
@ -34,7 +34,7 @@ inputs:
|
|||||||
arch:
|
arch:
|
||||||
description: Processor arch name
|
description: Processor arch name
|
||||||
required: true
|
required: true
|
||||||
default: "esp32"
|
default: esp32
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
|
8
.github/actions/setup-base/action.yml
vendored
8
.github/actions/setup-base/action.yml
vendored
@ -1,13 +1,13 @@
|
|||||||
name: "Setup Build Base Composite Action"
|
name: Setup Build Base Composite Action
|
||||||
description: "Base build actions for Meshtastic Platform IO steps"
|
description: Base build actions for Meshtastic Platform IO steps
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: recursive
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
|
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@ -1,26 +1,27 @@
|
|||||||
|
#trunk-ignore-all(yamllint/quoted-strings): required by dependabot syntax check
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: devcontainer
|
directory: /.devcontainer
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
time: "05:00"
|
||||||
timezone: US/Pacific
|
timezone: US/Pacific
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
time: "05:00"
|
||||||
timezone: US/Pacific
|
timezone: US/Pacific
|
||||||
- package-ecosystem: gitsubmodule
|
- package-ecosystem: gitsubmodule
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
time: "05:00"
|
||||||
timezone: US/Pacific
|
timezone: US/Pacific
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /.github/workflows
|
directory: /.github/workflows
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
time: "05:00"
|
||||||
timezone: US/Pacific
|
timezone: US/Pacific
|
||||||
|
2
.github/workflows/build_nrf52.yml
vendored
2
.github/workflows/build_nrf52.yml
vendored
@ -7,6 +7,8 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-nrf52:
|
build-nrf52:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/build_rpi2040.yml
vendored
2
.github/workflows/build_rpi2040.yml
vendored
@ -7,6 +7,8 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-rpi2040:
|
build-rpi2040:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/build_stm32.yml
vendored
2
.github/workflows/build_stm32.yml
vendored
@ -7,6 +7,8 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-stm32:
|
build-stm32:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
35
.github/workflows/generate-userprefs.yml
vendored
35
.github/workflows/generate-userprefs.yml
vendored
@ -1,35 +0,0 @@
|
|||||||
name: Generate UsersPrefs JSON manifest
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- userPrefs.h
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
generate-userprefs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Clang
|
|
||||||
run: sudo apt-get install -y clang
|
|
||||||
|
|
||||||
- name: Install trunk
|
|
||||||
run: curl https://get.trunk.io -fsSL | bash
|
|
||||||
|
|
||||||
- name: Generate userPrefs.jsom
|
|
||||||
run: python3 ./bin/build-userprefs-json.py
|
|
||||||
|
|
||||||
- name: Trunk format json
|
|
||||||
run: trunk format userPrefs.json
|
|
||||||
|
|
||||||
- name: Commit userPrefs.json
|
|
||||||
run: |
|
|
||||||
git config --global user.email "actions@github.com"
|
|
||||||
git config --global user.name "GitHub Actions"
|
|
||||||
git add userPrefs.json
|
|
||||||
git commit -m "Update userPrefs.json"
|
|
||||||
git push
|
|
16
.github/workflows/main_matrix.yml
vendored
16
.github/workflows/main_matrix.yml
vendored
@ -135,10 +135,10 @@ jobs:
|
|||||||
build_location: local
|
build_location: local
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
package-pio-deps-native:
|
package-pio-deps-native-tft:
|
||||||
uses: ./.github/workflows/package_pio_deps.yml
|
uses: ./.github/workflows/package_pio_deps.yml
|
||||||
with:
|
with:
|
||||||
pio_env: native
|
pio_env: native-tft
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
test-native:
|
test-native:
|
||||||
@ -288,7 +288,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- gather-artifacts
|
- gather-artifacts
|
||||||
- build-debian-src
|
- build-debian-src
|
||||||
- package-pio-deps-native
|
- package-pio-deps-native-tft
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -324,10 +324,10 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output/debian-src
|
path: ./output/debian-src
|
||||||
|
|
||||||
- name: Download native pio deps
|
- name: Download `native-tft` pio deps
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: platformio-deps-native-${{ steps.version.outputs.long }}
|
pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output/pio-deps-native
|
path: ./output/pio-deps-native
|
||||||
|
|
||||||
@ -352,6 +352,12 @@ jobs:
|
|||||||
run: >-
|
run: >-
|
||||||
bin/bump_version.py
|
bin/bump_version.py
|
||||||
|
|
||||||
|
- name: Ensure debian deps are installed
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y --fix-missing
|
||||||
|
sudo apt-get install -y devscripts
|
||||||
|
|
||||||
- name: Update debian changelog
|
- name: Update debian changelog
|
||||||
run: >-
|
run: >-
|
||||||
debian/ci_changelog.sh
|
debian/ci_changelog.sh
|
||||||
|
8
.github/workflows/nightly.yml
vendored
8
.github/workflows/nightly.yml
vendored
@ -9,7 +9,7 @@ permissions: read-all
|
|||||||
jobs:
|
jobs:
|
||||||
trunk_check:
|
trunk_check:
|
||||||
name: Trunk Check and Upload
|
name: Trunk Check and Upload
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -21,8 +21,9 @@ jobs:
|
|||||||
trunk-token: ${{ secrets.TRUNK_TOKEN }}
|
trunk-token: ${{ secrets.TRUNK_TOKEN }}
|
||||||
|
|
||||||
trunk_upgrade:
|
trunk_upgrade:
|
||||||
|
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
||||||
name: Trunk Upgrade (PR)
|
name: Trunk Upgrade (PR)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # For trunk to create PRs
|
contents: write # For trunk to create PRs
|
||||||
pull-requests: write # For trunk to create PRs
|
pull-requests: write # For trunk to create PRs
|
||||||
@ -30,6 +31,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# See https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
|
||||||
- name: Trunk Upgrade
|
- name: Trunk Upgrade
|
||||||
uses: trunk-io/trunk-action/upgrade@v1
|
uses: trunk-io/trunk-action/upgrade@v1
|
||||||
|
with:
|
||||||
|
base: master
|
||||||
|
41
.github/workflows/sec_sast_flawfinder.yml
vendored
41
.github/workflows/sec_sast_flawfinder.yml
vendored
@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
name: Flawfinder Scan
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master, develop]
|
|
||||||
paths-ignore:
|
|
||||||
- "**.md"
|
|
||||||
- "version.properties"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
flawfinder:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Flawfinder
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# step 1
|
|
||||||
- name: clone application source code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# step 2
|
|
||||||
- name: flawfinder_scan
|
|
||||||
uses: david-a-wheeler/flawfinder@2.0.19
|
|
||||||
with:
|
|
||||||
arguments: "--sarif ./"
|
|
||||||
output: "flawfinder_report.sarif"
|
|
||||||
|
|
||||||
# step 3
|
|
||||||
- name: save report as pipeline artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: flawfinder_report.sarif
|
|
||||||
overwrite: true
|
|
||||||
path: flawfinder_report.sarif
|
|
||||||
|
|
||||||
# step 4
|
|
||||||
- name: publish code scanning alerts
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
with:
|
|
||||||
sarif_file: flawfinder_report.sarif
|
|
||||||
category: flawfinder
|
|
6
.github/workflows/sec_sast_semgrep_cron.yml
vendored
6
.github/workflows/sec_sast_semgrep_cron.yml
vendored
@ -3,10 +3,10 @@ name: Semgrep Full Scan
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 1 * * 6"
|
- cron: 0 1 * * 6
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
semgrep-full:
|
semgrep-full:
|
||||||
|
2
.github/workflows/sec_sast_semgrep_pull.yml
vendored
2
.github/workflows/sec_sast_semgrep_pull.yml
vendored
@ -2,6 +2,8 @@
|
|||||||
name: Semgrep Differential Scan
|
name: Semgrep Differential Scan
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
semgrep-diff:
|
semgrep-diff:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
2
.github/workflows/stale_bot.yml
vendored
2
.github/workflows/stale_bot.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Stale PR+Issues
|
- name: Stale PR+Issues
|
||||||
uses: actions/stale@v9.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
exempt-issue-labels: pinned,3.0
|
exempt-issue-labels: pinned,3.0
|
||||||
exempt-pr-labels: pinned,3.0
|
exempt-pr-labels: pinned,3.0
|
||||||
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -2,9 +2,11 @@ name: End to end tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # Run every day at midnight
|
- cron: 0 0 * * * # Run every day at midnight
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
native-tests:
|
native-tests:
|
||||||
uses: ./.github/workflows/test_native.yml
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
@ -11,7 +11,7 @@ permissions: read-all
|
|||||||
jobs:
|
jobs:
|
||||||
trunk_check:
|
trunk_check:
|
||||||
name: Trunk Code Quality Annotate
|
name: Trunk Code Quality Annotate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
checks: write # For trunk to post annotations
|
checks: write # For trunk to post annotations
|
||||||
contents: read # For repo checkout
|
contents: read # For repo checkout
|
4
.github/workflows/trunk_check.yml
vendored
4
.github/workflows/trunk_check.yml
vendored
@ -9,7 +9,7 @@ permissions: read-all
|
|||||||
jobs:
|
jobs:
|
||||||
trunk_check:
|
trunk_check:
|
||||||
name: Trunk Check Runner
|
name: Trunk Check Runner
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
checks: write # For trunk to post annotations
|
checks: write # For trunk to post annotations
|
||||||
contents: read # For repo checkout
|
contents: read # For repo checkout
|
||||||
@ -20,3 +20,5 @@ jobs:
|
|||||||
|
|
||||||
- name: Trunk Check
|
- name: Trunk Check
|
||||||
uses: trunk-io/trunk-action@v1
|
uses: trunk-io/trunk-action@v1
|
||||||
|
with:
|
||||||
|
save-annotations: true
|
||||||
|
6
.github/workflows/trunk_format_pr.yml
vendored
6
.github/workflows/trunk_format_pr.yml
vendored
@ -4,11 +4,15 @@ on:
|
|||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trunk-fmt:
|
trunk-fmt:
|
||||||
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt')
|
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
6
.github/workflows/update_protobufs.yml
vendored
6
.github/workflows/update_protobufs.yml
vendored
@ -1,10 +1,14 @@
|
|||||||
name: Update protobufs and regenerate classes
|
name: Update protobufs and regenerate classes
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-protobufs:
|
update-protobufs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,6 +1,9 @@
|
|||||||
[submodule "protobufs"]
|
[submodule "protobufs"]
|
||||||
path = protobufs
|
path = protobufs
|
||||||
url = https://github.com/meshtastic/protobufs.git
|
url = https://github.com/meshtastic/protobufs.git
|
||||||
|
[submodule "lib/device-ui"]
|
||||||
|
path = lib/device-ui
|
||||||
|
url = https://github.com/meshtastic/device-ui.git
|
||||||
[submodule "meshtestic"]
|
[submodule "meshtestic"]
|
||||||
path = meshtestic
|
path = meshtestic
|
||||||
url = https://github.com/meshtastic/meshTestic
|
url = https://github.com/meshtastic/meshTestic
|
||||||
|
39
.trunk/configs/.clang-tidy
Normal file
39
.trunk/configs/.clang-tidy
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
Checks: >-
|
||||||
|
bugprone-*,
|
||||||
|
cppcoreguidelines-*,
|
||||||
|
google-*,
|
||||||
|
misc-*,
|
||||||
|
modernize-*,
|
||||||
|
performance-*,
|
||||||
|
readability-*,
|
||||||
|
-bugprone-lambda-function-name,
|
||||||
|
-bugprone-reserved-identifier,
|
||||||
|
-cppcoreguidelines-avoid-goto,
|
||||||
|
-cppcoreguidelines-avoid-magic-numbers,
|
||||||
|
-cppcoreguidelines-avoid-non-const-global-variables,
|
||||||
|
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||||
|
-cppcoreguidelines-pro-type-vararg,
|
||||||
|
-google-readability-braces-around-statements,
|
||||||
|
-google-readability-function-size,
|
||||||
|
-misc-no-recursion,
|
||||||
|
-modernize-return-braced-init-list,
|
||||||
|
-modernize-use-nodiscard,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-performance-unnecessary-value-param,
|
||||||
|
-readability-magic-numbers,
|
||||||
|
|
||||||
|
CheckOptions:
|
||||||
|
- key: readability-function-cognitive-complexity.Threshold
|
||||||
|
value: 100
|
||||||
|
- key: readability-function-cognitive-complexity.IgnoreMacros
|
||||||
|
value: true
|
||||||
|
# Set naming conventions for your style below (there are dozens of naming settings possible):
|
||||||
|
# See https://clang.llvm.org/extra/clang-tidy/checks/readability/identifier-naming.html
|
||||||
|
# - key: readability-identifier-naming.ClassCase
|
||||||
|
# value: CamelCase
|
||||||
|
# - key: readability-identifier-naming.NamespaceCase
|
||||||
|
# value: lower_case
|
||||||
|
# - key: readability-identifier-naming.PrivateMemberSuffix
|
||||||
|
# value: _
|
||||||
|
# - key: readability-identifier-naming.StructCase
|
||||||
|
# value: CamelCase
|
@ -8,3 +8,4 @@ line_length: false
|
|||||||
spaces: false
|
spaces: false
|
||||||
url: false
|
url: false
|
||||||
whitespace: false
|
whitespace: false
|
||||||
|
headings: false
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
version: 0.1
|
version: 0.1
|
||||||
cli:
|
cli:
|
||||||
version: 1.22.8
|
version: 1.22.10
|
||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.6.6
|
ref: v1.6.7
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- prettier@3.4.2
|
- prettier@3.5.2
|
||||||
- trufflehog@3.86.1
|
- trufflehog@3.88.14
|
||||||
- yamllint@1.35.1
|
- yamllint@1.35.1
|
||||||
- bandit@1.8.0
|
- bandit@1.8.3
|
||||||
- checkov@3.2.334
|
- checkov@3.2.378
|
||||||
- terrascan@1.19.9
|
- terrascan@1.19.9
|
||||||
- trivy@0.58.0
|
- trivy@0.59.1
|
||||||
#- trufflehog@3.63.2-rc0
|
|
||||||
- taplo@0.9.3
|
- taplo@0.9.3
|
||||||
- ruff@0.8.3
|
- ruff@0.9.7
|
||||||
- isort@5.13.2
|
- isort@6.0.1
|
||||||
- markdownlint@0.43.0
|
- markdownlint@0.44.0
|
||||||
- oxipng@9.1.3
|
- oxipng@9.1.4
|
||||||
- svgo@3.3.2
|
- svgo@3.3.2
|
||||||
- actionlint@1.7.4
|
- actionlint@1.7.7
|
||||||
- flake8@7.1.1
|
- flake8@7.1.2
|
||||||
- hadolint@2.12.1-beta
|
- hadolint@2.12.1-beta
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- shellcheck@0.10.0
|
- shellcheck@0.10.0
|
||||||
- black@24.10.0
|
- black@25.1.0
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gitleaks@8.21.2
|
- gitleaks@8.24.0
|
||||||
- clang-format@16.0.3
|
- clang-format@16.0.3
|
||||||
#- prettier@3.3.3
|
- clang-tidy@16.0.3
|
||||||
ignore:
|
ignore:
|
||||||
- linters: [ALL]
|
- linters: [ALL]
|
||||||
paths:
|
paths:
|
||||||
|
21
Dockerfile
21
Dockerfile
@ -1,21 +1,23 @@
|
|||||||
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
|
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
|
||||||
# trunk-ignore-all(hadolint/DL3008): Use latest version of apt packages for buildchain
|
|
||||||
# trunk-ignore-all(trivy/DS002): We must run as root for this container
|
# trunk-ignore-all(trivy/DS002): We must run as root for this container
|
||||||
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
|
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
|
||||||
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
|
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
|
||||||
|
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||||
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM python:3.12-bookworm AS builder
|
FROM python:3.13-bookworm AS builder
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV TZ=Etc/UTC
|
ENV TZ=Etc/UTC
|
||||||
|
|
||||||
# Install Dependencies
|
# Install Dependencies
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y wget g++ zip git ca-certificates \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
|
wget g++ zip git ca-certificates \
|
||||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev \
|
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev \
|
||||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config && \
|
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config \
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
pip install --no-cache-dir -U platformio==6.1.16 && \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
WORKDIR /tmp/firmware
|
WORKDIR /tmp/firmware
|
||||||
@ -35,8 +37,9 @@ ENV TZ=Etc/UTC
|
|||||||
# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root
|
# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apt-get update && apt-get --no-install-recommends -y install libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 && \
|
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/* \
|
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
&& mkdir -p /etc/meshtasticd/ssl
|
&& mkdir -p /etc/meshtasticd/ssl
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
# trunk-ignore-all(trivy/DS002): We must run as root for this container
|
# trunk-ignore-all(trivy/DS002): We must run as root for this container
|
||||||
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
|
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
|
||||||
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
|
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
|
||||||
|
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
||||||
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM python:3.12-alpine3.21 AS builder
|
FROM python:3.13-alpine3.21 AS builder
|
||||||
|
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
RUN apk add bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \
|
RUN apk --no-cache add \
|
||||||
libusb-dev i2c-tools-dev openssl-dev pkgconf argp-standalone && \
|
bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||||
pip install --no-cache-dir -U platformio==6.1.16 && \
|
libusb-dev i2c-tools-dev openssl-dev pkgconf argp-standalone \
|
||||||
mkdir /tmp/firmware
|
&& rm -rf /var/cache/apk/* \
|
||||||
|
&& pip install --no-cache-dir -U platformio \
|
||||||
|
&& mkdir /tmp/firmware
|
||||||
|
|
||||||
WORKDIR /tmp/firmware
|
WORKDIR /tmp/firmware
|
||||||
COPY . /tmp/firmware
|
COPY . /tmp/firmware
|
||||||
@ -27,7 +31,9 @@ FROM alpine:3.21
|
|||||||
# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root
|
# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apk add libstdc++ libgpiod yaml-cpp libusb i2c-tools \
|
RUN apk --no-cache add \
|
||||||
|
libstdc++ libgpiod yaml-cpp libusb i2c-tools \
|
||||||
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
&& mkdir -p /etc/meshtasticd/ssl
|
&& mkdir -p /etc/meshtasticd/ssl
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
[esp32_base]
|
[esp32_base]
|
||||||
extends = arduino_base
|
extends = arduino_base
|
||||||
custom_esp32_kind = esp32
|
custom_esp32_kind = esp32
|
||||||
platform = platformio/espressif32@6.9.0
|
platform = platformio/espressif32@6.10.0
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
||||||
@ -37,6 +37,7 @@ build_flags =
|
|||||||
-DLIBPAX_ARDUINO
|
-DLIBPAX_ARDUINO
|
||||||
-DLIBPAX_WIFI
|
-DLIBPAX_WIFI
|
||||||
-DLIBPAX_BLE
|
-DLIBPAX_BLE
|
||||||
|
-DHAS_UDP_MULTICAST=1
|
||||||
;-DDEBUG_HEAP
|
;-DDEBUG_HEAP
|
||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
@ -45,9 +46,9 @@ lib_deps =
|
|||||||
${environmental_base.lib_deps}
|
${environmental_base.lib_deps}
|
||||||
${radiolib_base.lib_deps}
|
${radiolib_base.lib_deps}
|
||||||
https://github.com/meshtastic/esp32_https_server.git#23665b3adc080a311dcbb586ed5941b5f94d6ea2
|
https://github.com/meshtastic/esp32_https_server.git#23665b3adc080a311dcbb586ed5941b5f94d6ea2
|
||||||
h2zero/NimBLE-Arduino@^1.4.2
|
h2zero/NimBLE-Arduino@^1.4.3
|
||||||
https://github.com/dbinfrago/libpax.git#3cdc0371c375676a97967547f4065607d4c53fd1
|
https://github.com/dbinfrago/libpax.git#3cdc0371c375676a97967547f4065607d4c53fd1
|
||||||
lewisxhe/XPowersLib@^0.2.6
|
lewisxhe/XPowersLib@^0.2.7
|
||||||
https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f
|
https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f
|
||||||
rweather/Crypto@^0.4.0
|
rweather/Crypto@^0.4.0
|
||||||
|
|
||||||
@ -65,4 +66,4 @@ lib_ignore =
|
|||||||
|
|
||||||
; customize the partition table
|
; customize the partition table
|
||||||
; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables
|
; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables
|
||||||
board_build.partitions = partition-table.csv
|
board_build.partitions = partition-table.csv
|
||||||
|
@ -24,7 +24,7 @@ lib_deps =
|
|||||||
${networking_base.lib_deps}
|
${networking_base.lib_deps}
|
||||||
${environmental_base.lib_deps}
|
${environmental_base.lib_deps}
|
||||||
${radiolib_base.lib_deps}
|
${radiolib_base.lib_deps}
|
||||||
lewisxhe/XPowersLib@^0.2.6
|
lewisxhe/XPowersLib@^0.2.7
|
||||||
https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f
|
https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f
|
||||||
rweather/Crypto@^0.4.0
|
rweather/Crypto@^0.4.0
|
||||||
|
|
||||||
@ -38,4 +38,4 @@ lib_ignore =
|
|||||||
NonBlockingRTTTL
|
NonBlockingRTTTL
|
||||||
NimBLE-Arduino
|
NimBLE-Arduino
|
||||||
libpax
|
libpax
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@ platform = platformio/nordicnrf52@^10.7.0
|
|||||||
extends = arduino_base
|
extends = arduino_base
|
||||||
platform_packages =
|
platform_packages =
|
||||||
; our custom Git version until they merge our PR
|
; our custom Git version until they merge our PR
|
||||||
framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf
|
platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf
|
||||||
toolchain-gccarmnoneeabi@~1.90301.0
|
platformio/toolchain-gccarmnoneeabi@~1.90301.0
|
||||||
|
|
||||||
build_type = debug
|
build_type = debug
|
||||||
build_flags =
|
build_flags =
|
||||||
|
@ -18,6 +18,7 @@ build_src_filter =
|
|||||||
|
|
||||||
lib_ignore =
|
lib_ignore =
|
||||||
BluetoothOTA
|
BluetoothOTA
|
||||||
|
lvgl
|
||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${arduino_base.lib_deps}
|
${arduino_base.lib_deps}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
; Common settings for rp2040 Processor based targets
|
; Common settings for rp2040 Processor based targets
|
||||||
[rp2350_base]
|
[rp2350_base]
|
||||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#19e30129fb1428b823be585c787dcb4ac0d9014c ; For arduino-pico >=4.2.1
|
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 ; For arduino-pico >= 4.4.3
|
||||||
extends = arduino_base
|
extends = arduino_base
|
||||||
platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#6024e9a7e82a72e38dd90f42029ba3748835eb2e ; 4.3.0 with fix MDNS
|
platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#4.4.3
|
||||||
|
|
||||||
board_build.core = earlephilhower
|
board_build.core = earlephilhower
|
||||||
board_build.filesystem_size = 0.5m
|
board_build.filesystem_size = 0.5m
|
||||||
@ -10,7 +10,6 @@ build_flags =
|
|||||||
${arduino_base.build_flags} -Wno-unused-variable -Wcast-align
|
${arduino_base.build_flags} -Wno-unused-variable -Wcast-align
|
||||||
-Isrc/platform/rp2xx0
|
-Isrc/platform/rp2xx0
|
||||||
-D__PLAT_RP2350__
|
-D__PLAT_RP2350__
|
||||||
# -D _POSIX_THREADS
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<modules/esp32> -<platform/nrf52/> -<platform/stm32wl> -<mesh/eth/> -<mesh/wifi/> -<mesh/http/> -<mesh/raspihttp> -<platform/rp2xx0/pico_sleep> -<platform/rp2xx0/hardware_rosc>
|
${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<modules/esp32> -<platform/nrf52/> -<platform/stm32wl> -<mesh/eth/> -<mesh/wifi/> -<mesh/http/> -<mesh/raspihttp> -<platform/rp2xx0/pico_sleep> -<platform/rp2xx0/hardware_rosc>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[stm32_base]
|
[stm32_base]
|
||||||
extends = arduino_base
|
extends = arduino_base
|
||||||
platform = ststm32
|
platform = platformio/ststm32
|
||||||
platform_packages = platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32.git#ea74156acd823b6d14739f389e6cdc648f8ee36e
|
platform_packages = platformio/framework-arduinoststm32@^4.20900.0
|
||||||
|
|
||||||
build_type = release
|
build_type = release
|
||||||
|
|
||||||
|
@ -35,11 +35,11 @@ cp $SRCBIN $OUTDIR/$basename-update.bin
|
|||||||
|
|
||||||
echo "Building Filesystem for ESP32 targets"
|
echo "Building Filesystem for ESP32 targets"
|
||||||
pio run --environment $1 -t buildfs
|
pio run --environment $1 -t buildfs
|
||||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$VERSION.bin
|
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
||||||
# Remove webserver files from the filesystem and rebuild
|
# Remove webserver files from the filesystem and rebuild
|
||||||
ls -l data/static # Diagnostic list of files
|
ls -l data/static # Diagnostic list of files
|
||||||
rm -rf data/static
|
rm -rf data/static
|
||||||
pio run --environment $1 -t buildfs
|
pio run --environment $1 -t buildfs
|
||||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$VERSION.bin
|
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||||
cp bin/device-install.* $OUTDIR
|
cp bin/device-install.* $OUTDIR
|
||||||
cp bin/device-update.* $OUTDIR
|
cp bin/device-update.* $OUTDIR
|
@ -24,7 +24,7 @@ mkdir -p $OUTDIR/
|
|||||||
rm -r $OUTDIR/* || true
|
rm -r $OUTDIR/* || true
|
||||||
|
|
||||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||||
platformio pkg update --environment native || platformioFailed
|
pio pkg update --environment native || platformioFailed
|
||||||
pio run --environment native || platformioFailed
|
pio run --environment native || platformioFailed
|
||||||
cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||||
cp bin/native-install.* $OUTDIR
|
cp bin/native-install.* $OUTDIR
|
||||||
|
4
bin/config.d/MUI/X11_480x480.yaml
Normal file
4
bin/config.d/MUI/X11_480x480.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Display:
|
||||||
|
Panel: X11
|
||||||
|
Width: 480
|
||||||
|
Height: 480
|
10
bin/config.d/lora-starter-edition-sx1262-i2c.yaml
Normal file
10
bin/config.d/lora-starter-edition-sx1262-i2c.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# https://www.waveshare.com/core1262-868m.htm
|
||||||
|
# https://github.com/markbirss/lora-starter-edition-sx1262-i2c
|
||||||
|
Lora:
|
||||||
|
Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
CS: 8
|
||||||
|
IRQ: 22
|
||||||
|
Busy: 4
|
||||||
|
Reset: 18
|
10
bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml
Normal file
10
bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
|
||||||
|
# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter
|
||||||
|
Lora:
|
||||||
|
Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
CS: 21
|
||||||
|
IRQ: 16
|
||||||
|
Busy: 20
|
||||||
|
Reset: 18
|
@ -35,6 +35,11 @@ for subdir, dirs, files in os.walk(rootdir):
|
|||||||
outlist.append(section)
|
outlist.append(section)
|
||||||
else:
|
else:
|
||||||
outlist.append(section)
|
outlist.append(section)
|
||||||
|
# Add the TFT variants if the base variant is selected
|
||||||
|
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||||
|
outlist.append(section)
|
||||||
|
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||||
|
outlist.append(section)
|
||||||
if "board_check" in config[config[c].name]:
|
if "board_check" in config[config[c].name]:
|
||||||
if (config[config[c].name]["board_check"] == "true") & (
|
if (config[config[c].name]["board_check"] == "true") & (
|
||||||
"check" in options
|
"check" in options
|
||||||
@ -43,4 +48,4 @@ for subdir, dirs, files in os.walk(rootdir):
|
|||||||
if ("quick" in options) & (len(outlist) > 3):
|
if ("quick" in options) & (len(outlist) > 3):
|
||||||
print(json.dumps(random.sample(outlist, 3)))
|
print(json.dumps(random.sample(outlist, 3)))
|
||||||
else:
|
else:
|
||||||
print(json.dumps(outlist))
|
print(json.dumps(outlist))
|
1
debian/control
vendored
1
debian/control
vendored
@ -3,6 +3,7 @@ Section: misc
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: Austin Lane <vidplace7@gmail.com>
|
Maintainer: Austin Lane <vidplace7@gmail.com>
|
||||||
Build-Depends: debhelper-compat (= 13),
|
Build-Depends: debhelper-compat (= 13),
|
||||||
|
lsb-release,
|
||||||
tar,
|
tar,
|
||||||
gzip,
|
gzip,
|
||||||
platformio,
|
platformio,
|
||||||
|
9
debian/rules
vendored
9
debian/rules
vendored
@ -11,6 +11,15 @@ PIO_ENV:=\
|
|||||||
PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
|
PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
|
||||||
PLATFORMIO_PACKAGES_DIR=pio/packages
|
PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||||
|
|
||||||
|
# Raspbian armhf builds should be compatible with armv6-hardfloat
|
||||||
|
# https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags
|
||||||
|
ifneq (,$(findstring Raspbian,$(shell lsb_release -is)))
|
||||||
|
ifeq ($(DEB_BUILD_ARCH),armhf)
|
||||||
|
PIO_ENV+=\
|
||||||
|
PLATFORMIO_BUILD_FLAGS="-mfloat-abi=hard -mfpu=vfp -march=armv6zk"
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
override_dh_auto_build:
|
override_dh_auto_build:
|
||||||
# Extract tarballs within source deb
|
# Extract tarballs within source deb
|
||||||
tar -xf pio.tar
|
tar -xf pio.tar
|
||||||
|
1
lib/device-ui
Submodule
1
lib/device-ui
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 5c6156d2aa10d62cca3e57ffc117b934ef2fbffe
|
@ -1,3 +1,6 @@
|
|||||||
|
# trunk-ignore-all(bandit/B404): subprocess is used to call addr2line
|
||||||
|
# trunk-ignore-all(bandit/B603): subprocess is used to call addr2line
|
||||||
|
|
||||||
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -59,7 +59,7 @@ lib_deps =
|
|||||||
https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159
|
https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159
|
||||||
https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4
|
https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4
|
||||||
https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0
|
https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0
|
||||||
nanopb/Nanopb@0.4.9
|
nanopb/Nanopb@0.4.91
|
||||||
erriez/ErriezCRC32@1.0.1
|
erriez/ErriezCRC32@1.0.1
|
||||||
robtillaart/I2CKeyPad@0.5.0
|
robtillaart/I2CKeyPad@0.5.0
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 068646653e8375fc145988026ad242a3cf70f7ab
|
Subproject commit 2a3a67f0431926dc3f32a8b216d264daab09b9bf
|
105
src/BluetoothStatus.h
Normal file
105
src/BluetoothStatus.h
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Status.h"
|
||||||
|
#include "assert.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "meshUtils.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace meshtastic
|
||||||
|
{
|
||||||
|
|
||||||
|
// Describes the state of the Bluetooth connection
|
||||||
|
// Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code
|
||||||
|
class BluetoothStatus : public Status
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class ConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
PAIRING,
|
||||||
|
CONNECTED,
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
CallbackObserver<BluetoothStatus, const BluetoothStatus *> statusObserver =
|
||||||
|
CallbackObserver<BluetoothStatus, const BluetoothStatus *>(this, &BluetoothStatus::updateStatus);
|
||||||
|
|
||||||
|
ConnectionState state = ConnectionState::DISCONNECTED;
|
||||||
|
std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero
|
||||||
|
|
||||||
|
public:
|
||||||
|
BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; }
|
||||||
|
|
||||||
|
// New BluetoothStatus: connected or disconnected
|
||||||
|
BluetoothStatus(ConnectionState state)
|
||||||
|
{
|
||||||
|
assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey
|
||||||
|
statusType = STATUS_TYPE_BLUETOOTH;
|
||||||
|
this->state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New BluetoothStatus: pairing, with passkey
|
||||||
|
BluetoothStatus(std::string passkey) : Status()
|
||||||
|
{
|
||||||
|
statusType = STATUS_TYPE_BLUETOOTH;
|
||||||
|
this->state = ConnectionState::PAIRING;
|
||||||
|
this->passkey = passkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState getConnectionState() const { return this->state; }
|
||||||
|
|
||||||
|
std::string getPasskey() const
|
||||||
|
{
|
||||||
|
assert(state == ConnectionState::PAIRING);
|
||||||
|
return this->passkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
void observe(Observable<const BluetoothStatus *> *source) { statusObserver.observe(source); }
|
||||||
|
|
||||||
|
bool matches(const BluetoothStatus *newStatus) const
|
||||||
|
{
|
||||||
|
if (this->state == newStatus->getConnectionState()) {
|
||||||
|
// Same state: CONNECTED / DISCONNECTED
|
||||||
|
if (this->state != ConnectionState::PAIRING)
|
||||||
|
return true;
|
||||||
|
// Same state: PAIRING, and passkey matches
|
||||||
|
else if (this->getPasskey() == newStatus->getPasskey())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int updateStatus(const BluetoothStatus *newStatus)
|
||||||
|
{
|
||||||
|
// Has the status changed?
|
||||||
|
if (!matches(newStatus)) {
|
||||||
|
// Copy the members
|
||||||
|
state = newStatus->getConnectionState();
|
||||||
|
if (state == ConnectionState::PAIRING)
|
||||||
|
passkey = newStatus->getPasskey();
|
||||||
|
|
||||||
|
// Tell anyone interested that we have an update
|
||||||
|
onNewStatus.notifyObservers(this);
|
||||||
|
|
||||||
|
// Debug only:
|
||||||
|
switch (state) {
|
||||||
|
case ConnectionState::PAIRING:
|
||||||
|
LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str());
|
||||||
|
break;
|
||||||
|
case ConnectionState::CONNECTED:
|
||||||
|
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConnectionState::DISCONNECTED:
|
||||||
|
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace meshtastic
|
||||||
|
|
||||||
|
extern meshtastic::BluetoothStatus *bluetoothStatus;
|
@ -11,6 +11,7 @@
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
|
#include "sleep.h"
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#endif
|
#endif
|
||||||
@ -99,6 +100,13 @@ ButtonThread::ButtonThread() : OSThread("Button")
|
|||||||
userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click?
|
userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
// Register callbacks for before and after lightsleep
|
||||||
|
// Used to detach and reattach interrupts
|
||||||
|
lsObserver.observe(¬ifyLightSleep);
|
||||||
|
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||||
|
#endif
|
||||||
|
|
||||||
attachButtonInterrupts();
|
attachButtonInterrupts();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -320,6 +328,26 @@ void ButtonThread::detachButtonInterrupts()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
|
||||||
|
// Detach our class' interrupts before lightsleep
|
||||||
|
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||||
|
int ButtonThread::beforeLightSleep(void *unused)
|
||||||
|
{
|
||||||
|
detachButtonInterrupts();
|
||||||
|
return 0; // Indicates success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconfigure our interrupts
|
||||||
|
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||||
|
int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||||
|
{
|
||||||
|
attachButtonInterrupts();
|
||||||
|
return 0; // Indicates success
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watch a GPIO and if we get an IRQ, wake the main thread.
|
* Watch a GPIO and if we get an IRQ, wake the main thread.
|
||||||
* Use to add wake on button press
|
* Use to add wake on button press
|
||||||
|
@ -37,6 +37,12 @@ class ButtonThread : public concurrency::OSThread
|
|||||||
void detachButtonInterrupts();
|
void detachButtonInterrupts();
|
||||||
void storeClickCount();
|
void storeClickCount();
|
||||||
|
|
||||||
|
// Disconnect and reconnect interrupts for light sleep
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
int beforeLightSleep(void *unused);
|
||||||
|
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
||||||
static OneButton userButton; // Static - accessed from an interrupt
|
static OneButton userButton; // Static - accessed from an interrupt
|
||||||
@ -48,6 +54,14 @@ class ButtonThread : public concurrency::OSThread
|
|||||||
OneButton userButtonTouch;
|
OneButton userButtonTouch;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
// Get notified when lightsleep begins and ends
|
||||||
|
CallbackObserver<ButtonThread, void *> lsObserver =
|
||||||
|
CallbackObserver<ButtonThread, void *>(this, &ButtonThread::beforeLightSleep);
|
||||||
|
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||||
|
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t>(this, &ButtonThread::afterLightSleep);
|
||||||
|
#endif
|
||||||
|
|
||||||
// set during IRQ
|
// set during IRQ
|
||||||
static volatile ButtonEventType btnEvent;
|
static volatile ButtonEventType btnEvent;
|
||||||
|
|
||||||
|
@ -23,6 +23,10 @@ SPIClass SPI1(HSPI);
|
|||||||
#define SDHandler SPI
|
#define SDHandler SPI
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef SD_SPI_FREQUENCY
|
||||||
|
#define SD_SPI_FREQUENCY 4000000U
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // HAS_SDCARD
|
#endif // HAS_SDCARD
|
||||||
|
|
||||||
#if defined(ARCH_STM32WL)
|
#if defined(ARCH_STM32WL)
|
||||||
@ -361,8 +365,7 @@ void setupSDCard()
|
|||||||
#ifdef HAS_SDCARD
|
#ifdef HAS_SDCARD
|
||||||
concurrency::LockGuard g(spiLock);
|
concurrency::LockGuard g(spiLock);
|
||||||
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
|
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
|
||||||
|
if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) {
|
||||||
if (!SD.begin(SDCARD_CS, SDHandler)) {
|
|
||||||
LOG_DEBUG("No SD_MMC card detected");
|
LOG_DEBUG("No SD_MMC card detected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#define STATUS_TYPE_POWER 1
|
#define STATUS_TYPE_POWER 1
|
||||||
#define STATUS_TYPE_GPS 2
|
#define STATUS_TYPE_GPS 2
|
||||||
#define STATUS_TYPE_NODE 3
|
#define STATUS_TYPE_NODE 3
|
||||||
|
#define STATUS_TYPE_BLUETOOTH 4
|
||||||
|
|
||||||
namespace meshtastic
|
namespace meshtastic
|
||||||
{
|
{
|
||||||
|
@ -247,6 +247,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
logFoundDevice("BMP-388", (uint8_t)addr.address);
|
logFoundDevice("BMP-388", (uint8_t)addr.address);
|
||||||
type = BMP_3XX;
|
type = BMP_3XX;
|
||||||
break;
|
break;
|
||||||
|
case 0x60: // BMP-390 should be 0x60
|
||||||
|
logFoundDevice("BMP-390", (uint8_t)addr.address);
|
||||||
|
type = BMP_3XX;
|
||||||
|
break;
|
||||||
case 0x58: // BMP-280 should be 0x58
|
case 0x58: // BMP-280 should be 0x58
|
||||||
default:
|
default:
|
||||||
logFoundDevice("BMP-280", (uint8_t)addr.address);
|
logFoundDevice("BMP-280", (uint8_t)addr.address);
|
||||||
@ -524,4 +528,4 @@ void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address)
|
|||||||
{
|
{
|
||||||
LOG_INFO("%s found at address 0x%x", device, address);
|
LOG_INFO("%s found at address 0x%x", device, address);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
@ -6,28 +6,28 @@
|
|||||||
|
|
||||||
void d_writeCommand(uint8_t c)
|
void d_writeCommand(uint8_t c)
|
||||||
{
|
{
|
||||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||||
if (PIN_EINK_DC >= 0)
|
if (PIN_EINK_DC >= 0)
|
||||||
digitalWrite(PIN_EINK_DC, LOW);
|
digitalWrite(PIN_EINK_DC, LOW);
|
||||||
if (PIN_EINK_CS >= 0)
|
if (PIN_EINK_CS >= 0)
|
||||||
digitalWrite(PIN_EINK_CS, LOW);
|
digitalWrite(PIN_EINK_CS, LOW);
|
||||||
SPI.transfer(c);
|
SPI1.transfer(c);
|
||||||
if (PIN_EINK_CS >= 0)
|
if (PIN_EINK_CS >= 0)
|
||||||
digitalWrite(PIN_EINK_CS, HIGH);
|
digitalWrite(PIN_EINK_CS, HIGH);
|
||||||
if (PIN_EINK_DC >= 0)
|
if (PIN_EINK_DC >= 0)
|
||||||
digitalWrite(PIN_EINK_DC, HIGH);
|
digitalWrite(PIN_EINK_DC, HIGH);
|
||||||
SPI.endTransaction();
|
SPI1.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
void d_writeData(uint8_t d)
|
void d_writeData(uint8_t d)
|
||||||
{
|
{
|
||||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||||
if (PIN_EINK_CS >= 0)
|
if (PIN_EINK_CS >= 0)
|
||||||
digitalWrite(PIN_EINK_CS, LOW);
|
digitalWrite(PIN_EINK_CS, LOW);
|
||||||
SPI.transfer(d);
|
SPI1.transfer(d);
|
||||||
if (PIN_EINK_CS >= 0)
|
if (PIN_EINK_CS >= 0)
|
||||||
digitalWrite(PIN_EINK_CS, HIGH);
|
digitalWrite(PIN_EINK_CS, HIGH);
|
||||||
SPI.endTransaction();
|
SPI1.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned long d_waitWhileBusy(uint16_t busy_time)
|
unsigned long d_waitWhileBusy(uint16_t busy_time)
|
||||||
@ -53,7 +53,7 @@ unsigned long d_waitWhileBusy(uint16_t busy_time)
|
|||||||
|
|
||||||
void scanEInkDevice(void)
|
void scanEInkDevice(void)
|
||||||
{
|
{
|
||||||
SPI.begin();
|
SPI1.begin();
|
||||||
d_writeCommand(0x22);
|
d_writeCommand(0x22);
|
||||||
d_writeData(0x83);
|
d_writeData(0x83);
|
||||||
d_writeCommand(0x20);
|
d_writeCommand(0x20);
|
||||||
@ -62,6 +62,6 @@ void scanEInkDevice(void)
|
|||||||
LOG_DEBUG("EInk display found");
|
LOG_DEBUG("EInk display found");
|
||||||
else
|
else
|
||||||
LOG_DEBUG("EInk display not found");
|
LOG_DEBUG("EInk display not found");
|
||||||
SPI.end();
|
SPI1.end();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
100
src/gps/GPS.cpp
100
src/gps/GPS.cpp
@ -48,8 +48,6 @@ HardwareSerial *GPS::_serial_gps = nullptr;
|
|||||||
|
|
||||||
GPS *gps = nullptr;
|
GPS *gps = nullptr;
|
||||||
|
|
||||||
static const char *ACK_SUCCESS_MESSAGE = "Get ack success!";
|
|
||||||
|
|
||||||
static GPSUpdateScheduling scheduling;
|
static GPSUpdateScheduling scheduling;
|
||||||
|
|
||||||
/// Multiple GPS instances might use the same serial port (in sequence), but we can
|
/// Multiple GPS instances might use the same serial port (in sequence), but we can
|
||||||
@ -437,6 +435,10 @@ static const int serialSpeeds[3] = {9600, 115200, 38400};
|
|||||||
static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE};
|
static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef GPS_PROBETRIES
|
||||||
|
#define GPS_PROBETRIES 2
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Setup the GPS based on the model detected.
|
* @brief Setup the GPS based on the model detected.
|
||||||
* We detect the GPS by cycling through a set of baud rates, first common then rare.
|
* We detect the GPS by cycling through a set of baud rates, first common then rare.
|
||||||
@ -460,11 +462,7 @@ bool GPS::setup()
|
|||||||
digitalWrite(PIN_GPS_EN, HIGH);
|
digitalWrite(PIN_GPS_EN, HIGH);
|
||||||
delay(1000);
|
delay(1000);
|
||||||
#endif
|
#endif
|
||||||
#ifdef TRACKER_T1000_E
|
if (probeTries < GPS_PROBETRIES) {
|
||||||
if (probeTries < 5) {
|
|
||||||
#else
|
|
||||||
if (probeTries < 2) {
|
|
||||||
#endif
|
|
||||||
LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]);
|
LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]);
|
||||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
@ -475,11 +473,7 @@ bool GPS::setup()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Rare Serial Speeds
|
// Rare Serial Speeds
|
||||||
#ifdef TRACKER_T1000_E
|
if (probeTries == GPS_PROBETRIES) {
|
||||||
if (probeTries == 5) {
|
|
||||||
#else
|
|
||||||
if (probeTries == 2) {
|
|
||||||
#endif
|
|
||||||
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
|
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
|
||||||
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
||||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
@ -1043,14 +1037,6 @@ int32_t GPS::runOnce()
|
|||||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
return disable();
|
return disable();
|
||||||
}
|
}
|
||||||
// ONCE we will factory reset the GPS for bug #327
|
|
||||||
if (!devicestate.did_gps_reset) {
|
|
||||||
LOG_WARN("GPS FactoryReset requested");
|
|
||||||
if (gps->factoryReset()) { // If we don't succeed try again next time
|
|
||||||
devicestate.did_gps_reset = true;
|
|
||||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GPSInitFinished = true;
|
GPSInitFinished = true;
|
||||||
publishUpdate();
|
publishUpdate();
|
||||||
}
|
}
|
||||||
@ -1063,24 +1049,6 @@ int32_t GPS::runOnce()
|
|||||||
if (whileActive()) {
|
if (whileActive()) {
|
||||||
// if we have received valid NMEA claim we are connected
|
// if we have received valid NMEA claim we are connected
|
||||||
setConnected();
|
setConnected();
|
||||||
} else {
|
|
||||||
if ((config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) &&
|
|
||||||
IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9,
|
|
||||||
GNSS_MODEL_UBLOX10)) {
|
|
||||||
// reset the GPS on next bootup
|
|
||||||
if (devicestate.did_gps_reset && scheduling.elapsedSearchMs() > 60 * 1000UL && !hasFlow()) {
|
|
||||||
LOG_DEBUG("GPS is not found, try factory reset on next boot");
|
|
||||||
devicestate.did_gps_reset = false;
|
|
||||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
|
||||||
return disable(); // Stop the GPS thread as it can do nothing useful until next reboot.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// At least one GPS has a bad habit of losing its mind from time to time
|
|
||||||
if (rebootsSeen > 2) {
|
|
||||||
rebootsSeen = 0;
|
|
||||||
LOG_DEBUG("Would normally factoryReset()");
|
|
||||||
// gps->factoryReset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're due for an update, wake the GPS
|
// If we're due for an update, wake the GPS
|
||||||
@ -1415,62 +1383,6 @@ static int32_t toDegInt(RawDegrees d)
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GPS::factoryReset()
|
|
||||||
{
|
|
||||||
#ifdef PIN_GPS_REINIT
|
|
||||||
// The L76K GNSS on the T-Echo requires the RESET pin to be pulled LOW
|
|
||||||
pinMode(PIN_GPS_REINIT, OUTPUT);
|
|
||||||
digitalWrite(PIN_GPS_REINIT, 0);
|
|
||||||
delay(150); // The L76K datasheet calls for at least 100MS delay
|
|
||||||
digitalWrite(PIN_GPS_REINIT, 1);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
|
||||||
byte _message_reset1[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1C, 0xA2};
|
|
||||||
_serial_gps->write(_message_reset1, sizeof(_message_reset1));
|
|
||||||
if (getACK(0x05, 0x01, 10000)) {
|
|
||||||
LOG_DEBUG(ACK_SUCCESS_MESSAGE);
|
|
||||||
}
|
|
||||||
delay(100);
|
|
||||||
byte _message_reset2[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1B, 0xA1};
|
|
||||||
_serial_gps->write(_message_reset2, sizeof(_message_reset2));
|
|
||||||
if (getACK(0x05, 0x01, 10000)) {
|
|
||||||
LOG_DEBUG(ACK_SUCCESS_MESSAGE);
|
|
||||||
}
|
|
||||||
delay(100);
|
|
||||||
byte _message_reset3[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0x1D, 0xB3};
|
|
||||||
_serial_gps->write(_message_reset3, sizeof(_message_reset3));
|
|
||||||
if (getACK(0x05, 0x01, 10000)) {
|
|
||||||
LOG_DEBUG(ACK_SUCCESS_MESSAGE);
|
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_MTK) {
|
|
||||||
// send the CAS10 to perform a factory restart of the device (and other device that support PCAS statements)
|
|
||||||
LOG_INFO("GNSS Factory Reset via PCAS10,3");
|
|
||||||
_serial_gps->write("$PCAS10,3*1F\r\n");
|
|
||||||
delay(100);
|
|
||||||
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
|
|
||||||
LOG_INFO("Factory Reset via CAS-CFG-RST");
|
|
||||||
uint8_t msglen = makeCASPacket(0x06, 0x02, sizeof(_message_CAS_CFG_RST_FACTORY), _message_CAS_CFG_RST_FACTORY);
|
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
|
||||||
delay(100);
|
|
||||||
} else {
|
|
||||||
// fire this for good measure, if we have an L76B - won't harm other devices.
|
|
||||||
_serial_gps->write("$PMTK104*37\r\n");
|
|
||||||
// No PMTK_ACK for this command.
|
|
||||||
delay(100);
|
|
||||||
// send the UBLOX Factory Reset Command regardless of detect state, something is very wrong, just assume it's
|
|
||||||
// UBLOX. Factory Reset
|
|
||||||
byte _message_reset[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFB, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x17, 0x2B, 0x7E};
|
|
||||||
_serial_gps->write(_message_reset, sizeof(_message_reset));
|
|
||||||
}
|
|
||||||
delay(1000);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform any processing that should be done only while the GPS is awake and looking for a fix.
|
* 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
|
* Override this method to check for new locations
|
||||||
|
@ -101,8 +101,6 @@ class GPS : private concurrency::OSThread
|
|||||||
// Empty the input buffer as quickly as possible
|
// Empty the input buffer as quickly as possible
|
||||||
void clearBuffer();
|
void clearBuffer();
|
||||||
|
|
||||||
virtual bool factoryReset();
|
|
||||||
|
|
||||||
// Creates an instance of the GPS class.
|
// Creates an instance of the GPS class.
|
||||||
// Returns the new instance or null if the GPS is not present.
|
// Returns the new instance or null if the GPS is not present.
|
||||||
static GPS *createGps();
|
static GPS *createGps();
|
||||||
|
File diff suppressed because it is too large
Load Diff
110
src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp
Normal file
110
src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "./LatchingBacklight.h"
|
||||||
|
|
||||||
|
#include "assert.h"
|
||||||
|
|
||||||
|
#include "sleep.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Private constructor
|
||||||
|
// Called by getInstance
|
||||||
|
LatchingBacklight::LatchingBacklight()
|
||||||
|
{
|
||||||
|
// Attach the deep sleep callback
|
||||||
|
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get access to (or create) the singleton instance of this class
|
||||||
|
LatchingBacklight *LatchingBacklight::getInstance()
|
||||||
|
{
|
||||||
|
// Instantiate the class the first time this method is called
|
||||||
|
static LatchingBacklight *const singletonInstance = new LatchingBacklight;
|
||||||
|
|
||||||
|
return singletonInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which pin controls the backlight?
|
||||||
|
// Is the light active HIGH (default) or active LOW?
|
||||||
|
void LatchingBacklight::setPin(uint8_t pin, bool activeWhen)
|
||||||
|
{
|
||||||
|
this->pin = pin;
|
||||||
|
this->logicActive = activeWhen;
|
||||||
|
|
||||||
|
pinMode(pin, OUTPUT);
|
||||||
|
off(); // Explicit off seem required by T-Echo?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when device is shutting down
|
||||||
|
// Ensures the backlight is off
|
||||||
|
int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||||
|
{
|
||||||
|
// We shouldn't need to guard the block like this
|
||||||
|
// Contingency for:
|
||||||
|
// - settings corruption: settings.optionalMenuItems.backlight guards backlight code in MenuApplet
|
||||||
|
// - improper use in the future
|
||||||
|
if (pin != (uint8_t)-1) {
|
||||||
|
off();
|
||||||
|
pinMode(pin, INPUT); // High impedence - unnecessary?
|
||||||
|
} else
|
||||||
|
LOG_WARN("LatchingBacklight instantiated, but pin not set");
|
||||||
|
return 0; // Continue with deep sleep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn the backlight on *temporarily*
|
||||||
|
// This should be used for momentary illumination, such as while a button is held
|
||||||
|
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||||
|
void LatchingBacklight::peek()
|
||||||
|
{
|
||||||
|
assert(pin != (uint8_t)-1);
|
||||||
|
digitalWrite(pin, logicActive); // On
|
||||||
|
on = true;
|
||||||
|
latched = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn the backlight on, and keep it on
|
||||||
|
// This should be used when the backlight should remain active, even after user input ends
|
||||||
|
// e.g. when enabled via the menu
|
||||||
|
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||||
|
void LatchingBacklight::latch()
|
||||||
|
{
|
||||||
|
assert(pin != (uint8_t)-1);
|
||||||
|
|
||||||
|
// Blink if moving from peek to latch
|
||||||
|
// Indicates to user that the transition has taken place
|
||||||
|
if (on && !latched) {
|
||||||
|
digitalWrite(pin, !logicActive); // Off
|
||||||
|
delay(25);
|
||||||
|
digitalWrite(pin, logicActive); // On
|
||||||
|
delay(25);
|
||||||
|
digitalWrite(pin, !logicActive); // Off
|
||||||
|
delay(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
digitalWrite(pin, logicActive); // On
|
||||||
|
on = true;
|
||||||
|
latched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn the backlight off
|
||||||
|
// Suitable for ending both peek and latch
|
||||||
|
void LatchingBacklight::off()
|
||||||
|
{
|
||||||
|
assert(pin != (uint8_t)-1);
|
||||||
|
digitalWrite(pin, !logicActive); // Off
|
||||||
|
on = false;
|
||||||
|
latched = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LatchingBacklight::isOn()
|
||||||
|
{
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LatchingBacklight::isLatched()
|
||||||
|
{
|
||||||
|
return latched;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
50
src/graphics/niche/Drivers/Backlight/LatchingBacklight.h
Normal file
50
src/graphics/niche/Drivers/Backlight/LatchingBacklight.h
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Singleton class
|
||||||
|
On-demand control of a display's backlight, connected to a GPIO
|
||||||
|
Initial use case is control of T-Echo's frontlight, via the capacitive touch button
|
||||||
|
|
||||||
|
- momentary on
|
||||||
|
- latched on
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "Observer.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
|
||||||
|
class LatchingBacklight
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static LatchingBacklight *getInstance(); // Create or get the singleton instance
|
||||||
|
void setPin(uint8_t pin, bool activeWhen = HIGH);
|
||||||
|
|
||||||
|
int beforeDeepSleep(void *unused); // Callback for auto-shutoff
|
||||||
|
|
||||||
|
void peek(); // Backlight on temporarily, e.g. while button held
|
||||||
|
void latch(); // Backlight on permanently, e.g. toggled via menu
|
||||||
|
void off(); // Backlight off. Suitable for both peek and latch
|
||||||
|
|
||||||
|
bool isOn(); // Either peek or latch
|
||||||
|
bool isLatched();
|
||||||
|
|
||||||
|
private:
|
||||||
|
LatchingBacklight(); // Constructor made private: force use of getInstance
|
||||||
|
|
||||||
|
// Get notified when the system is shutting down
|
||||||
|
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||||
|
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||||
|
|
||||||
|
uint8_t pin = (uint8_t)-1;
|
||||||
|
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||||
|
|
||||||
|
bool on = false; // Is light on (either peek or latched)
|
||||||
|
bool latched = false; // Is light latched on
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
1
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp
Normal file
1
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "./DEPG0154BNS800.h"
|
34
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h
Normal file
34
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- DEPG0154BNS800
|
||||||
|
- Manufacturer: DKE
|
||||||
|
- Size: 1.54 inch
|
||||||
|
- Resolution: 152px x 152px
|
||||||
|
- Flex connector marking: FPC7525
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./SSD16XX.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
class DEPG0154BNS800 : public SSD16XX
|
||||||
|
{
|
||||||
|
// Display properties
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t width = 152;
|
||||||
|
static constexpr uint32_t height = 152;
|
||||||
|
static constexpr UpdateTypes supported = (UpdateTypes)(FULL);
|
||||||
|
|
||||||
|
public:
|
||||||
|
DEPG0154BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
120
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp
Normal file
120
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
#include "./DEPG0290BNS800.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Describes the operation performed when a "fast refresh" is performed
|
||||||
|
// Source: custom, with DEPG0150BNS810 as a reference
|
||||||
|
static const uint8_t LUT_FAST[] = {
|
||||||
|
// 1 2 3 4
|
||||||
|
0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels)
|
||||||
|
0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels)
|
||||||
|
0x00, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels)
|
||||||
|
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels)
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM
|
||||||
|
|
||||||
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1. Tap existing black pixels back into place
|
||||||
|
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. Move new pixels
|
||||||
|
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. New pixels, and also existing black pixels
|
||||||
|
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // 4. All pixels, then cooldown
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
|
||||||
|
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
// How strongly the pixels are pulled and pushed
|
||||||
|
void DEPG0290BNS800::configVoltages()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
// Listed as "typical" in datasheet
|
||||||
|
sendCommand(0x04);
|
||||||
|
sendData(0x41); // VSH1 15V
|
||||||
|
sendData(0x00); // VSH2 NA
|
||||||
|
sendData(0x32); // VSL -15V
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
// From OTP memory
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings about how the pixels are moved from old state to new state during a refresh
|
||||||
|
// - manually specified,
|
||||||
|
// - or with stored values from displays OTP memory
|
||||||
|
void DEPG0290BNS800::configWaveform()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
sendCommand(0x3C); // Border waveform:
|
||||||
|
sendData(0x60); // Actively hold screen border during update
|
||||||
|
|
||||||
|
sendCommand(0x32); // Write LUT register from MCU:
|
||||||
|
sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh)
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
// From OTP memory
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describes the sequence of events performed by the displays controller IC during a refresh
|
||||||
|
// Includes "power up", "load settings from memory", "update the pixels", etc
|
||||||
|
void DEPG0290BNS800::configUpdateSequence()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
sendCommand(0x22); // Set "update sequence"
|
||||||
|
sendData(0xCF); // Differential, use manually loaded waveform
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
sendCommand(0x22); // Set "update sequence"
|
||||||
|
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the refresh operation has been started,
|
||||||
|
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
|
||||||
|
// Only used when refresh is "async"
|
||||||
|
void DEPG0290BNS800::detachFromUpdate()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
return beginPolling(50, 450); // At least 450ms for fast refresh
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
return beginPolling(100, 3000); // At least 3 seconds for full refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For this display, we do not need to re-write the new image.
|
||||||
|
// We're overriding SSD16XX::finalizeUpdate to make this small optimization.
|
||||||
|
// The display does also work just fine with the generic SSD16XX method, though.
|
||||||
|
void DEPG0290BNS800::finalizeUpdate()
|
||||||
|
{
|
||||||
|
// Put a copy of the image into the "old memory".
|
||||||
|
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||||
|
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||||
|
if (updateType != FULL) {
|
||||||
|
// writeNewImage(); // Not required for this display
|
||||||
|
writeOldImage();
|
||||||
|
sendCommand(0x7F); // Terminate image write without update
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
42
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h
Normal file
42
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- DEPG0290BNS800
|
||||||
|
- Manufacturer: DKE
|
||||||
|
- Size: 2.9 inch
|
||||||
|
- Resolution: 128px x 296px
|
||||||
|
- Flex connector marking: FPC-7519 rev.b
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./SSD16XX.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
class DEPG0290BNS800 : public SSD16XX
|
||||||
|
{
|
||||||
|
// Display properties
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t width = 128;
|
||||||
|
static constexpr uint32_t height = 296;
|
||||||
|
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||||
|
|
||||||
|
public:
|
||||||
|
DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void configVoltages() override;
|
||||||
|
void configWaveform() override;
|
||||||
|
void configUpdateSequence() override;
|
||||||
|
void detachFromUpdate() override;
|
||||||
|
void finalizeUpdate() override; // Only overriden for a slight optimization
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
70
src/graphics/niche/Drivers/EInk/EInk.cpp
Normal file
70
src/graphics/niche/Drivers/EInk/EInk.cpp
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#include "./EInk.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants
|
||||||
|
EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported)
|
||||||
|
: concurrency::OSThread("E-Ink Driver"), width(width), height(height), supportedUpdateTypes(supported)
|
||||||
|
{
|
||||||
|
OSThread::disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by NicheGraphics implementations to check if a display supports a specific refresh operation.
|
||||||
|
// Whether or the update type is supported is specified in the constructor
|
||||||
|
bool EInk::supports(UpdateTypes type)
|
||||||
|
{
|
||||||
|
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.
|
||||||
|
if (supportedUpdateTypes & type)
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begins using the OSThread to detect when a display update is complete
|
||||||
|
// This allows the refresh operation to run "asynchronously".
|
||||||
|
// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin
|
||||||
|
// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes.
|
||||||
|
// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration",
|
||||||
|
// provided its isUpdateDone() override always returns true.
|
||||||
|
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
|
||||||
|
{
|
||||||
|
updateRunning = true;
|
||||||
|
updateBegunAt = millis();
|
||||||
|
pollingInterval = interval;
|
||||||
|
|
||||||
|
// To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take
|
||||||
|
// By default, expectedDuration is 0, and we'll start polling immediately
|
||||||
|
OSThread::setIntervalFromNow(expectedDuration);
|
||||||
|
OSThread::enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meshtastic's pseudo-threading layer
|
||||||
|
// We're using this as a timer, to periodically check if an update is complete
|
||||||
|
// This is what allows us to update the display asynchronously
|
||||||
|
int32_t EInk::runOnce()
|
||||||
|
{
|
||||||
|
if (!isUpdateDone())
|
||||||
|
return pollingInterval; // Poll again in a few ms
|
||||||
|
|
||||||
|
// If update done:
|
||||||
|
finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc
|
||||||
|
updateRunning = false; // Change what we report via EInk::busy()
|
||||||
|
return disable(); // Stop polling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for an in progress update to complete before continuing
|
||||||
|
// Run a normal (async) update first, *then* call await
|
||||||
|
void EInk::await()
|
||||||
|
{
|
||||||
|
// Stop our concurrency thread
|
||||||
|
OSThread::disable();
|
||||||
|
|
||||||
|
// Sit and block until the update is complete
|
||||||
|
while (updateRunning) {
|
||||||
|
runOnce();
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
56
src/graphics/niche/Drivers/EInk/EInk.h
Normal file
56
src/graphics/niche/Drivers/EInk/EInk.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Base class for E-Ink display drivers
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
#include <SPI.h>
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
|
||||||
|
class EInk : private concurrency::OSThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Different possible operations used to update an E-Ink display
|
||||||
|
// Some displays will not support all operations
|
||||||
|
// Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType)
|
||||||
|
enum UpdateTypes : uint8_t {
|
||||||
|
UNSPECIFIED = 0,
|
||||||
|
FULL = 1 << 0,
|
||||||
|
FAST = 1 << 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||||
|
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
|
||||||
|
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
|
||||||
|
void await(); // Wait for an in-progress update to complete before proceeding
|
||||||
|
bool supports(UpdateTypes type); // Can display perfom a certain update type
|
||||||
|
bool busy() { return updateRunning; } // Display able to update right now?
|
||||||
|
|
||||||
|
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
|
||||||
|
const uint16_t height;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished
|
||||||
|
virtual bool isUpdateDone() = 0; // Check once if update finished
|
||||||
|
virtual void finalizeUpdate() {} // Run any post-update code
|
||||||
|
|
||||||
|
private:
|
||||||
|
int32_t runOnce() override; // Repeated checking if update finished
|
||||||
|
|
||||||
|
const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class
|
||||||
|
bool updateRunning = false; // see EInk::busy()
|
||||||
|
uint32_t updateBegunAt; // For initial pause before polling for update completion
|
||||||
|
uint32_t pollingInterval; // How often to check if update complete (ms)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
61
src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp
Normal file
61
src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#include "./GDEY0154D67.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Map the display controller IC's output to the conected panel
|
||||||
|
void GDEY0154D67::configScanning()
|
||||||
|
{
|
||||||
|
// "Driver output control"
|
||||||
|
sendCommand(0x01);
|
||||||
|
sendData(0xC7);
|
||||||
|
sendData(0x00);
|
||||||
|
sendData(0x00);
|
||||||
|
|
||||||
|
// To-do: delete this method?
|
||||||
|
// Values set here might be redundant: C7, 00, 00 seems to be default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||||
|
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||||
|
// the controller IC's OTP memory, when the update procedure begins.
|
||||||
|
void GDEY0154D67::configWaveform()
|
||||||
|
{
|
||||||
|
sendCommand(0x3C); // Border waveform:
|
||||||
|
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||||
|
|
||||||
|
sendCommand(0x18); // Temperature sensor:
|
||||||
|
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||||
|
}
|
||||||
|
|
||||||
|
void GDEY0154D67::configUpdateSequence()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
sendCommand(0x22); // Set "update sequence"
|
||||||
|
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
sendCommand(0x22); // Set "update sequence"
|
||||||
|
sendData(0xF7); // Will load LUT from OTP memory
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the refresh operation has been started,
|
||||||
|
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
|
||||||
|
// Only used when refresh is "async"
|
||||||
|
void GDEY0154D67::detachFromUpdate()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
42
src/graphics/niche/Drivers/EInk/GDEY0154D67.h
Normal file
42
src/graphics/niche/Drivers/EInk/GDEY0154D67.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- GDEY0154D67
|
||||||
|
- Manufacturer: Goodisplay
|
||||||
|
- Size: 1.54 inch
|
||||||
|
- Resolution: 200px x 200px
|
||||||
|
- Flex connector marking: FPC-B001
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./SSD16XX.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
class GDEY0154D67 : public SSD16XX
|
||||||
|
{
|
||||||
|
// Display properties
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t width = 200;
|
||||||
|
static constexpr uint32_t height = 200;
|
||||||
|
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||||
|
|
||||||
|
public:
|
||||||
|
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void configScanning() override;
|
||||||
|
virtual void configWaveform() override;
|
||||||
|
virtual void configUpdateSequence() override;
|
||||||
|
void detachFromUpdate() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
301
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp
Normal file
301
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
#include "./LCMEN2R13EFC1.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Look up table: fast refresh, common electrode
|
||||||
|
static const uint8_t LUT_FAST_VCOMDC[] = {
|
||||||
|
0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01, //
|
||||||
|
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up table: fast refresh, pixels which remain white
|
||||||
|
static const uint8_t LUT_FAST_WW[] = {
|
||||||
|
0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01, //
|
||||||
|
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up table: fast refresh, pixel which change from black to white
|
||||||
|
static const uint8_t LUT_FAST_BW[] = {
|
||||||
|
0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01, //
|
||||||
|
0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up table: fash refresh, pixels which change from white to black
|
||||||
|
static const uint8_t LUT_FAST_WB[] = {
|
||||||
|
0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, //
|
||||||
|
0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, //
|
||||||
|
0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up table: fash refresh, pixels which remain black
|
||||||
|
static const uint8_t LUT_FAST_BB[] = {
|
||||||
|
0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01, //
|
||||||
|
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||||
|
};
|
||||||
|
|
||||||
|
LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported)
|
||||||
|
{
|
||||||
|
// Pre-calculate size of the image buffer, for convenience
|
||||||
|
|
||||||
|
// Determine the X dimension of the image buffer, in bytes.
|
||||||
|
// Along rows, pixels are stored 8 per byte.
|
||||||
|
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||||
|
bufferRowSize = ((width - 1) / 8) + 1;
|
||||||
|
|
||||||
|
// Total size of image buffer, in bytes.
|
||||||
|
bufferSize = bufferRowSize * height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||||
|
{
|
||||||
|
this->spi = spi;
|
||||||
|
this->pin_dc = pin_dc;
|
||||||
|
this->pin_cs = pin_cs;
|
||||||
|
this->pin_busy = pin_busy;
|
||||||
|
this->pin_rst = pin_rst;
|
||||||
|
|
||||||
|
pinMode(pin_dc, OUTPUT);
|
||||||
|
pinMode(pin_cs, OUTPUT);
|
||||||
|
pinMode(pin_busy, INPUT);
|
||||||
|
|
||||||
|
// Reset is active low, hold high
|
||||||
|
pinMode(pin_rst, INPUT_PULLUP);
|
||||||
|
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type)
|
||||||
|
{
|
||||||
|
this->updateType = type;
|
||||||
|
this->buffer = imageData;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
|
||||||
|
// Config
|
||||||
|
if (updateType == FULL)
|
||||||
|
configFull();
|
||||||
|
else
|
||||||
|
configFast();
|
||||||
|
|
||||||
|
// Transfer image data
|
||||||
|
if (updateType == FULL) {
|
||||||
|
writeNewImage();
|
||||||
|
writeOldImage();
|
||||||
|
} else {
|
||||||
|
writeNewImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommand(0x04); // Power on the panel voltage
|
||||||
|
wait();
|
||||||
|
|
||||||
|
sendCommand(0x12); // Begin executing the update
|
||||||
|
|
||||||
|
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||||
|
// For a blocking update, call await after update
|
||||||
|
detachFromUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::wait()
|
||||||
|
{
|
||||||
|
// Busy when LOW
|
||||||
|
while (digitalRead(pin_busy) == LOW)
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::reset()
|
||||||
|
{
|
||||||
|
pinMode(pin_rst, OUTPUT);
|
||||||
|
digitalWrite(pin_rst, LOW);
|
||||||
|
delay(10);
|
||||||
|
pinMode(pin_rst, INPUT_PULLUP);
|
||||||
|
wait();
|
||||||
|
|
||||||
|
sendCommand(0x12);
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::sendCommand(const uint8_t command)
|
||||||
|
{
|
||||||
|
spi->beginTransaction(spiSettings);
|
||||||
|
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||||
|
digitalWrite(pin_cs, LOW);
|
||||||
|
spi->transfer(command);
|
||||||
|
digitalWrite(pin_cs, HIGH);
|
||||||
|
digitalWrite(pin_dc, HIGH);
|
||||||
|
spi->endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::sendData(uint8_t data)
|
||||||
|
{
|
||||||
|
// spi->beginTransaction(spiSettings);
|
||||||
|
// digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||||
|
// digitalWrite(pin_cs, LOW);
|
||||||
|
// spi->transfer(data);
|
||||||
|
// digitalWrite(pin_cs, HIGH);
|
||||||
|
// digitalWrite(pin_dc, HIGH);
|
||||||
|
// spi->endTransaction();
|
||||||
|
sendData(&data, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size)
|
||||||
|
{
|
||||||
|
spi->beginTransaction(spiSettings);
|
||||||
|
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||||
|
digitalWrite(pin_cs, LOW);
|
||||||
|
|
||||||
|
// Platform-specific SPI command
|
||||||
|
// Mothballing. This display model is only used by Heltec Wireless Paper (ESP32)
|
||||||
|
#if defined(ARCH_ESP32)
|
||||||
|
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||||
|
#elif defined(ARCH_NRF52)
|
||||||
|
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||||
|
#else
|
||||||
|
#error Not implemented yet? Feel free to add other platforms here.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
digitalWrite(pin_cs, HIGH);
|
||||||
|
digitalWrite(pin_dc, HIGH);
|
||||||
|
spi->endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::configFull()
|
||||||
|
{
|
||||||
|
sendCommand(0x00); // Panel setting register
|
||||||
|
sendData(0b11 << 6 // Display resolution
|
||||||
|
| 1 << 4 // B&W only
|
||||||
|
| 1 << 3 // Vertical scan direction
|
||||||
|
| 1 << 2 // Horizontal scan direction
|
||||||
|
| 1 << 1 // Shutdown: no
|
||||||
|
| 1 << 0 // Reset: no
|
||||||
|
);
|
||||||
|
|
||||||
|
sendCommand(0x50); // VCOM and data interval setting register
|
||||||
|
sendData(0b10 << 6 // Border driven white
|
||||||
|
| 0b11 << 4 // Invert image colors: no
|
||||||
|
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::configFast()
|
||||||
|
{
|
||||||
|
sendCommand(0x00); // Panel setting register
|
||||||
|
sendData(0b11 << 6 // Display resolution
|
||||||
|
| 1 << 5 // LUT from registers (set below)
|
||||||
|
| 1 << 4 // B&W only
|
||||||
|
| 1 << 3 // Vertical scan direction
|
||||||
|
| 1 << 2 // Horizontal scan direction
|
||||||
|
| 1 << 1 // Shutdown: no
|
||||||
|
| 1 << 0 // Reset: no
|
||||||
|
);
|
||||||
|
|
||||||
|
sendCommand(0x50); // VCOM and data interval setting register
|
||||||
|
sendData(0b11 << 6 // Border floating
|
||||||
|
| 0b01 << 4 // Invert image colors: no
|
||||||
|
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the various LUTs
|
||||||
|
sendCommand(0x20); // VCOM
|
||||||
|
sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
|
||||||
|
|
||||||
|
sendCommand(0x21); // White -> White
|
||||||
|
sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
|
||||||
|
|
||||||
|
sendCommand(0x22); // Black -> White
|
||||||
|
sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
|
||||||
|
|
||||||
|
sendCommand(0x23); // White -> Black
|
||||||
|
sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
|
||||||
|
|
||||||
|
sendCommand(0x24); // Black -> Black
|
||||||
|
sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::writeNewImage()
|
||||||
|
{
|
||||||
|
sendCommand(0x13);
|
||||||
|
sendData(buffer, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::writeOldImage()
|
||||||
|
{
|
||||||
|
sendCommand(0x10);
|
||||||
|
sendData(buffer, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::detachFromUpdate()
|
||||||
|
{
|
||||||
|
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||||
|
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||||
|
// If not implemented, we'll just poll right from the get-go
|
||||||
|
switch (updateType) {
|
||||||
|
case FULL:
|
||||||
|
EInk::beginPolling(10, 3650);
|
||||||
|
break;
|
||||||
|
case FAST:
|
||||||
|
EInk::beginPolling(10, 720);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LCMEN213EFC1::isUpdateDone()
|
||||||
|
{
|
||||||
|
// Busy when LOW
|
||||||
|
if (digitalRead(pin_busy) == LOW)
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LCMEN213EFC1::finalizeUpdate()
|
||||||
|
{
|
||||||
|
// Power off the panel voltages
|
||||||
|
sendCommand(0x02);
|
||||||
|
wait();
|
||||||
|
|
||||||
|
// Put a copy of the image into the "old memory".
|
||||||
|
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||||
|
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||||
|
if (updateType != FULL) {
|
||||||
|
writeOldImage();
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
68
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h
Normal file
68
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- LCMEN213EFC1
|
||||||
|
- Manufacturer: Wisevast
|
||||||
|
- Size: 2.13 inch
|
||||||
|
- Resolution: 122px x 250px
|
||||||
|
- Flex connector marking: HINK-E0213A162-FPC-A0 (Hidden, printed on back-side)
|
||||||
|
|
||||||
|
Note: this display uses an uncommon controller IC, Fitipower JD79656.
|
||||||
|
It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./EInk.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
|
||||||
|
class LCMEN213EFC1 : public EInk
|
||||||
|
{
|
||||||
|
// Display properties
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t width = 122;
|
||||||
|
static constexpr uint32_t height = 250;
|
||||||
|
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||||
|
|
||||||
|
public:
|
||||||
|
LCMEN213EFC1();
|
||||||
|
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
|
||||||
|
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void wait();
|
||||||
|
void reset();
|
||||||
|
void sendCommand(const uint8_t command);
|
||||||
|
void sendData(const uint8_t data);
|
||||||
|
void sendData(const uint8_t *data, uint32_t size);
|
||||||
|
void configFull(); // Configure display for FULL refresh
|
||||||
|
void configFast(); // Configure display for FAST refresh
|
||||||
|
void writeNewImage();
|
||||||
|
void writeOldImage();
|
||||||
|
|
||||||
|
void detachFromUpdate();
|
||||||
|
bool isUpdateDone();
|
||||||
|
void finalizeUpdate();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint8_t bufferOffsetX; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||||
|
uint8_t bufferRowSize; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||||
|
uint32_t bufferSize; // In bytes. Rows * Columns
|
||||||
|
uint8_t *buffer;
|
||||||
|
UpdateTypes updateType;
|
||||||
|
|
||||||
|
uint8_t pin_dc, pin_cs, pin_busy, pin_rst;
|
||||||
|
SPIClass *spi;
|
||||||
|
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
82
src/graphics/niche/Drivers/EInk/README.md
Normal file
82
src/graphics/niche/Drivers/EInk/README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# NicheGraphics - E-Ink Driver
|
||||||
|
|
||||||
|
A driver for E-Ink SPI displays. Suitable for re-use by various NicheGraphics UIs.
|
||||||
|
|
||||||
|
Your UI should use the class `NicheGraphics::Drivers::EInk` .
|
||||||
|
When you set up a hardware variant, you will use one of specific display model classes, which extend the EInk class.
|
||||||
|
|
||||||
|
An example setup might look like this:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void setupNicheGraphics()
|
||||||
|
{
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
// An imaginary UI
|
||||||
|
YourCustomUI *yourUI = new YourCustomUI();
|
||||||
|
|
||||||
|
// Setup SPI
|
||||||
|
SPIClass *hspi = new SPIClass(HSPI);
|
||||||
|
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
|
||||||
|
|
||||||
|
// Setup Enk driver
|
||||||
|
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
|
||||||
|
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
|
||||||
|
|
||||||
|
// Pass the driver to your UI
|
||||||
|
YourUI::driver = driver;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### `update(uint8_t *imageData, UpdateTypes type, bool async=true)`
|
||||||
|
|
||||||
|
Update the image on the display
|
||||||
|
|
||||||
|
- _`imageData`_ to draw to the display.
|
||||||
|
- _`type`_ which type of update to perform.
|
||||||
|
- `FULL`
|
||||||
|
- `FAST`
|
||||||
|
- (Other custom types may be possible)
|
||||||
|
- _`async`_ whether to wait for update to complete, or continue code execution
|
||||||
|
|
||||||
|
The imageData is a 1-bit image. X-Pixels are 8-per byte, with the MSB being the leftmost pixel. This was not an InkHUD design decision; it is the raw format accepted by the E-Ink display controllers ICs.
|
||||||
|
|
||||||
|
_To-do: add a helper method to `InkHUD::Drivers::EInk` to do this arithmetic for you._
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
uint16_t w = driver::width();
|
||||||
|
uint16_t h = driver::height();
|
||||||
|
|
||||||
|
uint8_t image[ (w/8) * h ]; // X pixels are 8-per-byte
|
||||||
|
|
||||||
|
image[0] |= (1 << 7); // Set pixel x=0, y=0
|
||||||
|
image[0] |= (1 << 0); // Set pixel x=7, y=0
|
||||||
|
image[1] |= (1 << 7); // Set pixel x=8, y=0
|
||||||
|
|
||||||
|
uint8_t x = 12;
|
||||||
|
uint8_t y = 2;
|
||||||
|
uint8_t yBytes = y * (w/8);
|
||||||
|
uint8_t xBytes = x / 8;
|
||||||
|
uint8_t xBits = (7-x) % 8;
|
||||||
|
image[yByte + xByte] |= (1 << xBits); // Set pixel x=12, y=2
|
||||||
|
```
|
||||||
|
|
||||||
|
### `supports(UpdateTypes type)`
|
||||||
|
|
||||||
|
Check if display supports a specific update type. `true` if supported.
|
||||||
|
|
||||||
|
- _`type`_ type to check
|
||||||
|
|
||||||
|
### `busy()`
|
||||||
|
|
||||||
|
Check if display is already performing an `update()`. `true` if already updating.
|
||||||
|
|
||||||
|
### `width()`
|
||||||
|
|
||||||
|
Width of the display, in pixels. Note: most displays are portait. Your UI will need to implement rotation in software.
|
||||||
|
|
||||||
|
### `height()`
|
||||||
|
|
||||||
|
Height of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software.
|
227
src/graphics/niche/Drivers/EInk/SSD16XX.cpp
Normal file
227
src/graphics/niche/Drivers/EInk/SSD16XX.cpp
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
#include "./SSD16XX.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX)
|
||||||
|
: EInk(width, height, supported), bufferOffsetX(bufferOffsetX)
|
||||||
|
{
|
||||||
|
// Pre-calculate size of the image buffer, for convenience
|
||||||
|
|
||||||
|
// Determine the X dimension of the image buffer, in bytes.
|
||||||
|
// Along rows, pixels are stored 8 per byte.
|
||||||
|
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||||
|
bufferRowSize = ((width - 1) / 8) + 1;
|
||||||
|
|
||||||
|
// Total size of image buffer, in bytes.
|
||||||
|
bufferSize = bufferRowSize * height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||||
|
{
|
||||||
|
this->spi = spi;
|
||||||
|
this->pin_dc = pin_dc;
|
||||||
|
this->pin_cs = pin_cs;
|
||||||
|
this->pin_busy = pin_busy;
|
||||||
|
this->pin_rst = pin_rst;
|
||||||
|
|
||||||
|
pinMode(pin_dc, OUTPUT);
|
||||||
|
pinMode(pin_cs, OUTPUT);
|
||||||
|
pinMode(pin_busy, INPUT);
|
||||||
|
|
||||||
|
// If using a reset pin, hold high
|
||||||
|
// Reset is active low for solmon systech ICs
|
||||||
|
if (pin_rst != 0xFF)
|
||||||
|
pinMode(pin_rst, INPUT_PULLUP);
|
||||||
|
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::wait()
|
||||||
|
{
|
||||||
|
// Busy when HIGH
|
||||||
|
while (digitalRead(pin_busy) == HIGH)
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::reset()
|
||||||
|
{
|
||||||
|
// Check if reset pin is defined
|
||||||
|
if (pin_rst != 0xFF) {
|
||||||
|
pinMode(pin_rst, OUTPUT);
|
||||||
|
digitalWrite(pin_rst, LOW);
|
||||||
|
delay(50);
|
||||||
|
pinMode(pin_rst, INPUT_PULLUP);
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommand(0x12);
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::sendCommand(const uint8_t command)
|
||||||
|
{
|
||||||
|
spi->beginTransaction(spiSettings);
|
||||||
|
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||||
|
digitalWrite(pin_cs, LOW);
|
||||||
|
spi->transfer(command);
|
||||||
|
digitalWrite(pin_cs, HIGH);
|
||||||
|
digitalWrite(pin_dc, HIGH);
|
||||||
|
spi->endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::sendData(uint8_t data)
|
||||||
|
{
|
||||||
|
// spi->beginTransaction(spiSettings);
|
||||||
|
// digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||||
|
// digitalWrite(pin_cs, LOW);
|
||||||
|
// spi->transfer(data);
|
||||||
|
// digitalWrite(pin_cs, HIGH);
|
||||||
|
// digitalWrite(pin_dc, HIGH);
|
||||||
|
// spi->endTransaction();
|
||||||
|
sendData(&data, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::sendData(const uint8_t *data, uint32_t size)
|
||||||
|
{
|
||||||
|
spi->beginTransaction(spiSettings);
|
||||||
|
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||||
|
digitalWrite(pin_cs, LOW);
|
||||||
|
|
||||||
|
// Platform-specific SPI command
|
||||||
|
#if defined(ARCH_ESP32)
|
||||||
|
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||||
|
#elif defined(ARCH_NRF52)
|
||||||
|
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||||
|
#else
|
||||||
|
#error Not implemented yet? Feel free to add other platforms here.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
digitalWrite(pin_cs, HIGH);
|
||||||
|
digitalWrite(pin_dc, HIGH);
|
||||||
|
spi->endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::configFullscreen()
|
||||||
|
{
|
||||||
|
// Placing this code in a separate method because it's probably pretty consistent between displays
|
||||||
|
// Should make it tidier to override SSD16XX::configure
|
||||||
|
|
||||||
|
// Define the boundaries of the "fullscreen" region, for the controller IC
|
||||||
|
static const uint16_t sx = bufferOffsetX; // Notice the offset
|
||||||
|
static const uint16_t sy = 0;
|
||||||
|
static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
|
||||||
|
static const uint16_t ey = height;
|
||||||
|
|
||||||
|
// Split into bytes
|
||||||
|
static const uint8_t sy1 = sy & 0xFF;
|
||||||
|
static const uint8_t sy2 = (sy >> 8) & 0xFF;
|
||||||
|
static const uint8_t ey1 = ey & 0xFF;
|
||||||
|
static const uint8_t ey2 = (ey >> 8) & 0xFF;
|
||||||
|
|
||||||
|
// Data entry mode - Left to Right, Top to Bottom
|
||||||
|
sendCommand(0x11);
|
||||||
|
sendData(0x03);
|
||||||
|
|
||||||
|
// Select controller IC memory region to display a fullscreen image
|
||||||
|
sendCommand(0x44); // Memory X start - end
|
||||||
|
sendData(sx);
|
||||||
|
sendData(ex);
|
||||||
|
sendCommand(0x45); // Memory Y start - end
|
||||||
|
sendData(sy1);
|
||||||
|
sendData(sy2);
|
||||||
|
sendData(ey1);
|
||||||
|
sendData(ey2);
|
||||||
|
|
||||||
|
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
|
||||||
|
sendCommand(0x4E); // Memory cursor X
|
||||||
|
sendData(sx);
|
||||||
|
sendCommand(0x4F); // Memory cursor y
|
||||||
|
sendData(sy1);
|
||||||
|
sendData(sy2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::update(uint8_t *imageData, UpdateTypes type)
|
||||||
|
{
|
||||||
|
this->updateType = type;
|
||||||
|
this->buffer = imageData;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
|
||||||
|
configFullscreen();
|
||||||
|
configScanning(); // Virtual, unused by base class
|
||||||
|
configVoltages(); // Virtual, unused by base class
|
||||||
|
configWaveform(); // Virtual, unused by base class
|
||||||
|
wait();
|
||||||
|
|
||||||
|
if (updateType == FULL) {
|
||||||
|
writeNewImage();
|
||||||
|
writeOldImage();
|
||||||
|
} else {
|
||||||
|
writeNewImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
configUpdateSequence();
|
||||||
|
sendCommand(0x20); // Begin executing the update
|
||||||
|
|
||||||
|
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||||
|
// For a blocking update, call await after update
|
||||||
|
detachFromUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send SPI commands for controller IC to begin executing the refresh operation
|
||||||
|
void SSD16XX::configUpdateSequence()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
default:
|
||||||
|
sendCommand(0x22); // Set "update sequence"
|
||||||
|
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::writeNewImage()
|
||||||
|
{
|
||||||
|
sendCommand(0x24);
|
||||||
|
sendData(buffer, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::writeOldImage()
|
||||||
|
{
|
||||||
|
sendCommand(0x26);
|
||||||
|
sendData(buffer, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::detachFromUpdate()
|
||||||
|
{
|
||||||
|
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||||
|
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||||
|
// If not implemented, we'll just poll right from the get-go
|
||||||
|
switch (updateType) {
|
||||||
|
default:
|
||||||
|
EInk::beginPolling(100, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SSD16XX::isUpdateDone()
|
||||||
|
{
|
||||||
|
// Busy when HIGH
|
||||||
|
if (digitalRead(pin_busy) == HIGH)
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SSD16XX::finalizeUpdate()
|
||||||
|
{
|
||||||
|
// Put a copy of the image into the "old memory".
|
||||||
|
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||||
|
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||||
|
if (updateType != FULL) {
|
||||||
|
writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678?
|
||||||
|
writeOldImage();
|
||||||
|
sendCommand(0x7F); // Terminate image write without update
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
62
src/graphics/niche/Drivers/EInk/SSD16XX.h
Normal file
62
src/graphics/niche/Drivers/EInk/SSD16XX.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink base class for displays based on SSD16XX
|
||||||
|
|
||||||
|
Most (but not all) SPI E-Ink displays use this family of controller IC.
|
||||||
|
Implementing new SSD16XX displays should be fairly painless.
|
||||||
|
See DEPG0154BNS800 and DEPG0290BNS800 for examples.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./EInk.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
|
||||||
|
class SSD16XX : public EInk
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0);
|
||||||
|
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1);
|
||||||
|
virtual void update(uint8_t *imageData, UpdateTypes type) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void wait();
|
||||||
|
virtual void reset();
|
||||||
|
virtual void sendCommand(const uint8_t command);
|
||||||
|
virtual void sendData(const uint8_t data);
|
||||||
|
virtual void sendData(const uint8_t *data, uint32_t size);
|
||||||
|
virtual void configFullscreen(); // Select memory region on controller IC
|
||||||
|
virtual void configScanning() {} // Optional. First & last gates, scan direction, etc
|
||||||
|
virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc
|
||||||
|
virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc
|
||||||
|
virtual void configUpdateSequence(); // Tell controller IC which operations to run
|
||||||
|
|
||||||
|
virtual void writeNewImage();
|
||||||
|
virtual void writeOldImage();
|
||||||
|
|
||||||
|
virtual void detachFromUpdate();
|
||||||
|
virtual bool isUpdateDone() override;
|
||||||
|
virtual void finalizeUpdate() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint8_t bufferOffsetX; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||||
|
uint8_t bufferRowSize; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||||
|
uint32_t bufferSize; // In bytes. Rows * Columns
|
||||||
|
uint8_t *buffer;
|
||||||
|
UpdateTypes updateType;
|
||||||
|
|
||||||
|
uint8_t pin_dc, pin_cs, pin_busy, pin_rst;
|
||||||
|
SPIClass *spi;
|
||||||
|
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
3
src/graphics/niche/Drivers/README.md
Normal file
3
src/graphics/niche/Drivers/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# NicheGraphics - Drivers
|
||||||
|
|
||||||
|
Common drivers which can be used by various NicheGrapihcs UIs
|
140
src/graphics/niche/FlashData.h
Normal file
140
src/graphics/niche/FlashData.h
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Re-usable NicheGraphics tool
|
||||||
|
|
||||||
|
Save settings / data to flash, without use of the Meshtastic Protobufs
|
||||||
|
Avoid bloating everyone's protobuf code for our one-off UI implementations
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "SafeFile.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics
|
||||||
|
{
|
||||||
|
|
||||||
|
template <typename T> class FlashData
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
static std::string getFilename(const char *label)
|
||||||
|
{
|
||||||
|
std::string filename;
|
||||||
|
filename += "/NicheGraphics";
|
||||||
|
filename += "/";
|
||||||
|
filename += label;
|
||||||
|
filename += ".data";
|
||||||
|
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t getHash(T *data)
|
||||||
|
{
|
||||||
|
uint32_t hash = 0;
|
||||||
|
|
||||||
|
// Sum all bytes of the image buffer together
|
||||||
|
for (uint32_t i = 0; i < sizeof(T); i++)
|
||||||
|
hash ^= ((uint8_t *)data)[i] + 1;
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static bool load(T *data, const char *label)
|
||||||
|
{
|
||||||
|
// Set false if we run into issues
|
||||||
|
bool okay = true;
|
||||||
|
|
||||||
|
// Get a filename based on the label
|
||||||
|
std::string filename = getFilename(label);
|
||||||
|
|
||||||
|
#ifdef FSCom
|
||||||
|
|
||||||
|
// Check that the file *does* actually exist
|
||||||
|
if (!FSCom.exists(filename.c_str())) {
|
||||||
|
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||||
|
okay = false;
|
||||||
|
return okay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the file
|
||||||
|
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||||
|
|
||||||
|
// If opened, start reading
|
||||||
|
if (f) {
|
||||||
|
LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str());
|
||||||
|
|
||||||
|
// Create an object which will received data from flash
|
||||||
|
// We read here first, so we can verify the checksum, without committing to overwriting the *data object
|
||||||
|
// Allows us to retain any defaults that might be set after we declared *data, but before loading settings,
|
||||||
|
// in case the flash values are corrupt
|
||||||
|
T flashData;
|
||||||
|
|
||||||
|
// Read the actual data
|
||||||
|
f.readBytes((char *)&flashData, sizeof(T));
|
||||||
|
|
||||||
|
// Read the hash
|
||||||
|
uint32_t savedHash = 0;
|
||||||
|
f.readBytes((char *)&savedHash, sizeof(savedHash));
|
||||||
|
|
||||||
|
// Calculate hash of the loaded data, then compare with the saved hash
|
||||||
|
// If hash looks good, copy the values to the main data object
|
||||||
|
uint32_t calculatedHash = getHash(&flashData);
|
||||||
|
if (savedHash != calculatedHash) {
|
||||||
|
LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str());
|
||||||
|
okay = false;
|
||||||
|
} else
|
||||||
|
*data = flashData;
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||||
|
okay = false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_ERROR("Filesystem not implemented");
|
||||||
|
state = LoadFileState::NO_FILESYSTEM;
|
||||||
|
okay = false;
|
||||||
|
#endif
|
||||||
|
return okay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save module's custom data (settings?) to flash. Does use protobufs
|
||||||
|
static void save(T *data, const char *label)
|
||||||
|
{
|
||||||
|
// Get a filename based on the label
|
||||||
|
std::string filename = getFilename(label);
|
||||||
|
|
||||||
|
#ifdef FSCom
|
||||||
|
FSCom.mkdir("/NicheGraphics");
|
||||||
|
|
||||||
|
auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename.
|
||||||
|
|
||||||
|
LOG_INFO("Saving %s", filename.c_str());
|
||||||
|
|
||||||
|
// Calculate a hash of the data
|
||||||
|
uint32_t hash = getHash(data);
|
||||||
|
|
||||||
|
f.write((uint8_t *)data, sizeof(T)); // Write the actualy data
|
||||||
|
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
|
||||||
|
|
||||||
|
// f.flush();
|
||||||
|
|
||||||
|
bool writeSucceeded = f.close();
|
||||||
|
|
||||||
|
if (!writeSucceeded) {
|
||||||
|
LOG_ERROR("Can't write data!");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics
|
||||||
|
|
||||||
|
#endif
|
129
src/graphics/niche/Fonts/FreeSans6pt7b.h
Normal file
129
src/graphics/niche/Fonts/FreeSans6pt7b.h
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
const uint8_t FreeSans6pt7bBitmaps[] PROGMEM = {
|
||||||
|
0xAA, 0xA8, 0xC0, 0xF6, 0xA0, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00,
|
||||||
|
0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, 0x70, 0x91, 0x23, 0x86, 0x12, 0xA2, 0x4E, 0xF4, 0xE0,
|
||||||
|
0x5A, 0xAA, 0x94, 0x89, 0x12, 0x49, 0x29, 0x00, 0x27, 0x50, 0x21, 0x3E, 0x42, 0x00, 0xE0, 0xC0, 0x80, 0x24, 0xA4, 0xA4, 0x80,
|
||||||
|
0x74, 0xE3, 0x18, 0xC6, 0x33, 0x70, 0x27, 0x92, 0x49, 0x20, 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, 0x79, 0x30, 0x43, 0x18,
|
||||||
|
0x10, 0x71, 0x78, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0x7D, 0x04, 0x1E, 0x44, 0x10, 0x51, 0x78, 0x74, 0x61, 0xE8, 0xC6,
|
||||||
|
0x31, 0x70, 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53,
|
||||||
|
0x78, 0x82, 0x87, 0x01, 0xF1, 0x83, 0x04, 0xF8, 0x3E, 0x07, 0x06, 0x36, 0x40, 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, 0x0F, 0x86,
|
||||||
|
0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42,
|
||||||
|
0x42, 0xC3, 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, 0xF9, 0x0A, 0x1C,
|
||||||
|
0x18, 0x30, 0x61, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x61,
|
||||||
|
0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x87,
|
||||||
|
0x29, 0x70, 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
|
||||||
|
0xA5, 0x99, 0x99, 0x99, 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6,
|
||||||
|
0x1E, 0x00, 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1F, 0x00, 0x00,
|
||||||
|
0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04,
|
||||||
|
0x08, 0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, 0xC2, 0x42, 0x42, 0x64, 0x24, 0x24, 0x38, 0x18, 0x18, 0xC4, 0x28,
|
||||||
|
0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, 0x42, 0x66, 0x24, 0x18, 0x18, 0x18, 0x24, 0x46, 0x42, 0xC3,
|
||||||
|
0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24,
|
||||||
|
0x89, 0x20, 0xE9, 0x24, 0x92, 0x49, 0x70, 0x46, 0xA9, 0x10, 0xFE, 0x40, 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, 0x84, 0x3D, 0x18,
|
||||||
|
0xC6, 0x31, 0xF0, 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, 0x39, 0x38, 0x7F, 0x81, 0x13,
|
||||||
|
0x80, 0x6B, 0xA4, 0x92, 0x40, 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, 0xBF, 0x80,
|
||||||
|
0x45, 0x55, 0x57, 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, 0xFF, 0x80, 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, 0xF4, 0x63, 0x18,
|
||||||
|
0xC6, 0x20, 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04,
|
||||||
|
0xF2, 0x49, 0x20, 0x79, 0x24, 0x1C, 0x0B, 0x27, 0x80, 0x5D, 0x24, 0x93, 0x8C, 0x63, 0x18, 0xCF, 0xA0, 0x85, 0x24, 0x92, 0x30,
|
||||||
|
0xC3, 0x00, 0x89, 0x2C, 0x96, 0x4A, 0xA5, 0x61, 0x30, 0x98, 0x49, 0x23, 0x08, 0x31, 0x2C, 0x80, 0x89, 0x24, 0x94, 0x50, 0xC2,
|
||||||
|
0x08, 0x21, 0x00, 0x78, 0x44, 0x46, 0x23, 0xE0, 0x6A, 0xAA, 0xA9, 0xFF, 0xE0, 0x95, 0x55, 0x56, 0x66, 0x60};
|
||||||
|
|
||||||
|
const GFXglyph FreeSans6pt7bGlyphs[] PROGMEM = {{0, 0, 0, 3, 0, 1}, // 0x20 ' '
|
||||||
|
{0, 2, 9, 4, 1, -8}, // 0x21 '!'
|
||||||
|
{3, 4, 3, 4, 0, -8}, // 0x22 '"'
|
||||||
|
{5, 7, 8, 7, 0, -7}, // 0x23 '#'
|
||||||
|
{12, 6, 11, 7, 0, -9}, // 0x24 '$'
|
||||||
|
{21, 10, 9, 11, 0, -8}, // 0x25 '%'
|
||||||
|
{33, 7, 9, 8, 1, -8}, // 0x26 '&'
|
||||||
|
{41, 1, 3, 2, 1, -8}, // 0x27 '''
|
||||||
|
{42, 2, 11, 4, 1, -8}, // 0x28 '('
|
||||||
|
{45, 3, 11, 4, 0, -8}, // 0x29 ')'
|
||||||
|
{50, 4, 3, 5, 0, -8}, // 0x2A '*'
|
||||||
|
{52, 5, 5, 7, 1, -4}, // 0x2B '+'
|
||||||
|
{56, 1, 3, 3, 1, 0}, // 0x2C ','
|
||||||
|
{57, 2, 1, 4, 1, -3}, // 0x2D '-'
|
||||||
|
{58, 1, 1, 3, 1, 0}, // 0x2E '.'
|
||||||
|
{59, 3, 9, 3, 0, -8}, // 0x2F '/'
|
||||||
|
{63, 5, 9, 7, 1, -8}, // 0x30 '0'
|
||||||
|
{69, 3, 9, 7, 1, -8}, // 0x31 '1'
|
||||||
|
{73, 6, 9, 7, 0, -8}, // 0x32 '2'
|
||||||
|
{80, 6, 9, 7, 0, -8}, // 0x33 '3'
|
||||||
|
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
|
||||||
|
{94, 6, 9, 7, 0, -8}, // 0x35 '5'
|
||||||
|
{101, 5, 9, 7, 1, -8}, // 0x36 '6'
|
||||||
|
{107, 5, 9, 7, 1, -8}, // 0x37 '7'
|
||||||
|
{113, 6, 9, 7, 0, -8}, // 0x38 '8'
|
||||||
|
{120, 6, 9, 7, 0, -8}, // 0x39 '9'
|
||||||
|
{127, 1, 7, 3, 1, -6}, // 0x3A ':'
|
||||||
|
{128, 1, 8, 3, 1, -5}, // 0x3B ';'
|
||||||
|
{129, 5, 6, 7, 1, -5}, // 0x3C '<'
|
||||||
|
{133, 5, 3, 7, 1, -3}, // 0x3D '='
|
||||||
|
{135, 5, 6, 7, 1, -5}, // 0x3E '>'
|
||||||
|
{139, 5, 9, 7, 1, -8}, // 0x3F '?'
|
||||||
|
{145, 11, 11, 12, 0, -8}, // 0x40 '@'
|
||||||
|
{161, 8, 9, 8, 0, -8}, // 0x41 'A'
|
||||||
|
{170, 6, 9, 8, 1, -8}, // 0x42 'B'
|
||||||
|
{177, 8, 9, 9, 0, -8}, // 0x43 'C'
|
||||||
|
{186, 7, 9, 8, 1, -8}, // 0x44 'D'
|
||||||
|
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
|
||||||
|
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
|
||||||
|
{208, 8, 9, 9, 0, -8}, // 0x47 'G'
|
||||||
|
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
|
||||||
|
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
|
||||||
|
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
|
||||||
|
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
|
||||||
|
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
|
||||||
|
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
|
||||||
|
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
|
||||||
|
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
|
||||||
|
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
|
||||||
|
{282, 9, 10, 9, 0, -8}, // 0x51 'Q'
|
||||||
|
{294, 7, 9, 9, 1, -8}, // 0x52 'R'
|
||||||
|
{302, 6, 9, 8, 1, -8}, // 0x53 'S'
|
||||||
|
{309, 7, 9, 8, 0, -8}, // 0x54 'T'
|
||||||
|
{317, 7, 9, 9, 1, -8}, // 0x55 'U'
|
||||||
|
{325, 8, 9, 8, 0, -8}, // 0x56 'V'
|
||||||
|
{334, 11, 9, 11, 0, -8}, // 0x57 'W'
|
||||||
|
{347, 8, 9, 8, 0, -8}, // 0x58 'X'
|
||||||
|
{356, 8, 9, 8, 0, -8}, // 0x59 'Y'
|
||||||
|
{365, 7, 9, 7, 0, -8}, // 0x5A 'Z'
|
||||||
|
{373, 2, 12, 3, 1, -8}, // 0x5B '['
|
||||||
|
{376, 3, 9, 3, 0, -8}, // 0x5C '\'
|
||||||
|
{380, 3, 12, 3, 0, -8}, // 0x5D ']'
|
||||||
|
{385, 4, 5, 6, 1, -8}, // 0x5E '^'
|
||||||
|
{388, 7, 1, 7, 0, 2}, // 0x5F '_'
|
||||||
|
{389, 3, 1, 3, 0, -8}, // 0x60 '`'
|
||||||
|
{390, 6, 7, 7, 0, -6}, // 0x61 'a'
|
||||||
|
{396, 5, 9, 7, 1, -8}, // 0x62 'b'
|
||||||
|
{402, 6, 7, 6, 0, -6}, // 0x63 'c'
|
||||||
|
{408, 6, 9, 7, 0, -8}, // 0x64 'd'
|
||||||
|
{415, 6, 7, 6, 0, -6}, // 0x65 'e'
|
||||||
|
{421, 3, 9, 3, 0, -8}, // 0x66 'f'
|
||||||
|
{425, 6, 10, 7, 0, -6}, // 0x67 'g'
|
||||||
|
{433, 5, 9, 6, 1, -8}, // 0x68 'h'
|
||||||
|
{439, 1, 9, 3, 1, -8}, // 0x69 'i'
|
||||||
|
{441, 2, 12, 3, 0, -8}, // 0x6A 'j'
|
||||||
|
{444, 5, 9, 6, 1, -8}, // 0x6B 'k'
|
||||||
|
{450, 1, 9, 3, 1, -8}, // 0x6C 'l'
|
||||||
|
{452, 8, 7, 10, 1, -6}, // 0x6D 'm'
|
||||||
|
{459, 5, 7, 6, 1, -6}, // 0x6E 'n'
|
||||||
|
{464, 6, 7, 6, 0, -6}, // 0x6F 'o'
|
||||||
|
{470, 5, 9, 7, 1, -6}, // 0x70 'p'
|
||||||
|
{476, 6, 9, 7, 0, -6}, // 0x71 'q'
|
||||||
|
{483, 3, 7, 4, 1, -6}, // 0x72 'r'
|
||||||
|
{486, 6, 7, 6, 0, -6}, // 0x73 's'
|
||||||
|
{492, 3, 8, 3, 0, -7}, // 0x74 't'
|
||||||
|
{495, 5, 7, 6, 1, -6}, // 0x75 'u'
|
||||||
|
{500, 6, 7, 6, 0, -6}, // 0x76 'v'
|
||||||
|
{506, 9, 7, 9, 0, -6}, // 0x77 'w'
|
||||||
|
{514, 6, 7, 6, 0, -6}, // 0x78 'x'
|
||||||
|
{520, 6, 10, 6, 0, -6}, // 0x79 'y'
|
||||||
|
{528, 5, 7, 6, 0, -6}, // 0x7A 'z'
|
||||||
|
{533, 2, 12, 4, 1, -8}, // 0x7B '{'
|
||||||
|
{536, 1, 11, 3, 1, -8}, // 0x7C '|'
|
||||||
|
{538, 2, 12, 4, 1, -8}, // 0x7D '}'
|
||||||
|
{541, 6, 2, 6, 0, -4}}; // 0x7E '~'
|
||||||
|
|
||||||
|
const GFXfont FreeSans6pt7b PROGMEM = {(uint8_t *)FreeSans6pt7bBitmaps, (GFXglyph *)FreeSans6pt7bGlyphs, 0x20, 0x7E, 14};
|
||||||
|
|
||||||
|
// Approx. 1215 bytes
|
302
src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h
Normal file
302
src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Uses Windows-1251 encoding to map translingual Cyrillic characters to range between (uint8_t)127 and (uint8_t)255
|
||||||
|
https://en.wikipedia.org/wiki/Windows-1251
|
||||||
|
|
||||||
|
Cyrillic characters present to the firmware as UTF8.
|
||||||
|
A Niche Graphics implementation needs to identify these, and subsitute the appropriate Windows-1251 char value.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
const uint8_t FreeSans6pt8bCyrillicBitmaps[] PROGMEM = {
|
||||||
|
0xFF, 0xA0, 0xC0, 0xFF, 0xA0, 0xC0, 0xB6, 0x80, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x31, 0x75, 0x54, 0x78, 0x79, 0x75,
|
||||||
|
0x7C, 0x41, 0x00, 0x01, 0x1C, 0x49, 0x22, 0x50, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0xC0, 0x21, 0x44, 0x94, 0x62, 0x59, 0xE2,
|
||||||
|
0xF4, 0xE0, 0x6A, 0xAA, 0x90, 0x48, 0x92, 0x49, 0x4A, 0x00, 0x5D, 0x40, 0x21, 0x09, 0xF2, 0x10, 0xE0, 0xC0, 0x80, 0x25, 0x25,
|
||||||
|
0x24, 0x26, 0xA3, 0x18, 0xC6, 0x31, 0xF0, 0x27, 0x92, 0x49, 0x20, 0x11, 0xB4, 0x41, 0x0C, 0xC6, 0x10, 0xFC, 0x26, 0xA2, 0x13,
|
||||||
|
0x04, 0x31, 0xF0, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0xFF, 0xE1, 0x4D, 0x84, 0x31, 0xF0, 0x26, 0xE3, 0x0F, 0x46, 0x31,
|
||||||
|
0xF0, 0xFF, 0xC4, 0x22, 0x11, 0x08, 0x40, 0x11, 0xA4, 0x51, 0x39, 0x1C, 0x51, 0x78, 0x11, 0xA4, 0x71, 0x45, 0xF0, 0x51, 0x78,
|
||||||
|
0xC0, 0x30, 0xC0, 0x36, 0x1F, 0x20, 0xE0, 0x80, 0xF8, 0x3E, 0xC1, 0xC2, 0xE8, 0x00, 0x74, 0x62, 0x11, 0x10, 0x80, 0x20, 0x0F,
|
||||||
|
0x06, 0x18, 0x81, 0xA7, 0xD4, 0x93, 0x22, 0x64, 0x4A, 0x7E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x1C, 0x24, 0x24, 0x7E,
|
||||||
|
0x42, 0x42, 0xC3, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xF9, 0x1A, 0x1C,
|
||||||
|
0x18, 0x30, 0x60, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x3C, 0x46,
|
||||||
|
0x82, 0x80, 0x8F, 0x81, 0x83, 0xC3, 0x7D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x86,
|
||||||
|
0x31, 0x78, 0x87, 0x1A, 0x65, 0x8F, 0x1A, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
|
||||||
|
0xA5, 0x99, 0x99, 0x99, 0x83, 0x87, 0x8D, 0x19, 0x32, 0x62, 0xC3, 0x86, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC2,
|
||||||
|
0x3E, 0x00, 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x3F, 0x00, 0xFD,
|
||||||
|
0x0E, 0x0C, 0x1F, 0xD0, 0xA0, 0xC1, 0x82, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08,
|
||||||
|
0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0x7C, 0xC3, 0x42, 0x42, 0x26, 0x24, 0x24, 0x14, 0x18, 0x18, 0xC4, 0x28, 0xC5,
|
||||||
|
0x39, 0xA5, 0x24, 0xA4, 0x52, 0x8C, 0x71, 0x8C, 0x30, 0x80, 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, 0xC3, 0x42, 0x26, 0x24,
|
||||||
|
0x18, 0x18, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x10, 0x41, 0x06, 0x08, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24, 0x89, 0x20, 0xED,
|
||||||
|
0xB6, 0xDB, 0x6D, 0xF0, 0x46, 0xAA, 0x90, 0xFC, 0x90, 0xFC, 0x4F, 0x98, 0xFC, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0xF0, 0x79, 0x18,
|
||||||
|
0x20, 0x45, 0xE0, 0x04, 0x10, 0x5F, 0xC6, 0x18, 0x51, 0x7C, 0xFC, 0x7F, 0x08, 0xF8, 0x29, 0x74, 0x92, 0x40, 0x7D, 0x18, 0x61,
|
||||||
|
0x45, 0xF0, 0x52, 0x30, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0x88, 0xDF, 0x80, 0x51, 0x55, 0x56, 0x84, 0x21, 0x2A, 0x72, 0x92, 0x98,
|
||||||
|
0xFF, 0x80, 0xFF, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFC, 0x63, 0x18, 0xC4, 0x79, 0x18, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xFA,
|
||||||
|
0x10, 0x80, 0x7D, 0x18, 0x61, 0x45, 0xF0, 0x41, 0x04, 0xF2, 0x49, 0x00, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0x4B, 0xA4, 0x93, 0x8C,
|
||||||
|
0x63, 0x18, 0xFC, 0xCD, 0x24, 0x94, 0x30, 0xC0, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, 0x96, 0x66, 0x99, 0xCA, 0x52, 0x63, 0x18,
|
||||||
|
0x84, 0x40, 0x78, 0xC4, 0x44, 0x7C, 0x6A, 0xAA, 0xA9, 0xFF, 0xF0, 0xC9, 0x24, 0x4A, 0x49, 0x40, 0xE8, 0xC0, 0xFE, 0x18, 0x61,
|
||||||
|
0x86, 0x18, 0x61, 0xFC, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, 0x10, 0x8F, 0xE0, 0x82,
|
||||||
|
0x08, 0x20, 0x82, 0x08, 0x00, 0x64, 0x0F, 0x88, 0x88, 0x80, 0x3D, 0x0C, 0x2E, 0xF9, 0x04, 0x0F, 0x7C, 0x08, 0x81, 0x10, 0x22,
|
||||||
|
0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0,
|
||||||
|
0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0x83,
|
||||||
|
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, 0x78, 0x24, 0x13,
|
||||||
|
0xC9, 0x14, 0x8E, 0x7C, 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, 0x60, 0x9A, 0xCC, 0xA9, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51,
|
||||||
|
0x44, 0x8C, 0x63, 0x18, 0xFC, 0x80, 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, 0x70, 0x22, 0x95, 0xA8, 0xC4,
|
||||||
|
0x23, 0x10, 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, 0x28, 0x0F, 0xE0, 0x82, 0x0F,
|
||||||
|
0xE0, 0x82, 0x0F, 0xC0, 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, 0x51, 0x55, 0x56, 0xA1, 0x24, 0x92, 0x49, 0x00, 0xFF,
|
||||||
|
0x80, 0xDF, 0x80, 0x27, 0xC9, 0x24, 0x8A, 0x28, 0xA2, 0x8B, 0xF8, 0x20, 0x80, 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, 0x88,
|
||||||
|
0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x89, 0x80, 0x79, 0x1F, 0x30, 0x45, 0xE0, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61,
|
||||||
|
0x7C, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0xB4, 0x24, 0x92, 0x40, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, 0xFE, 0x08,
|
||||||
|
0x20, 0xFE, 0x18, 0x61, 0xFC, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, 0x1F, 0x08,
|
||||||
|
0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0x88, 0xA4, 0x9A,
|
||||||
|
0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68,
|
||||||
|
0xE1, 0xC2, 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, 0x3E, 0x44,
|
||||||
|
0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60,
|
||||||
|
0xC1, 0x82, 0x3C, 0x46, 0x83, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, 0xFA, 0x18,
|
||||||
|
0x61, 0xFE, 0x08, 0x20, 0x80, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10,
|
||||||
|
0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, 0x08, 0x1E, 0x32, 0xD1, 0x38, 0x8C, 0x4F, 0x2C, 0xFC, 0x08, 0x00, 0x87, 0x34,
|
||||||
|
0x8C, 0x30, 0xC4, 0xB3, 0x84, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, 0x8E, 0x38, 0xE3, 0x8D, 0xF0,
|
||||||
|
0xC3, 0x0C, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF,
|
||||||
|
0x80, 0x40, 0x20, 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC,
|
||||||
|
0x2E, 0x17, 0x0B, 0xF9, 0x80, 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, 0x87,
|
||||||
|
0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, 0x79, 0x11, 0xD9,
|
||||||
|
0xCD, 0xD0, 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, 0xF4, 0xBD, 0x29, 0xF8, 0xF8, 0x88, 0x88, 0x3C, 0x48, 0x91, 0x22, 0x5F,
|
||||||
|
0xE0, 0x80, 0x79, 0x1F, 0xF0, 0x45, 0xE0, 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, 0x78, 0x23, 0x82, 0xCD, 0xE0, 0x9C, 0xEB, 0x5C,
|
||||||
|
0xC4, 0x70, 0x27, 0x3A, 0xD7, 0x31, 0x9A, 0xCC, 0xA9, 0x7A, 0x52, 0x94, 0xE4, 0x8F, 0x3D, 0x6D, 0xA6, 0x90, 0x8C, 0x7F, 0x18,
|
||||||
|
0xC4, 0x79, 0x1C, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xC4, 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, 0x79, 0x1C, 0x30, 0x45, 0xE0,
|
||||||
|
0xF9, 0x08, 0x42, 0x10, 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04,
|
||||||
|
0x01, 0x00, 0x40, 0x4B, 0x8C, 0x65, 0xE4, 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, 0x99, 0x97, 0x11, 0x96, 0x59, 0x65, 0x97, 0xF0,
|
||||||
|
0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, 0x86, 0x1F, 0x63, 0x8F, 0xD0, 0x84, 0x3D, 0x18,
|
||||||
|
0xF8, 0xF4, 0xDE, 0x19, 0xF8, 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, 0xFC, 0x7E, 0xD4, 0xC4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GFXglyph FreeSans6pt8bCyrillicGlyphs[] PROGMEM = {
|
||||||
|
{0, 0, 0, 3, 0, 0}, // 0x20 ' '
|
||||||
|
{3, 2, 9, 3, 1, -8}, // 0x21 '!'
|
||||||
|
{6, 3, 3, 4, 1, -8}, // 0x22 '"'
|
||||||
|
{8, 7, 8, 7, 0, -7}, // 0x23 '#'
|
||||||
|
{15, 6, 11, 7, 0, -8}, // 0x24 '$'
|
||||||
|
{24, 10, 9, 11, 0, -8}, // 0x25 '%'
|
||||||
|
{36, 6, 9, 8, 1, -8}, // 0x26 '&'
|
||||||
|
{43, 1, 3, 2, 1, -8}, // 0x27 '''
|
||||||
|
{44, 2, 10, 4, 1, -7}, // 0x28 '('
|
||||||
|
{47, 3, 11, 4, 0, -7}, // 0x29 ')'
|
||||||
|
{52, 3, 4, 5, 1, -8}, // 0x2A '*'
|
||||||
|
{54, 5, 6, 7, 1, -5}, // 0x2B '+'
|
||||||
|
{58, 1, 3, 3, 1, 0}, // 0x2C ','
|
||||||
|
{59, 2, 1, 4, 1, -3}, // 0x2D '-'
|
||||||
|
{60, 1, 1, 3, 1, 0}, // 0x2E '.'
|
||||||
|
{61, 3, 8, 3, 0, -7}, // 0x2F '/'
|
||||||
|
{64, 5, 9, 7, 1, -8}, // 0x30 '0'
|
||||||
|
{70, 3, 9, 7, 1, -8}, // 0x31 '1'
|
||||||
|
{74, 6, 9, 7, 0, -8}, // 0x32 '2'
|
||||||
|
{81, 5, 9, 7, 1, -8}, // 0x33 '3'
|
||||||
|
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
|
||||||
|
{94, 5, 9, 7, 1, -8}, // 0x35 '5'
|
||||||
|
{100, 5, 9, 7, 1, -8}, // 0x36 '6'
|
||||||
|
{106, 5, 9, 7, 1, -8}, // 0x37 '7'
|
||||||
|
{112, 6, 9, 7, 0, -8}, // 0x38 '8'
|
||||||
|
{119, 6, 9, 7, 0, -8}, // 0x39 '9'
|
||||||
|
{126, 2, 6, 3, 1, -5}, // 0x3A ':'
|
||||||
|
{128, 2, 8, 3, 1, -5}, // 0x3B ';'
|
||||||
|
{130, 5, 5, 7, 1, -4}, // 0x3C '<'
|
||||||
|
{134, 5, 3, 7, 1, -3}, // 0x3D '='
|
||||||
|
{136, 5, 5, 7, 1, -4}, // 0x3E '>'
|
||||||
|
{140, 5, 9, 7, 1, -8}, // 0x3F '?'
|
||||||
|
{146, 11, 11, 12, 0, -8}, // 0x40 '@'
|
||||||
|
{162, 8, 9, 8, 0, -8}, // 0x41 'A'
|
||||||
|
{171, 6, 9, 8, 1, -8}, // 0x42 'B'
|
||||||
|
{178, 7, 9, 9, 1, -8}, // 0x43 'C'
|
||||||
|
{186, 7, 9, 9, 1, -8}, // 0x44 'D'
|
||||||
|
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
|
||||||
|
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
|
||||||
|
{208, 8, 9, 9, 1, -8}, // 0x47 'G'
|
||||||
|
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
|
||||||
|
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
|
||||||
|
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
|
||||||
|
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
|
||||||
|
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
|
||||||
|
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
|
||||||
|
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
|
||||||
|
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
|
||||||
|
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
|
||||||
|
{282, 9, 9, 9, 0, -8}, // 0x51 'Q'
|
||||||
|
{293, 7, 9, 9, 1, -8}, // 0x52 'R'
|
||||||
|
{301, 6, 9, 8, 1, -8}, // 0x53 'S'
|
||||||
|
{308, 7, 9, 7, 0, -8}, // 0x54 'T'
|
||||||
|
{316, 7, 9, 9, 1, -8}, // 0x55 'U'
|
||||||
|
{324, 8, 9, 8, 0, -8}, // 0x56 'V'
|
||||||
|
{333, 11, 9, 11, 0, -8}, // 0x57 'W'
|
||||||
|
{346, 6, 9, 8, 1, -8}, // 0x58 'X'
|
||||||
|
{353, 8, 9, 8, 0, -8}, // 0x59 'Y'
|
||||||
|
{362, 7, 9, 7, 0, -8}, // 0x5A 'Z'
|
||||||
|
{370, 2, 12, 3, 1, -8}, // 0x5B '['
|
||||||
|
{373, 3, 9, 3, 0, -8}, // 0x5C '\'
|
||||||
|
{377, 3, 12, 3, 0, -8}, // 0x5D ']'
|
||||||
|
{382, 4, 5, 6, 1, -8}, // 0x5E '^'
|
||||||
|
{385, 6, 1, 7, 0, 2}, // 0x5F '_'
|
||||||
|
{386, 2, 2, 4, 1, -8}, // 0x60 '`'
|
||||||
|
{387, 5, 6, 7, 1, -5}, // 0x61 'a'
|
||||||
|
{391, 5, 9, 7, 1, -8}, // 0x62 'b'
|
||||||
|
{397, 6, 6, 6, 0, -5}, // 0x63 'c'
|
||||||
|
{402, 6, 9, 7, 0, -8}, // 0x64 'd'
|
||||||
|
{409, 5, 6, 7, 1, -5}, // 0x65 'e'
|
||||||
|
{413, 3, 9, 3, 0, -8}, // 0x66 'f'
|
||||||
|
{417, 6, 9, 7, 0, -5}, // 0x67 'g'
|
||||||
|
{424, 5, 9, 7, 1, -8}, // 0x68 'h'
|
||||||
|
{430, 1, 9, 3, 1, -8}, // 0x69 'i'
|
||||||
|
{432, 2, 12, 3, 0, -8}, // 0x6A 'j'
|
||||||
|
{435, 5, 9, 6, 1, -8}, // 0x6B 'k'
|
||||||
|
{441, 1, 9, 3, 1, -8}, // 0x6C 'l'
|
||||||
|
{443, 8, 6, 10, 1, -5}, // 0x6D 'm'
|
||||||
|
{449, 5, 6, 7, 1, -5}, // 0x6E 'n'
|
||||||
|
{453, 6, 6, 7, 0, -5}, // 0x6F 'o'
|
||||||
|
{458, 5, 9, 7, 1, -5}, // 0x70 'p'
|
||||||
|
{464, 6, 9, 7, 0, -5}, // 0x71 'q'
|
||||||
|
{471, 3, 6, 4, 1, -5}, // 0x72 'r'
|
||||||
|
{474, 6, 6, 6, 0, -5}, // 0x73 's'
|
||||||
|
{479, 3, 8, 3, 0, -7}, // 0x74 't'
|
||||||
|
{482, 5, 6, 7, 1, -5}, // 0x75 'u'
|
||||||
|
{486, 6, 6, 6, 0, -5}, // 0x76 'v'
|
||||||
|
{491, 8, 6, 9, 0, -5}, // 0x77 'w'
|
||||||
|
{497, 4, 6, 6, 1, -5}, // 0x78 'x'
|
||||||
|
{500, 5, 9, 6, 0, -5}, // 0x79 'y'
|
||||||
|
{506, 5, 6, 6, 0, -5}, // 0x7A 'z'
|
||||||
|
{510, 2, 12, 4, 1, -8}, // 0x7B '{'
|
||||||
|
{513, 1, 12, 3, 1, -8}, // 0x7C '|'
|
||||||
|
{515, 3, 12, 4, 0, -8}, // 0x7D '}'
|
||||||
|
{520, 5, 2, 7, 1, -4}, // 0x7E '~'
|
||||||
|
{522, 6, 9, 8, 1, -8}, //
|
||||||
|
{529, 9, 11, 9, 0, -8}, //
|
||||||
|
{542, 6, 11, 7, 1, -10}, //
|
||||||
|
{551, 0, 0, 8, 0, 0}, //
|
||||||
|
{551, 4, 9, 5, 1, -8}, //
|
||||||
|
{556, 0, 0, 8, 0, 0}, //
|
||||||
|
{556, 0, 0, 8, 0, 0}, //
|
||||||
|
{556, 0, 0, 8, 0, 0}, //
|
||||||
|
{556, 0, 0, 8, 0, 0}, //
|
||||||
|
{556, 6, 8, 8, 1, -7}, //
|
||||||
|
{562, 0, 0, 8, 0, 0}, //
|
||||||
|
{562, 11, 9, 13, 1, -8}, //
|
||||||
|
{575, 0, 0, 8, 0, 0}, //
|
||||||
|
{575, 11, 9, 12, 1, -8}, //
|
||||||
|
{588, 6, 11, 8, 1, -10}, //
|
||||||
|
{597, 9, 9, 9, 0, -8}, //
|
||||||
|
{608, 7, 11, 9, 1, -8}, //
|
||||||
|
{618, 6, 11, 7, 0, -8}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 0, 0, 8, 0, 0}, //
|
||||||
|
{627, 9, 6, 10, 0, -5}, //
|
||||||
|
{634, 0, 0, 8, 0, 0}, //
|
||||||
|
{634, 9, 6, 10, 1, -5}, //
|
||||||
|
{641, 4, 8, 6, 1, -7}, //
|
||||||
|
{645, 6, 9, 7, 0, -8}, //
|
||||||
|
{652, 5, 7, 7, 1, -5}, //
|
||||||
|
{657, 0, 0, 8, 0, 0}, //
|
||||||
|
{657, 7, 11, 7, 0, -10}, //
|
||||||
|
{667, 5, 11, 6, 0, -7}, //
|
||||||
|
{674, 5, 9, 6, 0, -8}, //
|
||||||
|
{680, 0, 0, 8, 0, 0}, //
|
||||||
|
{680, 6, 10, 7, 1, -9}, //
|
||||||
|
{688, 0, 0, 8, 0, 0}, //
|
||||||
|
{688, 0, 0, 8, 0, 0}, //
|
||||||
|
{688, 6, 11, 8, 1, -10}, //
|
||||||
|
{697, 7, 9, 9, 1, -8}, //
|
||||||
|
{705, 0, 0, 8, 0, 0}, //
|
||||||
|
{705, 0, 0, 8, 0, 0}, //
|
||||||
|
{705, 2, 12, 3, 0, -8}, //
|
||||||
|
{708, 0, 0, 8, 0, 0}, //
|
||||||
|
{708, 0, 0, 8, 0, 0}, //
|
||||||
|
{708, 3, 11, 3, 0, -10}, //
|
||||||
|
{713, 0, 0, 8, 0, 0}, //
|
||||||
|
{713, 0, 0, 8, 0, 0}, //
|
||||||
|
{713, 1, 9, 3, 1, -8}, //
|
||||||
|
{715, 1, 9, 3, 1, -8}, //
|
||||||
|
{717, 3, 8, 5, 1, -7}, //
|
||||||
|
{720, 6, 9, 7, 1, -5}, //
|
||||||
|
{727, 0, 0, 8, 0, 0}, //
|
||||||
|
{727, 0, 0, 8, 0, 0}, //
|
||||||
|
{727, 6, 9, 7, 0, -8}, //
|
||||||
|
{734, 9, 9, 11, 1, -8}, //
|
||||||
|
{745, 6, 6, 6, 0, -5}, //
|
||||||
|
{750, 0, 0, 8, 0, 0}, //
|
||||||
|
{750, 0, 0, 8, 0, 0}, //
|
||||||
|
{750, 6, 9, 8, 1, -8}, //
|
||||||
|
{757, 6, 6, 6, 0, -5}, //
|
||||||
|
{762, 3, 9, 3, 0, -8}, //
|
||||||
|
{766, 8, 9, 8, 0, -8}, //
|
||||||
|
{775, 6, 9, 8, 1, -8}, //
|
||||||
|
{782, 6, 9, 8, 1, -8}, //
|
||||||
|
{789, 6, 9, 7, 1, -8}, //
|
||||||
|
{796, 9, 11, 10, 0, -8}, //
|
||||||
|
{809, 6, 9, 8, 1, -8}, //
|
||||||
|
{816, 9, 9, 11, 1, -8}, //
|
||||||
|
{827, 6, 9, 8, 1, -8}, //
|
||||||
|
{834, 7, 9, 9, 1, -8}, //
|
||||||
|
{842, 7, 11, 9, 1, -10}, //
|
||||||
|
{852, 6, 9, 8, 1, -8}, //
|
||||||
|
{859, 7, 9, 8, 0, -8}, //
|
||||||
|
{867, 8, 9, 10, 1, -8}, //
|
||||||
|
{876, 7, 9, 9, 1, -8}, //
|
||||||
|
{884, 8, 9, 10, 1, -8}, //
|
||||||
|
{893, 7, 9, 9, 1, -8}, //
|
||||||
|
{901, 6, 9, 8, 1, -8}, //
|
||||||
|
{908, 7, 9, 9, 1, -8}, //
|
||||||
|
{916, 7, 9, 7, 0, -8}, //
|
||||||
|
{924, 7, 9, 7, 0, -8}, //
|
||||||
|
{932, 9, 9, 10, 1, -8}, //
|
||||||
|
{943, 6, 9, 8, 1, -8}, //
|
||||||
|
{950, 8, 11, 9, 1, -8}, //
|
||||||
|
{961, 6, 9, 8, 1, -8}, //
|
||||||
|
{968, 8, 9, 10, 1, -8}, //
|
||||||
|
{977, 9, 11, 10, 1, -8}, //
|
||||||
|
{990, 10, 9, 10, 0, -8}, //
|
||||||
|
{1002, 9, 9, 10, 1, -8}, //
|
||||||
|
{1013, 6, 9, 8, 1, -8}, //
|
||||||
|
{1020, 7, 9, 9, 1, -8}, //
|
||||||
|
{1028, 10, 9, 12, 1, -8}, //
|
||||||
|
{1040, 6, 9, 8, 1, -8}, //
|
||||||
|
{1047, 6, 6, 7, 0, -5}, //
|
||||||
|
{1052, 6, 9, 7, 0, -8}, //
|
||||||
|
{1059, 5, 6, 6, 1, -5}, //
|
||||||
|
{1063, 4, 6, 5, 1, -5}, //
|
||||||
|
{1066, 7, 7, 7, 0, -5}, //
|
||||||
|
{1073, 6, 6, 7, 0, -5}, //
|
||||||
|
{1078, 8, 6, 9, 1, -5}, //
|
||||||
|
{1084, 6, 6, 6, 0, -5}, //
|
||||||
|
{1089, 5, 6, 7, 1, -5}, //
|
||||||
|
{1093, 5, 8, 7, 1, -7}, //
|
||||||
|
{1098, 4, 6, 6, 1, -5}, //
|
||||||
|
{1101, 5, 6, 6, 0, -5}, //
|
||||||
|
{1105, 6, 6, 7, 1, -5}, //
|
||||||
|
{1110, 5, 6, 7, 1, -5}, //
|
||||||
|
{1114, 6, 6, 7, 0, -5}, //
|
||||||
|
{1119, 5, 6, 7, 1, -5}, //
|
||||||
|
{1123, 5, 9, 7, 1, -5}, //
|
||||||
|
{1129, 6, 6, 6, 0, -5}, //
|
||||||
|
{1134, 5, 6, 5, 0, -5}, //
|
||||||
|
{1138, 5, 9, 6, 0, -5}, //
|
||||||
|
{1144, 10, 11, 10, 0, -7}, //
|
||||||
|
{1158, 5, 6, 6, 0, -5}, //
|
||||||
|
{1162, 6, 7, 7, 1, -5}, //
|
||||||
|
{1168, 4, 6, 6, 1, -5}, //
|
||||||
|
{1171, 6, 6, 8, 1, -5}, //
|
||||||
|
{1176, 7, 7, 9, 1, -5}, //
|
||||||
|
{1183, 7, 6, 8, 0, -5}, //
|
||||||
|
{1189, 6, 6, 8, 1, -5}, //
|
||||||
|
{1194, 5, 6, 6, 1, -5}, //
|
||||||
|
{1198, 5, 6, 6, 1, -5}, //
|
||||||
|
{1202, 8, 6, 9, 1, -5}, //
|
||||||
|
{1208, 5, 6, 7, 1, -5} //
|
||||||
|
};
|
||||||
|
|
||||||
|
const GFXfont FreeSans6pt8bCyrillic PROGMEM = {(uint8_t *)FreeSans6pt8bCyrillicBitmaps, (GFXglyph *)FreeSans6pt8bCyrillicGlyphs,
|
||||||
|
0x20, 0xFF, 16};
|
4
src/graphics/niche/Fonts/README.md
Normal file
4
src/graphics/niche/Fonts/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# NicheGraphics - Fonts
|
||||||
|
|
||||||
|
A common area to store fonts which might be reused by different Niche Graphics UIs
|
||||||
|
In future, we may want to separate these by library (AdafruitGFX, u8g2, etc)
|
843
src/graphics/niche/InkHUD/Applet.cpp
Normal file
843
src/graphics/niche/InkHUD/Applet.cpp
Normal file
@ -0,0 +1,843 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./Applet.h"
|
||||||
|
|
||||||
|
#include "RTC.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose font. Set by setDefaultFonts
|
||||||
|
InkHUD::AppletFont InkHUD::Applet::fontSmall; // General purpose font. Set by setDefaultFonts
|
||||||
|
constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo
|
||||||
|
|
||||||
|
InkHUD::Applet::Applet() : GFX(0, 0)
|
||||||
|
{
|
||||||
|
// GFX is given initial dimensions of 0
|
||||||
|
// The width and height will change dynamically, depending on Applet tiling
|
||||||
|
// If you're getting a "divide by zero error", consider it an assert:
|
||||||
|
// WindowManager should be the only one controlling the rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
// The raw pixel output generated by AdafruitGFX drawing
|
||||||
|
// Hand off to the applet's tile, which will in-turn pass to the window manager
|
||||||
|
void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
|
||||||
|
{
|
||||||
|
// Only render pixels if they fall within user's cropped region
|
||||||
|
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
|
||||||
|
assignedTile->handleAppletPixel(x, y, (Color)color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets which tile the applet renders for
|
||||||
|
// Pixel output is passed to tile during render()
|
||||||
|
// This should only be called by Tile::assignApplet
|
||||||
|
void InkHUD::Applet::setTile(Tile *t)
|
||||||
|
{
|
||||||
|
// If we're setting (not clearing), make sure the link is "reciprocal"
|
||||||
|
if (t)
|
||||||
|
assert(t->getAssignedApplet() == this);
|
||||||
|
|
||||||
|
assignedTile = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which tile will the applet render() to?
|
||||||
|
InkHUD::Tile *InkHUD::Applet::getTile()
|
||||||
|
{
|
||||||
|
return assignedTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::Applet::render()
|
||||||
|
{
|
||||||
|
assert(assignedTile); // Ensure that we have a tile
|
||||||
|
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
|
||||||
|
|
||||||
|
wantRender = false; // Clear the flag set by requestUpdate
|
||||||
|
wantAutoshow = false; // If we're rendering now, it means our request was considered. It may or may not have been granted.
|
||||||
|
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Our requested type has been considered by now. Tidy up.
|
||||||
|
|
||||||
|
updateDimensions();
|
||||||
|
resetDrawingSpace();
|
||||||
|
onRender(); // Derived applet's drawing takes place here
|
||||||
|
|
||||||
|
// If our tile is (or was) highlighted, to indicate a change in focus
|
||||||
|
if (Tile::highlightTarget == assignedTile) {
|
||||||
|
// Draw the highlight
|
||||||
|
if (!Tile::highlightShown) {
|
||||||
|
drawRect(0, 0, width(), height(), BLACK);
|
||||||
|
Tile::startHighlightTimeout();
|
||||||
|
Tile::highlightShown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the highlight
|
||||||
|
else {
|
||||||
|
Tile::cancelHighlightTimeout();
|
||||||
|
Tile::highlightShown = false;
|
||||||
|
Tile::highlightTarget = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the applet want to render now?
|
||||||
|
// Checks whether the applet called requestUpdate() recently, in response to an event
|
||||||
|
bool InkHUD::Applet::wantsToRender()
|
||||||
|
{
|
||||||
|
return wantRender;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the applet want to be moved to foreground before next render, to show new data?
|
||||||
|
// User specifies whether an applet has permission for this, using the on-screen menu
|
||||||
|
bool InkHUD::Applet::wantsToAutoshow()
|
||||||
|
{
|
||||||
|
return wantAutoshow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which technique would this applet prefer that the display use to change the image?
|
||||||
|
Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
||||||
|
{
|
||||||
|
return wantUpdateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get size of the applet's drawing space from its tile
|
||||||
|
void InkHUD::Applet::updateDimensions()
|
||||||
|
{
|
||||||
|
assert(assignedTile);
|
||||||
|
WIDTH = assignedTile->getWidth();
|
||||||
|
HEIGHT = assignedTile->getHeight();
|
||||||
|
_width = WIDTH;
|
||||||
|
_height = HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that render() always starts with the same initial drawing config
|
||||||
|
void InkHUD::Applet::resetDrawingSpace()
|
||||||
|
{
|
||||||
|
resetCrop(); // Allow pixel from any region of the applet to draw
|
||||||
|
setTextColor(BLACK); // Reset text params
|
||||||
|
setCursor(0, 0);
|
||||||
|
setTextWrap(false);
|
||||||
|
setFont(AppletFont()); // Restore the default AdafruitGFX font
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the window manager that we want to render now
|
||||||
|
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
|
||||||
|
// When an applet decides it has heard something important, and wants to redraw, it calls this method
|
||||||
|
// Once the window manager has given other applets a chance to process whatever event we just detected,
|
||||||
|
// it will run Applet::render(), which may draw our applet to screen, if it is shown (forgeround)
|
||||||
|
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
|
||||||
|
{
|
||||||
|
wantRender = true;
|
||||||
|
wantUpdateType = type;
|
||||||
|
WindowManager::getInstance()->requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask window manager to move this applet to foreground at start of next render
|
||||||
|
// Users select which applets have permission for this using the on-screen menu
|
||||||
|
void InkHUD::Applet::requestAutoshow()
|
||||||
|
{
|
||||||
|
wantAutoshow = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when an Applet begins running
|
||||||
|
// Active applets are considered "enabled"
|
||||||
|
// They should now listen for events, and request their own updates
|
||||||
|
// They may also be force rendered by the window manager at any time
|
||||||
|
// Applets can be activated at run-time through the on-screen menu
|
||||||
|
void InkHUD::Applet::activate()
|
||||||
|
{
|
||||||
|
onActivate(); // Call derived class' handler
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when an Applet stop running
|
||||||
|
// Inactive applets are considered "disabled"
|
||||||
|
// They should not listen for events, process data
|
||||||
|
// They will not be rendered
|
||||||
|
// Applets can be deactivated at run-time through the on-screen menu
|
||||||
|
void InkHUD::Applet::deactivate()
|
||||||
|
{
|
||||||
|
// If applet is still in foreground, run its onBackground code first
|
||||||
|
if (isForeground())
|
||||||
|
sendToBackground();
|
||||||
|
|
||||||
|
// If applet is active, run its onDeactivate code first
|
||||||
|
if (isActive())
|
||||||
|
onDeactivate(); // Derived class' handler
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the Applet running?
|
||||||
|
// Note: active / inactive is not related to background / foreground
|
||||||
|
// An inactive applet is *fully* disabled
|
||||||
|
bool InkHUD::Applet::isActive()
|
||||||
|
{
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin showing the Applet
|
||||||
|
// It will be rendered immediately to whichever tile it is assigned
|
||||||
|
// The window manager will also now honor requestUpdate() calls from this applet
|
||||||
|
void InkHUD::Applet::bringToForeground()
|
||||||
|
{
|
||||||
|
if (!foreground) {
|
||||||
|
foreground = true;
|
||||||
|
onForeground(); // Run derived applet class' handler
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop showing the Applet
|
||||||
|
// Calls to requestUpdate() will no longer be honored
|
||||||
|
// When one applet moves to background, another should move to foreground
|
||||||
|
void InkHUD::Applet::sendToBackground()
|
||||||
|
{
|
||||||
|
if (foreground) {
|
||||||
|
foreground = false;
|
||||||
|
onBackground(); // Run derived applet class' handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the applet currently displayed on a tile
|
||||||
|
bool InkHUD::Applet::isForeground()
|
||||||
|
{
|
||||||
|
return foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit drawing to a certain region of the applet
|
||||||
|
// Pixels outside this region will be discarded
|
||||||
|
void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height)
|
||||||
|
{
|
||||||
|
cropLeft = left;
|
||||||
|
cropTop = top;
|
||||||
|
cropWidth = width;
|
||||||
|
cropHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow drawing to any region of the Applet
|
||||||
|
// Reverses Applet::setCrop
|
||||||
|
void InkHUD::Applet::resetCrop()
|
||||||
|
{
|
||||||
|
setCrop(0, 0, width(), height());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert relative width to absolute width, in px
|
||||||
|
// X(0) is 0
|
||||||
|
// X(0.5) is width() / 2
|
||||||
|
// X(1) is width()
|
||||||
|
uint16_t InkHUD::Applet::X(float f)
|
||||||
|
{
|
||||||
|
return width() * f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert relative hight to absolute height, in px
|
||||||
|
// Y(0) is 0
|
||||||
|
// Y(0.5) is height() / 2
|
||||||
|
// Y(1) is height()
|
||||||
|
uint16_t InkHUD::Applet::Y(float f)
|
||||||
|
{
|
||||||
|
return height() * f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print text, specifying the position of any edge / corner of the textbox
|
||||||
|
void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va)
|
||||||
|
{
|
||||||
|
printAt(x, y, std::string(text), ha, va);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print text, specifying the position of any edge / corner of the textbox
|
||||||
|
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
|
||||||
|
{
|
||||||
|
// Custom font
|
||||||
|
// - set with AppletFont::addSubstitution
|
||||||
|
// - find certain UTF8 chars
|
||||||
|
// - replace with glpyh from custom font (or suitable ASCII addSubstitution?)
|
||||||
|
getFont().applySubstitutions(&text);
|
||||||
|
|
||||||
|
// We do still have to run getTextBounds to find the width
|
||||||
|
int16_t textOffsetX, textOffsetY;
|
||||||
|
uint16_t textWidth, textHeight;
|
||||||
|
getTextBounds(text.c_str(), 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight);
|
||||||
|
|
||||||
|
int16_t cursorX = 0;
|
||||||
|
int16_t cursorY = 0;
|
||||||
|
|
||||||
|
switch (ha) {
|
||||||
|
case LEFT:
|
||||||
|
cursorX = x - textOffsetX;
|
||||||
|
break;
|
||||||
|
case CENTER:
|
||||||
|
cursorX = (x - textOffsetX) - (textWidth / 2);
|
||||||
|
break;
|
||||||
|
case RIGHT:
|
||||||
|
cursorX = (x - textOffsetX) - textWidth;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're using a fixed line height (getFontDimensions), rather than sizing to text (getTextBounds)
|
||||||
|
// Note: the FontDimensions values for this are unsigned
|
||||||
|
|
||||||
|
switch (va) {
|
||||||
|
case TOP:
|
||||||
|
cursorY = y + currentFont.heightAboveCursor();
|
||||||
|
break;
|
||||||
|
case MIDDLE:
|
||||||
|
cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2);
|
||||||
|
break;
|
||||||
|
case BOTTOM:
|
||||||
|
cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCursor(cursorX, cursorY);
|
||||||
|
print(text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set which font should be used for subsequent drawing
|
||||||
|
// This is AppletFont type, which is a wrapper for AdfruitGFX font, with some precalculated dimension data
|
||||||
|
void InkHUD::Applet::setFont(AppletFont f)
|
||||||
|
{
|
||||||
|
GFX::setFont(f.gfxFont);
|
||||||
|
currentFont = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get which font is currently being used for drawing
|
||||||
|
// This is AppletFont type, which is a wrapper for AdfruitGFX font, with some precalculated dimension data
|
||||||
|
InkHUD::AppletFont InkHUD::Applet::getFont()
|
||||||
|
{
|
||||||
|
return currentFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set two general-purpose fonts, which are reused by many applets
|
||||||
|
// Applets are also permitted to use other fonts, if they can justify the flash usage
|
||||||
|
void InkHUD::Applet::setDefaultFonts(AppletFont large, AppletFont small)
|
||||||
|
{
|
||||||
|
Applet::fontSmall = small;
|
||||||
|
Applet::fontLarge = large;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets rendered width of a string
|
||||||
|
// Wrapper for getTextBounds
|
||||||
|
uint16_t InkHUD::Applet::getTextWidth(const char *text)
|
||||||
|
{
|
||||||
|
|
||||||
|
// We do still have to run getTextBounds to find the width
|
||||||
|
int16_t textOffsetX, textOffsetY;
|
||||||
|
uint16_t textWidth, textHeight;
|
||||||
|
getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight);
|
||||||
|
|
||||||
|
return textWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets rendered width of a string
|
||||||
|
// Wrappe for getTextBounds
|
||||||
|
uint16_t InkHUD::Applet::getTextWidth(std::string text)
|
||||||
|
{
|
||||||
|
getFont().applySubstitutions(&text);
|
||||||
|
|
||||||
|
return getTextWidth(text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels
|
||||||
|
// Roughly comparable to values used by the iOS app;
|
||||||
|
// I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator
|
||||||
|
InkHUD::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi)
|
||||||
|
{
|
||||||
|
uint8_t score = 0;
|
||||||
|
|
||||||
|
// Give a score for the SNR
|
||||||
|
if (snr > -17.5)
|
||||||
|
score += 2;
|
||||||
|
else if (snr > -26.0)
|
||||||
|
score += 1;
|
||||||
|
|
||||||
|
// Give a score for the RSSI
|
||||||
|
if (rssi > -115.0)
|
||||||
|
score += 3;
|
||||||
|
else if (rssi > -120.0)
|
||||||
|
score += 2;
|
||||||
|
else if (rssi > -126.0)
|
||||||
|
score += 1;
|
||||||
|
|
||||||
|
// Combine scores, then give a result
|
||||||
|
if (score >= 5)
|
||||||
|
return SIGNAL_GOOD;
|
||||||
|
else if (score >= 4)
|
||||||
|
return SIGNAL_FAIR;
|
||||||
|
else if (score > 0)
|
||||||
|
return SIGNAL_BAD;
|
||||||
|
else
|
||||||
|
return SIGNAL_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the standard "node id" formatting to a nodenum int: !0123abdc
|
||||||
|
std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
|
||||||
|
{
|
||||||
|
// Not found in nodeDB, show a hex nodeid instead
|
||||||
|
char nodeIdHex[10];
|
||||||
|
sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format
|
||||||
|
return std::string(nodeIdHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
|
||||||
|
{
|
||||||
|
// Custom font glyphs
|
||||||
|
// - set with AppletFont::addSubstitution
|
||||||
|
// - find certain UTF8 chars
|
||||||
|
// - replace with glpyh from custom font (or suitable ASCII addSubstitution?)
|
||||||
|
getFont().applySubstitutions(&text);
|
||||||
|
|
||||||
|
// Place the AdafruitGFX cursor to suit our "top" coord
|
||||||
|
setCursor(left, top + getFont().heightAboveCursor());
|
||||||
|
|
||||||
|
// How wide a space character is
|
||||||
|
// Used when simulating print, for dimensioning
|
||||||
|
// Works around issues where getTextDimensions() doesn't account for whitespace
|
||||||
|
const uint8_t wSp = getFont().widthBetweenWords();
|
||||||
|
|
||||||
|
// Move through our text, character by character
|
||||||
|
uint16_t wordStart = 0;
|
||||||
|
for (uint16_t i = 0; i < text.length(); i++) {
|
||||||
|
|
||||||
|
// Found: end of word (split by spaces or newline)
|
||||||
|
// Also handles end of string
|
||||||
|
if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) {
|
||||||
|
// Isolate this word
|
||||||
|
uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1
|
||||||
|
std::string word = text.substr(wordStart, wordLength);
|
||||||
|
wordStart = i + 1; // Next word starts *after* the space
|
||||||
|
|
||||||
|
// If word is terminated by a newline char, don't actually print it.
|
||||||
|
// We'll manually add a new line later
|
||||||
|
if (word.back() == '\n')
|
||||||
|
word.pop_back();
|
||||||
|
|
||||||
|
// Measure the word, in px
|
||||||
|
int16_t l, t;
|
||||||
|
uint16_t w, h;
|
||||||
|
getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h);
|
||||||
|
|
||||||
|
// Word is short
|
||||||
|
if (w < width) {
|
||||||
|
// Word fits on current line
|
||||||
|
if ((l + w + wSp) < left + width)
|
||||||
|
print(word.c_str());
|
||||||
|
|
||||||
|
// Word doesn't fit on current line
|
||||||
|
else {
|
||||||
|
setCursor(left, getCursorY() + getFont().lineHeight()); // Newline
|
||||||
|
print(word.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word is really long
|
||||||
|
// (wider than applet)
|
||||||
|
else {
|
||||||
|
// Horribly inefficient:
|
||||||
|
// Rather than working directly with the glyph sizes,
|
||||||
|
// we're going to run everything through getTextBounds as a c-string of length 1
|
||||||
|
// This is because AdafruitGFX has special internal handling for their legacy 6x8 font,
|
||||||
|
// which would be a pain to add manually here.
|
||||||
|
// These super-long strings probably don't come up often so we can maybe tolerate this.
|
||||||
|
|
||||||
|
// Todo: rewrite making use of AdafruitGFX native text wrapping
|
||||||
|
char cstr[] = {0, 0};
|
||||||
|
int16_t l, t;
|
||||||
|
uint16_t w, h;
|
||||||
|
for (uint16_t c = 0; c < word.length(); c++) {
|
||||||
|
// Shove next char into a c string
|
||||||
|
cstr[0] = word[c];
|
||||||
|
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
|
||||||
|
|
||||||
|
// Manual newline, if next character will spill beyond screen edge
|
||||||
|
if ((l + w) > left + width)
|
||||||
|
setCursor(left, getCursorY() + getFont().lineHeight());
|
||||||
|
|
||||||
|
// Print next character
|
||||||
|
print(word[c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If word was terminated by a newline char, manually add the new line now
|
||||||
|
if (text[i] == '\n') {
|
||||||
|
setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline
|
||||||
|
wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate running printWrapped, to determine how tall the block of text will be.
|
||||||
|
// This is a wasteful way of handling things. Maybe some way to optimize in future?
|
||||||
|
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
|
||||||
|
{
|
||||||
|
// Cache the current crop region
|
||||||
|
int16_t cL = cropLeft;
|
||||||
|
int16_t cT = cropTop;
|
||||||
|
uint16_t cW = cropWidth;
|
||||||
|
uint16_t cH = cropHeight;
|
||||||
|
|
||||||
|
setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels
|
||||||
|
printWrapped(left, 0, width, text); // Simulate only - no pixels drawn
|
||||||
|
|
||||||
|
// Restore previous crop region
|
||||||
|
cropLeft = cL;
|
||||||
|
cropTop = cT;
|
||||||
|
cropWidth = cW;
|
||||||
|
cropHeight = cH;
|
||||||
|
|
||||||
|
// Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val,
|
||||||
|
// so we need to account for that when determining the height
|
||||||
|
return (getCursorY() + getFont().heightBelowCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill a region with sparse diagonal lines, to create a pseudo-translucent fill
|
||||||
|
void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color)
|
||||||
|
{
|
||||||
|
// Cache the currently cropped region
|
||||||
|
int16_t oldCropL = cropLeft;
|
||||||
|
int16_t oldCropT = cropTop;
|
||||||
|
uint16_t oldCropW = cropWidth;
|
||||||
|
uint16_t oldCropH = cropHeight;
|
||||||
|
|
||||||
|
setCrop(x, y, w, h);
|
||||||
|
|
||||||
|
// Draw lines starting along the top edge, every few px
|
||||||
|
for (int16_t ix = x; ix < x + w; ix += spacing) {
|
||||||
|
for (int16_t i = 0; i < w || i < h; i++) {
|
||||||
|
drawPixel(ix + i, y + i, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw lines starting along the left edge, every few px
|
||||||
|
for (int16_t iy = y; iy < y + h; iy += spacing) {
|
||||||
|
for (int16_t i = 0; i < w || i < h; i++) {
|
||||||
|
drawPixel(x + i, iy + i, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore any previous crop
|
||||||
|
// If none was set, this will clear
|
||||||
|
cropLeft = oldCropL;
|
||||||
|
cropTop = oldCropT;
|
||||||
|
cropWidth = oldCropW;
|
||||||
|
cropHeight = oldCropH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a human readable time representation of an epoch time (seconds since 1970)
|
||||||
|
// If time is invalid, this will be an empty string
|
||||||
|
std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds)
|
||||||
|
{
|
||||||
|
#ifdef BUILD_EPOCH
|
||||||
|
constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build
|
||||||
|
#else
|
||||||
|
constexpr uint32_t validAfterEpoch = 1727740800 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to October 1, 2024 12:00:00 AM GMT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||||
|
|
||||||
|
int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY;
|
||||||
|
int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR;
|
||||||
|
|
||||||
|
// Times are invalid: rtc is much older than when code was built
|
||||||
|
// Don't give any human readable string
|
||||||
|
if (epochNow <= validAfterEpoch) {
|
||||||
|
LOG_DEBUG("RTC prior to buildtime");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times are invalid: argument time is significantly ahead of RTC
|
||||||
|
// Don't give any human readable string
|
||||||
|
if (daysAgo < -2) {
|
||||||
|
LOG_DEBUG("RTC in future");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times are probably invalid: more than 6 months ago
|
||||||
|
if (daysAgo > 6 * 30) {
|
||||||
|
LOG_DEBUG("RTC val > 6 months old");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysAgo > 1)
|
||||||
|
return to_string(daysAgo) + " days ago";
|
||||||
|
|
||||||
|
else if (hoursAgo > 18)
|
||||||
|
return "Yesterday";
|
||||||
|
|
||||||
|
else {
|
||||||
|
|
||||||
|
uint32_t hms = epochSeconds % SEC_PER_DAY;
|
||||||
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
|
// Tear apart hms into h:m
|
||||||
|
uint32_t hour = hms / SEC_PER_HOUR;
|
||||||
|
uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
|
||||||
|
// Format the clock string
|
||||||
|
char clockStr[11];
|
||||||
|
sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM");
|
||||||
|
|
||||||
|
return clockStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no argument specified, get time string for the current RTC time
|
||||||
|
std::string InkHUD::Applet::getTimeString()
|
||||||
|
{
|
||||||
|
return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how many nodes have been seen within our preferred window of activity
|
||||||
|
// This period is set by user, via the menu
|
||||||
|
// Todo: optimize to calculate once only per WindowManager::render
|
||||||
|
uint16_t InkHUD::Applet::getActiveNodeCount()
|
||||||
|
{
|
||||||
|
// Don't even try to count nodes if RTC isn't set
|
||||||
|
// The last heard values in nodedb will be incomprehensible
|
||||||
|
if (getRTCQuality() == RTCQualityNone)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
uint16_t count = 0;
|
||||||
|
|
||||||
|
// For each node in db
|
||||||
|
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Check if heard recently, and not our own node
|
||||||
|
if (sinceLastSeen(node) < settings.recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an abbreviated, human readable, distance string
|
||||||
|
// Honors config.display.units, to offer both metric and imperial
|
||||||
|
std::string InkHUD::Applet::localizeDistance(uint32_t meters)
|
||||||
|
{
|
||||||
|
constexpr float FEET_PER_METER = 3.28084;
|
||||||
|
constexpr uint16_t FEET_PER_MILE = 5280;
|
||||||
|
|
||||||
|
// Resulting string
|
||||||
|
std::string localized;
|
||||||
|
|
||||||
|
// Imeperial
|
||||||
|
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||||
|
uint32_t feet = meters * FEET_PER_METER;
|
||||||
|
// Distant (miles, rounded)
|
||||||
|
if (feet > FEET_PER_MILE / 2) {
|
||||||
|
localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE));
|
||||||
|
localized += "mi";
|
||||||
|
}
|
||||||
|
// Nearby (feet)
|
||||||
|
else {
|
||||||
|
localized += to_string(feet);
|
||||||
|
localized += "ft";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metric
|
||||||
|
else {
|
||||||
|
// Distant (kilometers, rounded)
|
||||||
|
if (meters >= 500) {
|
||||||
|
localized += to_string((uint32_t)roundf(meters / 1000.0));
|
||||||
|
localized += "km";
|
||||||
|
}
|
||||||
|
// Nearby (meters)
|
||||||
|
else {
|
||||||
|
localized += to_string(meters);
|
||||||
|
localized += "m";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
|
||||||
|
{
|
||||||
|
// How many times to draw along x axis
|
||||||
|
int16_t xStart;
|
||||||
|
int16_t xEnd;
|
||||||
|
switch (thicknessX) {
|
||||||
|
case 0:
|
||||||
|
assert(false);
|
||||||
|
case 1:
|
||||||
|
xStart = xCenter;
|
||||||
|
xEnd = xCenter;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
xStart = xCenter;
|
||||||
|
xEnd = xCenter + 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
xStart = xCenter - (thicknessX / 2);
|
||||||
|
xEnd = xCenter + (thicknessX / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many times to draw along Y axis
|
||||||
|
int16_t yStart;
|
||||||
|
int16_t yEnd;
|
||||||
|
switch (thicknessY) {
|
||||||
|
case 0:
|
||||||
|
assert(false);
|
||||||
|
case 1:
|
||||||
|
yStart = yCenter;
|
||||||
|
yEnd = yCenter;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
yStart = yCenter;
|
||||||
|
yEnd = yCenter + 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
yStart = yCenter - (thicknessY / 2);
|
||||||
|
yEnd = yCenter + (thicknessY / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print multiple times, overlapping
|
||||||
|
for (int16_t x = xStart; x <= xEnd; x++) {
|
||||||
|
for (int16_t y = yStart; y <= yEnd; y++) {
|
||||||
|
printAt(x, y, text, CENTER, MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow this applet to suppress notifications
|
||||||
|
// Asked before a notification is shown via the NotificationApplet
|
||||||
|
// An applet might want to suppress a notification if the applet itself already displays this info
|
||||||
|
// Example: AllMessageApplet should not approve notifications for messages, if it is in foreground
|
||||||
|
bool InkHUD::Applet::approveNotification(InkHUD::Notification &n)
|
||||||
|
{
|
||||||
|
// By default, no objection
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the standard header, used by most Applets
|
||||||
|
void InkHUD::Applet::drawHeader(std::string text)
|
||||||
|
{
|
||||||
|
setFont(fontSmall);
|
||||||
|
|
||||||
|
// Y position for divider
|
||||||
|
// - between header text and messages
|
||||||
|
constexpr int16_t padDivH = 2;
|
||||||
|
const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1;
|
||||||
|
|
||||||
|
// Print header
|
||||||
|
printAt(0, padDivH, text);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
// - below header text: separates message
|
||||||
|
// - above header text: separates other applets
|
||||||
|
for (int16_t x = 0; x < width(); x += 2) {
|
||||||
|
drawPixel(x, 0, BLACK);
|
||||||
|
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the height of the standard applet header
|
||||||
|
// This will vary, depending on font
|
||||||
|
// Applets use this value to avoid drawing overtop the header
|
||||||
|
uint16_t InkHUD::Applet::getHeaderHeight()
|
||||||
|
{
|
||||||
|
// Y position for divider
|
||||||
|
// - between header text and messages
|
||||||
|
constexpr int16_t padDivH = 2;
|
||||||
|
const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1;
|
||||||
|
|
||||||
|
return headerDivY + 1; // "Plus one": height is always one more than Y position
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio
|
||||||
|
uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight)
|
||||||
|
{
|
||||||
|
// Determine whether we're limited by width or height
|
||||||
|
// Makes sure we draw the logo as large as possible, within the specified region,
|
||||||
|
// while still maintaining correct aspect ratio
|
||||||
|
if (limitWidth > limitHeight * LOGO_ASPECT_RATIO)
|
||||||
|
return limitHeight * LOGO_ASPECT_RATIO;
|
||||||
|
else
|
||||||
|
return limitWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio
|
||||||
|
uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight)
|
||||||
|
{
|
||||||
|
// Determine whether we're limited by width or height
|
||||||
|
// Makes sure we draw the logo as large as possible, within the specified region,
|
||||||
|
// while still maintaining correct aspect ratio
|
||||||
|
if (limitHeight > limitWidth / LOGO_ASPECT_RATIO)
|
||||||
|
return limitWidth / LOGO_ASPECT_RATIO;
|
||||||
|
else
|
||||||
|
return limitHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a scalable Meshtastic logo
|
||||||
|
// Make sure to provide dimensions which have the correct aspect ratio (~2)
|
||||||
|
// Three paths, drawn thick using quads, with one corner "radiused"
|
||||||
|
void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height)
|
||||||
|
{
|
||||||
|
struct Point {
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
};
|
||||||
|
typedef Point Distance;
|
||||||
|
|
||||||
|
int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org.
|
||||||
|
int16_t logoL = centerX - (width / 2) + (logoTh / 2);
|
||||||
|
int16_t logoT = centerY - (height / 2) + (logoTh / 2);
|
||||||
|
int16_t logoW = width - logoTh;
|
||||||
|
int16_t logoH = height - logoTh;
|
||||||
|
int16_t logoR = logoL + logoW - 1;
|
||||||
|
int16_t logoB = logoT + logoH - 1;
|
||||||
|
|
||||||
|
// Points for paths (a, b, and c)
|
||||||
|
Point a1 = {map(0, 0, 3, logoL, logoR), logoB};
|
||||||
|
Point a2 = {map(1, 0, 3, logoL, logoR), logoT};
|
||||||
|
Point b1 = {map(1, 0, 3, logoL, logoR), logoB};
|
||||||
|
Point b2 = {map(2, 0, 3, logoL, logoR), logoT};
|
||||||
|
Point c1 = {map(2, 0, 3, logoL, logoR), logoT};
|
||||||
|
Point c2 = {map(3, 0, 3, logoL, logoR), logoB};
|
||||||
|
|
||||||
|
// Find right-angle to the path
|
||||||
|
// Used to thicken the single pixel paths
|
||||||
|
Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)};
|
||||||
|
float angle = tanh((float)deltaA.y / deltaA.x);
|
||||||
|
|
||||||
|
// Distance {at right angle from the paths), which will give corners for our "quads"
|
||||||
|
// The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner
|
||||||
|
Distance fromPath;
|
||||||
|
fromPath.x = cos(radians(90) - angle) * logoTh * 0.5;
|
||||||
|
fromPath.y = sin(radians(90) - angle) * logoTh * 0.5;
|
||||||
|
|
||||||
|
// Make the path thick: path a becomes quad a
|
||||||
|
Point aq1{a1.x - fromPath.x, a1.y - fromPath.y};
|
||||||
|
Point aq2{a2.x - fromPath.x, a2.y - fromPath.y};
|
||||||
|
Point aq3{a2.x + fromPath.x, a2.y + fromPath.y};
|
||||||
|
Point aq4{a1.x + fromPath.x, a1.y + fromPath.y};
|
||||||
|
fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, BLACK);
|
||||||
|
fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, BLACK);
|
||||||
|
|
||||||
|
// Make the path thick: path b becomes quad b
|
||||||
|
Point bq1{b1.x - fromPath.x, b1.y - fromPath.y};
|
||||||
|
Point bq2{b2.x - fromPath.x, b2.y - fromPath.y};
|
||||||
|
Point bq3{b2.x + fromPath.x, b2.y + fromPath.y};
|
||||||
|
Point bq4{b1.x + fromPath.x, b1.y + fromPath.y};
|
||||||
|
fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, BLACK);
|
||||||
|
fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, BLACK);
|
||||||
|
|
||||||
|
// Make the path hick: path c becomes quad c
|
||||||
|
Point cq1{c1.x - fromPath.x, c1.y + fromPath.y};
|
||||||
|
Point cq2{c2.x - fromPath.x, c2.y + fromPath.y};
|
||||||
|
Point cq3{c2.x + fromPath.x, c2.y - fromPath.y};
|
||||||
|
Point cq4{c1.x + fromPath.x, c1.y - fromPath.y};
|
||||||
|
fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, BLACK);
|
||||||
|
fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, BLACK);
|
||||||
|
|
||||||
|
// Radius the intersection of quad b and quad c
|
||||||
|
// Don't attempt if logo is tiny
|
||||||
|
if (logoTh > 3) {
|
||||||
|
// The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding
|
||||||
|
// We get better results just rederiving it
|
||||||
|
int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2));
|
||||||
|
fillCircle(b2.x, b2.y, capRad, BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
234
src/graphics/niche/InkHUD/Applet.h
Normal file
234
src/graphics/niche/InkHUD/Applet.h
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Base class for InkHUD applets
|
||||||
|
Must be overriden
|
||||||
|
|
||||||
|
An applet is one "program" which may show info on the display.
|
||||||
|
|
||||||
|
===================================
|
||||||
|
Preliminary notes, for the curious
|
||||||
|
===================================
|
||||||
|
|
||||||
|
(This info to be streamlined, and moved to a more official documentation)
|
||||||
|
|
||||||
|
User Applets vs System Applets
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
There are either "User Applets", or "System Applets".
|
||||||
|
This concept is only for our understanding; as far at the code is concerned, both are just "Applets"
|
||||||
|
|
||||||
|
User applets are the "normal" applets.
|
||||||
|
User applets are applets like "AllMessageApplet", or "MapApplet".
|
||||||
|
User applets may be enabled / disabled by user, via the on-screen menu.
|
||||||
|
Incorporating new UserApplets is easy: just add them during setupNicheGraphics
|
||||||
|
If a UserApplet is not added during setupNicheGraphics, it will not be built.
|
||||||
|
The set of available UserApplets is allowed to vary from device to device.
|
||||||
|
|
||||||
|
|
||||||
|
Examples of system applets include "NotificationApplet" and "MenuApplet".
|
||||||
|
For their own reasons, system applets each require some amount of special handling.
|
||||||
|
|
||||||
|
Drawing
|
||||||
|
--------
|
||||||
|
|
||||||
|
*All* drawing must be performed by an Applet.
|
||||||
|
Applets implement the onRender() method, where all drawing takes place.
|
||||||
|
Applets are told how wide and tall they are, and are expected to draw to suit this size.
|
||||||
|
When an applet draws, it uses co-ordinates in "Applet Space": between 0 and applet width/height.
|
||||||
|
|
||||||
|
Event-driven rendering
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Applets don't render unless something on the display needs to change.
|
||||||
|
An applet is expected to determine for itself when it has new info to display.
|
||||||
|
It should interact with the firmware via the MeshModule API, via Observables, etc.
|
||||||
|
Please don't directly add hooks throughout the existing firmware code.
|
||||||
|
|
||||||
|
When an applet decides it would like to update the display, it should call requestUpdate()
|
||||||
|
The WindowManager will shortly call the onRender() method for all affected applets
|
||||||
|
|
||||||
|
An Applet may be unexpectedly asked to render at any point in time.
|
||||||
|
|
||||||
|
Applets should cache their data, but not their pixel output: they should re-render when onRender runs.
|
||||||
|
An Applet's dimensions are not know until onRender is called, so pre-rendering of UI elements is prohibited.
|
||||||
|
|
||||||
|
Tiles
|
||||||
|
-----
|
||||||
|
|
||||||
|
Applets are assigned to "Tiles".
|
||||||
|
Assigning an applet to a tile creates a reciprocal link between the two.
|
||||||
|
When an applet renders, it passes pixels to its tile.
|
||||||
|
The tile translates these to the correct position, to be placed into the fullscreen framebuffer.
|
||||||
|
User applets don't get to choose their own tile; the multiplexing is handled by the WindowManager.
|
||||||
|
System applets might do strange things though.
|
||||||
|
|
||||||
|
Foreground and Background
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
The user can cycle between applets by short-pressing the user button.
|
||||||
|
Any applets which are currently displayed on the display are "foreground".
|
||||||
|
When the user button is short pressed, and an applet is hidden, it becomes "background".
|
||||||
|
|
||||||
|
Although the WindowManager will not render background applets, they should still collect data,
|
||||||
|
so they are ready to display when they are brought to foreground again.
|
||||||
|
Even if they are in background, Applets should still request updates when an event affects them,
|
||||||
|
as the user may have given them permission to "autoshow"; bringing themselves foreground automatically
|
||||||
|
|
||||||
|
Applets can implement the onForeground and onBackground methods to handle this change in state.
|
||||||
|
They can also check their state by calling isForeground() at any time.
|
||||||
|
|
||||||
|
Active and Inactive
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The user can select which applets are available, using the onscreen applet selection menu.
|
||||||
|
Applets which are enabled in this menu are "active"; otherwise they are "inactive".
|
||||||
|
|
||||||
|
An inactive applet is expected not collect data; not to consume resources.
|
||||||
|
Applets are activated at boot, or when enabled via the menu.
|
||||||
|
They are deactivated at shutdown, or when disabled via the menu.
|
||||||
|
|
||||||
|
Applets can implement the onActivation and onDeactivation methods to handle this change in state.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include <GFX.h>
|
||||||
|
|
||||||
|
#include "./AppletFont.h"
|
||||||
|
#include "./Applets/System/Notification/Notification.h"
|
||||||
|
#include "./Tile.h"
|
||||||
|
#include "./Types.h"
|
||||||
|
#include "./WindowManager.h"
|
||||||
|
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
using NicheGraphics::Drivers::EInk;
|
||||||
|
using std::to_string;
|
||||||
|
|
||||||
|
class Tile;
|
||||||
|
class WindowManager;
|
||||||
|
|
||||||
|
class Applet : public GFX
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Applet();
|
||||||
|
|
||||||
|
void setTile(Tile *t); // Applets draw via a tile (for multiplexing)
|
||||||
|
Tile *getTile();
|
||||||
|
|
||||||
|
void render();
|
||||||
|
bool wantsToRender(); // Check whether applet wants to render
|
||||||
|
bool wantsToAutoshow(); // Check whether applets wants to become foreground, to show new data, if permitted
|
||||||
|
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||||
|
void updateDimensions(); // Get current size from tile
|
||||||
|
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||||
|
|
||||||
|
// Change the applet's state
|
||||||
|
|
||||||
|
void activate();
|
||||||
|
void deactivate();
|
||||||
|
void bringToForeground();
|
||||||
|
void sendToBackground();
|
||||||
|
|
||||||
|
// Info about applet's state
|
||||||
|
|
||||||
|
bool isActive();
|
||||||
|
bool isForeground();
|
||||||
|
|
||||||
|
// Allow derived applets to handle changes in state
|
||||||
|
|
||||||
|
virtual void onRender() = 0; // All drawing happens here
|
||||||
|
virtual void onActivate() {}
|
||||||
|
virtual void onDeactivate() {}
|
||||||
|
virtual void onForeground() {}
|
||||||
|
virtual void onBackground() {}
|
||||||
|
virtual void onShutdown() {}
|
||||||
|
virtual void onButtonShortPress() {} // For use by System Applets only
|
||||||
|
virtual void onButtonLongPress() {} // For use by System Applets only
|
||||||
|
virtual void onLockAvailable() {} // For use by System Applets only
|
||||||
|
|
||||||
|
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||||
|
|
||||||
|
static void setDefaultFonts(AppletFont large, AppletFont small); // Set the general purpose fonts
|
||||||
|
static uint16_t getHeaderHeight(); // How tall is the "standard" applet header
|
||||||
|
|
||||||
|
const char *name = nullptr; // Shown in applet selection menu
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Place a single pixel. All drawing methods output through here
|
||||||
|
void drawPixel(int16_t x, int16_t y, uint16_t color) override;
|
||||||
|
|
||||||
|
// Tell WindowManager to update display
|
||||||
|
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED);
|
||||||
|
|
||||||
|
// Ask for applet to be moved to foreground
|
||||||
|
void requestAutoshow();
|
||||||
|
|
||||||
|
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||||
|
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||||
|
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
|
||||||
|
void resetCrop(); // Removes setCrop()
|
||||||
|
|
||||||
|
void setFont(AppletFont f);
|
||||||
|
AppletFont getFont();
|
||||||
|
|
||||||
|
uint16_t getTextWidth(std::string text);
|
||||||
|
uint16_t getTextWidth(const char *text);
|
||||||
|
|
||||||
|
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||||
|
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||||
|
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY);
|
||||||
|
|
||||||
|
// Print text, with per-word line wrapping
|
||||||
|
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text);
|
||||||
|
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text);
|
||||||
|
|
||||||
|
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
|
||||||
|
void drawHeader(std::string text); // Draw the standard applet header
|
||||||
|
|
||||||
|
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
|
||||||
|
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||||
|
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||||
|
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height); // Draw the meshtastic logo
|
||||||
|
|
||||||
|
std::string hexifyNodeNum(NodeNum num);
|
||||||
|
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
|
||||||
|
std::string getTimeString(uint32_t epochSeconds); // Human readable
|
||||||
|
std::string getTimeString(); // Current time, human readable
|
||||||
|
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
|
||||||
|
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||||
|
|
||||||
|
static AppletFont fontSmall, fontLarge; // General purpose fonts, used cross-applet
|
||||||
|
|
||||||
|
private:
|
||||||
|
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
|
||||||
|
bool active = false; // Has the user enabled this applet (at run-time)?
|
||||||
|
bool foreground = false; // Is the applet currently drawn on a tile?
|
||||||
|
|
||||||
|
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
|
||||||
|
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
||||||
|
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||||
|
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
|
||||||
|
|
||||||
|
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
||||||
|
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||||
|
|
||||||
|
AppletFont currentFont; // As passed to setFont
|
||||||
|
|
||||||
|
// As set by setCrop
|
||||||
|
int16_t cropLeft;
|
||||||
|
int16_t cropTop;
|
||||||
|
uint16_t cropWidth;
|
||||||
|
uint16_t cropHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
}; // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
208
src/graphics/niche/InkHUD/AppletFont.cpp
Normal file
208
src/graphics/niche/InkHUD/AppletFont.cpp
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./AppletFont.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
InkHUD::AppletFont::AppletFont()
|
||||||
|
{
|
||||||
|
// Default constructor uses the in-built AdafruitGFX font
|
||||||
|
}
|
||||||
|
|
||||||
|
InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafruitGFXFont)
|
||||||
|
{
|
||||||
|
// AdafruitGFX fonts are drawn relative to a "cursor line";
|
||||||
|
// they print as if the glyphs resting on the line of piece of ruled paper.
|
||||||
|
// The glyphs also each have a different height.
|
||||||
|
|
||||||
|
// To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text
|
||||||
|
// We also need to know where that "cursor line" sits inside this "line height";
|
||||||
|
// we need this additional info in order to align text by top-left, bottom-right, etc
|
||||||
|
|
||||||
|
// AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding,
|
||||||
|
// which we'd rather not deal with. If we want padding, we'll add it manually.
|
||||||
|
|
||||||
|
// Scan each glyph in the AdafruitGFX font
|
||||||
|
for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) {
|
||||||
|
uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph
|
||||||
|
this->height = max(this->height, glyphHeight); // Store if it's a new max
|
||||||
|
|
||||||
|
// Calculate how far the glyph rises the cursor line
|
||||||
|
// Store if new max value
|
||||||
|
// Caution: signed and unsigned types
|
||||||
|
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
|
||||||
|
if (glyphAscender > 0)
|
||||||
|
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine how far characters may hang "below the line"
|
||||||
|
descenderHeight = height - ascenderHeight;
|
||||||
|
|
||||||
|
// Find how far the cursor advances when we "print" a space character
|
||||||
|
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t InkHUD::AppletFont::lineHeight()
|
||||||
|
{
|
||||||
|
return this->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper).
|
||||||
|
// This value is the height of the font, above that imaginary line.
|
||||||
|
// Used to calculate the true height of the font
|
||||||
|
uint8_t InkHUD::AppletFont::heightAboveCursor()
|
||||||
|
{
|
||||||
|
return this->ascenderHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper).
|
||||||
|
// This value is the height of the font, below that imaginary line.
|
||||||
|
// Used to calculate the true height of the font
|
||||||
|
uint8_t InkHUD::AppletFont::heightBelowCursor()
|
||||||
|
{
|
||||||
|
return this->descenderHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width of the space character
|
||||||
|
// Used with Applet::printWrapped
|
||||||
|
uint8_t InkHUD::AppletFont::widthBetweenWords()
|
||||||
|
{
|
||||||
|
return this->spaceCharWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the list of substituted glyphs
|
||||||
|
// This "find and replace" operation will be run before text is printed
|
||||||
|
// Used to swap out UTF8 special characters, either with a custom font, or with a suitable ASCII approximation
|
||||||
|
void InkHUD::AppletFont::addSubstitution(const char *from, const char *to)
|
||||||
|
{
|
||||||
|
substitutions.push_back({.from = from, .to = to});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all registered subtitutions on a string
|
||||||
|
// Used to swap out UTF8 special chars
|
||||||
|
void InkHUD::AppletFont::applySubstitutions(std::string *text)
|
||||||
|
{
|
||||||
|
// For each substitution
|
||||||
|
for (Substitution s : substitutions) {
|
||||||
|
|
||||||
|
// Find and replace
|
||||||
|
// - search for Substitution::from
|
||||||
|
// - replace with Subsitution::to
|
||||||
|
size_t i = text->find(s.from);
|
||||||
|
while (i != std::string::npos) {
|
||||||
|
text->replace(i, strlen(s.from), s.to);
|
||||||
|
i = text->find(s.from, i); // Continue looking from last position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a set of substitutions which remap UTF8 for a Windows-1251 font
|
||||||
|
// Windows-1251 is an 8-bit character encoding, designed to cover languages that use the Cyrillic script
|
||||||
|
void InkHUD::AppletFont::addSubstitutionsWin1251()
|
||||||
|
{
|
||||||
|
addSubstitution("Ђ", "\x80");
|
||||||
|
addSubstitution("Ѓ", "\x81");
|
||||||
|
addSubstitution("ѓ", "\x83");
|
||||||
|
addSubstitution("€", "\x88");
|
||||||
|
addSubstitution("Љ", "\x8A");
|
||||||
|
addSubstitution("Њ", "\x8C");
|
||||||
|
addSubstitution("Ќ", "\x8D");
|
||||||
|
addSubstitution("Ћ", "\x8E");
|
||||||
|
addSubstitution("Џ", "\x8F");
|
||||||
|
|
||||||
|
addSubstitution("ђ", "\x90");
|
||||||
|
addSubstitution("љ", "\x9A");
|
||||||
|
addSubstitution("њ", "\x9C");
|
||||||
|
addSubstitution("ќ", "\x9D");
|
||||||
|
addSubstitution("ћ", "\x9E");
|
||||||
|
addSubstitution("џ", "\x9F");
|
||||||
|
|
||||||
|
addSubstitution("Ў", "\xA1");
|
||||||
|
addSubstitution("ў", "\xA2");
|
||||||
|
addSubstitution("Ј", "\xA3");
|
||||||
|
addSubstitution("Ґ", "\xA5");
|
||||||
|
addSubstitution("Ё", "\xA8");
|
||||||
|
addSubstitution("Є", "\xAA");
|
||||||
|
addSubstitution("Ї", "\xAF");
|
||||||
|
|
||||||
|
addSubstitution("І", "\xB2");
|
||||||
|
addSubstitution("і", "\xB3");
|
||||||
|
addSubstitution("ґ", "\xB4");
|
||||||
|
addSubstitution("ё", "\xB8");
|
||||||
|
addSubstitution("№", "\xB9");
|
||||||
|
addSubstitution("є", "\xBA");
|
||||||
|
addSubstitution("ј", "\xBC");
|
||||||
|
addSubstitution("Ѕ", "\xBD");
|
||||||
|
addSubstitution("ѕ", "\xBE");
|
||||||
|
addSubstitution("ї", "\xBF");
|
||||||
|
|
||||||
|
addSubstitution("А", "\xC0");
|
||||||
|
addSubstitution("Б", "\xC1");
|
||||||
|
addSubstitution("В", "\xC2");
|
||||||
|
addSubstitution("Г", "\xC3");
|
||||||
|
addSubstitution("Д", "\xC4");
|
||||||
|
addSubstitution("Е", "\xC5");
|
||||||
|
addSubstitution("Ж", "\xC6");
|
||||||
|
addSubstitution("З", "\xC7");
|
||||||
|
addSubstitution("И", "\xC8");
|
||||||
|
addSubstitution("Й", "\xC9");
|
||||||
|
addSubstitution("К", "\xCA");
|
||||||
|
addSubstitution("Л", "\xCB");
|
||||||
|
addSubstitution("М", "\xCC");
|
||||||
|
addSubstitution("Н", "\xCD");
|
||||||
|
addSubstitution("О", "\xCE");
|
||||||
|
addSubstitution("П", "\xCF");
|
||||||
|
|
||||||
|
addSubstitution("Р", "\xD0");
|
||||||
|
addSubstitution("С", "\xD1");
|
||||||
|
addSubstitution("Т", "\xD2");
|
||||||
|
addSubstitution("У", "\xD3");
|
||||||
|
addSubstitution("Ф", "\xD4");
|
||||||
|
addSubstitution("Х", "\xD5");
|
||||||
|
addSubstitution("Ц", "\xD6");
|
||||||
|
addSubstitution("Ч", "\xD7");
|
||||||
|
addSubstitution("Ш", "\xD8");
|
||||||
|
addSubstitution("Щ", "\xD9");
|
||||||
|
addSubstitution("Ъ", "\xDA");
|
||||||
|
addSubstitution("Ы", "\xDB");
|
||||||
|
addSubstitution("Ь", "\xDC");
|
||||||
|
addSubstitution("Э", "\xDD");
|
||||||
|
addSubstitution("Ю", "\xDE");
|
||||||
|
addSubstitution("Я", "\xDF");
|
||||||
|
|
||||||
|
addSubstitution("а", "\xE0");
|
||||||
|
addSubstitution("б", "\xE1");
|
||||||
|
addSubstitution("в", "\xE2");
|
||||||
|
addSubstitution("г", "\xE3");
|
||||||
|
addSubstitution("д", "\xE4");
|
||||||
|
addSubstitution("е", "\xE5");
|
||||||
|
addSubstitution("ж", "\xE6");
|
||||||
|
addSubstitution("з", "\xE7");
|
||||||
|
addSubstitution("и", "\xE8");
|
||||||
|
addSubstitution("й", "\xE9");
|
||||||
|
addSubstitution("к", "\xEA");
|
||||||
|
addSubstitution("л", "\xEB");
|
||||||
|
addSubstitution("м", "\xEC");
|
||||||
|
addSubstitution("н", "\xED");
|
||||||
|
addSubstitution("о", "\xEE");
|
||||||
|
addSubstitution("п", "\xEF");
|
||||||
|
|
||||||
|
addSubstitution("р", "\xF0");
|
||||||
|
addSubstitution("с", "\xF1");
|
||||||
|
addSubstitution("т", "\xF2");
|
||||||
|
addSubstitution("у", "\xF3");
|
||||||
|
addSubstitution("ф", "\xF4");
|
||||||
|
addSubstitution("х", "\xF5");
|
||||||
|
addSubstitution("ц", "\xF6");
|
||||||
|
addSubstitution("ч", "\xF7");
|
||||||
|
addSubstitution("ш", "\xF8");
|
||||||
|
addSubstitution("щ", "\xF9");
|
||||||
|
addSubstitution("ъ", "\xFA");
|
||||||
|
addSubstitution("ы", "\xFB");
|
||||||
|
addSubstitution("ь", "\xFC");
|
||||||
|
addSubstitution("э", "\xFD");
|
||||||
|
addSubstitution("ю", "\xFE");
|
||||||
|
addSubstitution("я", "\xFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
59
src/graphics/niche/InkHUD/AppletFont.h
Normal file
59
src/graphics/niche/InkHUD/AppletFont.h
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Wrapper class for an AdafruitGFX font
|
||||||
|
Pre-calculates some font dimension info which InkHUD uses repeatedly
|
||||||
|
|
||||||
|
Also contains an optional set of "substitutions".
|
||||||
|
These can be used to detect special UTF8 chars, and replace occurrences with a remapped char val to suit a custom font
|
||||||
|
These can also be used to swap UTF8 chars for a suitable ASCII substitution (e.g. German ö -> oe, etc)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include <GFX.h>
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
// An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD
|
||||||
|
class AppletFont
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AppletFont();
|
||||||
|
AppletFont(const GFXfont &adafruitGFXFont);
|
||||||
|
uint8_t lineHeight();
|
||||||
|
uint8_t heightAboveCursor();
|
||||||
|
uint8_t heightBelowCursor();
|
||||||
|
uint8_t widthBetweenWords();
|
||||||
|
|
||||||
|
void applySubstitutions(std::string *text); // Run all char-substitution operations, prior to printing
|
||||||
|
void addSubstitution(const char *from, const char *to); // Register a find-replace action, for remapping UTF8 chars
|
||||||
|
void addSubstitutionsWin1251(); // Cyrillic fonts: remap UTF8 values to their Win-1251 equivalent
|
||||||
|
// Todo: Polish font
|
||||||
|
|
||||||
|
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t height = 8; // Default value: in-built AdafruitGFX font
|
||||||
|
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
|
||||||
|
uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font
|
||||||
|
uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font
|
||||||
|
|
||||||
|
// One pair of find-replace values, for substituting or remapping UTF8 chars
|
||||||
|
struct Substitution {
|
||||||
|
const char *from;
|
||||||
|
const char *to;
|
||||||
|
};
|
||||||
|
|
||||||
|
// List of all character substitutions to run, prior to printing a string
|
||||||
|
std::vector<Substitution> substitutions;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
429
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
Normal file
429
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./MapApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
void InkHUD::MapApplet::onRender()
|
||||||
|
{
|
||||||
|
setFont(fontSmall);
|
||||||
|
|
||||||
|
// Abort if no markers to render
|
||||||
|
if (!enoughMarkers()) {
|
||||||
|
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE);
|
||||||
|
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find center of map
|
||||||
|
// - latitude and longitude
|
||||||
|
// - will be placed at X(0.5), Y(0.5)
|
||||||
|
getMapCenter(&latCenter, &lngCenter);
|
||||||
|
|
||||||
|
// Calculate North+East distance of each node to map center
|
||||||
|
// - which nodes to use controlled by virtual shouldDrawNode method
|
||||||
|
calculateAllMarkers();
|
||||||
|
|
||||||
|
// Set the region shown on the map
|
||||||
|
// - default: fit all nodes, plus padding
|
||||||
|
// - maybe overriden by derived applet
|
||||||
|
getMapSize(&widthMeters, &heightMeters);
|
||||||
|
|
||||||
|
// Set the metersToPx conversion value
|
||||||
|
calculateMapScale();
|
||||||
|
|
||||||
|
// Special marker for own node
|
||||||
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
if (ourNode && nodeDB->hasValidPosition(ourNode))
|
||||||
|
drawLabeledMarker(ourNode);
|
||||||
|
|
||||||
|
// Draw all markers
|
||||||
|
for (Marker m : markers) {
|
||||||
|
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
|
||||||
|
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
|
||||||
|
|
||||||
|
// Cross Size
|
||||||
|
constexpr uint16_t csMin = 5;
|
||||||
|
constexpr uint16_t csMax = 12;
|
||||||
|
|
||||||
|
// Too many hops away
|
||||||
|
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops
|
||||||
|
printAt(x, y, "!", CENTER, MIDDLE);
|
||||||
|
else if (!m.hasHopsAway) // Unknown hops
|
||||||
|
drawCross(x, y, csMin);
|
||||||
|
else // The fewer hops, the larger the cross
|
||||||
|
drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the center point, in the middle of all node positions
|
||||||
|
// Calculated values are written to the *lat and *long pointer args
|
||||||
|
// - Finds the "mean lat long"
|
||||||
|
// - Calculates furthest nodes from "mean lat long"
|
||||||
|
// - Place map center directly between these furthest nodes
|
||||||
|
|
||||||
|
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||||
|
{
|
||||||
|
// Find mean lat long coords
|
||||||
|
// ============================
|
||||||
|
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
|
||||||
|
// - averages the x, y and z coords
|
||||||
|
// - uses tan to find angles for lat / long degrees
|
||||||
|
// - longitude: triangle formed by x and y (on plane of the equator)
|
||||||
|
// - latitude: triangle formed by z (north south),
|
||||||
|
// and the line along plane of equator which stetches from earth's axis to where point xyz intersects planet's surface
|
||||||
|
|
||||||
|
// Working totals, averaged after nodeDB processed
|
||||||
|
uint32_t positionCount = 0;
|
||||||
|
float xAvg = 0;
|
||||||
|
float yAvg = 0;
|
||||||
|
float zAvg = 0;
|
||||||
|
|
||||||
|
// For each node in db
|
||||||
|
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Skip if no position
|
||||||
|
if (!nodeDB->hasValidPosition(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip if derived applet doesn't want to show this node on the map
|
||||||
|
if (!shouldDrawNode(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Latitude and Longitude of node, in radians
|
||||||
|
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||||
|
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||||
|
|
||||||
|
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||||
|
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||||
|
float x = cos(latRad) * cos(lngRad);
|
||||||
|
float y = cos(latRad) * sin(lngRad);
|
||||||
|
float z = sin(latRad);
|
||||||
|
|
||||||
|
// To find mean values shortly
|
||||||
|
xAvg += x;
|
||||||
|
yAvg += y;
|
||||||
|
zAvg += z;
|
||||||
|
positionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All NodeDB processed, find mean values
|
||||||
|
xAvg /= positionCount;
|
||||||
|
yAvg /= positionCount;
|
||||||
|
zAvg /= positionCount;
|
||||||
|
|
||||||
|
// Longitude from cartesian coords
|
||||||
|
// (Angle from 3D coords describing a point of globe's surface)
|
||||||
|
/*
|
||||||
|
UK
|
||||||
|
/-------\
|
||||||
|
(Top View) /- -\
|
||||||
|
/- (You) -\
|
||||||
|
/- . -\
|
||||||
|
/- . X -\
|
||||||
|
Asia - ... - USA
|
||||||
|
\- Y -/
|
||||||
|
\- -/
|
||||||
|
\- -/
|
||||||
|
\- -/
|
||||||
|
\- -----/
|
||||||
|
Pacific
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
|
||||||
|
|
||||||
|
// Latitude from cartesian cooods
|
||||||
|
// (Angle from 3D coords describing a point on the globe's surface)
|
||||||
|
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
|
||||||
|
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
|
||||||
|
/*
|
||||||
|
UK North
|
||||||
|
/-------\ (Front View) /-------\
|
||||||
|
(Top View) /- -\ /- -\
|
||||||
|
/- (You) -\ /-(You) -\
|
||||||
|
/- /. -\ /- . -\
|
||||||
|
/- √X²+Y²/ . X -\ /- Z . -\
|
||||||
|
Asia - /... - USA - ..... -
|
||||||
|
\- Y -/ \- √X²+Y² -/
|
||||||
|
\- -/ \- -/
|
||||||
|
\- -/ \- -/
|
||||||
|
\- -/ \- -/
|
||||||
|
\- -----/ \- -----/
|
||||||
|
Pacific South
|
||||||
|
*/
|
||||||
|
|
||||||
|
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
|
||||||
|
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// This has given us the "mean position"
|
||||||
|
// This will be a position *somewhere* near the center of our nodes.
|
||||||
|
// What we actually want is to place our center so that our outermost nodes end up on the border of our map.
|
||||||
|
// The only real use of our "mean position" is to give us a reference frame:
|
||||||
|
// which direction is east, and which is west.
|
||||||
|
//------------------------------------------------
|
||||||
|
|
||||||
|
// Find furthest nodes from "mean lat long"
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
float northernmost = latCenter;
|
||||||
|
float southernmost = latCenter;
|
||||||
|
float easternmost = lngCenter;
|
||||||
|
float westernmost = lngCenter;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Skip if no position
|
||||||
|
if (!nodeDB->hasValidPosition(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip if derived applet doesn't want to show this node on the map
|
||||||
|
if (!shouldDrawNode(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check for a new top or bottom latitude
|
||||||
|
float lat = node->position.latitude_i * 1e-7;
|
||||||
|
northernmost = max(northernmost, lat);
|
||||||
|
southernmost = min(southernmost, lat);
|
||||||
|
|
||||||
|
// Longitude is trickier
|
||||||
|
float lng = node->position.longitude_i * 1e-7;
|
||||||
|
float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees travelled east from lngCenter to reach node
|
||||||
|
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees travelled west from lngCenter to reach node
|
||||||
|
if (degEastward < degWestward)
|
||||||
|
easternmost = max(easternmost, lngCenter + degEastward);
|
||||||
|
else
|
||||||
|
westernmost = min(westernmost, lngCenter - degWestward);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: check for issues with map spans >180 deg. MQTT only..
|
||||||
|
latCenter = (northernmost + southernmost) / 2;
|
||||||
|
lngCenter = (westernmost + easternmost) / 2;
|
||||||
|
|
||||||
|
// In case our new center is west of -180, or east of +180, for some reason
|
||||||
|
lngCenter = fmod(lngCenter, 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size of map in meters
|
||||||
|
// Grown to fit the nodes furthest from map center
|
||||||
|
// Overridable if derived applet wants a custom map size (fixed size?)
|
||||||
|
void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters)
|
||||||
|
{
|
||||||
|
// Reset the value
|
||||||
|
*widthMeters = 0;
|
||||||
|
*heightMeters = 0;
|
||||||
|
|
||||||
|
// Find the greatest distance horizontally and vertically from map center
|
||||||
|
for (Marker m : markers) {
|
||||||
|
*widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2);
|
||||||
|
*heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
*widthMeters *= 1.1;
|
||||||
|
*heightMeters *= 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert and store info we need for drawing a marker
|
||||||
|
// Lat / long to "meters relative to map center", for position on screen
|
||||||
|
// Info about hopsAway, for marker size
|
||||||
|
InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway)
|
||||||
|
{
|
||||||
|
assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling.
|
||||||
|
|
||||||
|
// Bearing and distance from map center to node
|
||||||
|
float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng);
|
||||||
|
float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians
|
||||||
|
|
||||||
|
// Split into meters north and meters east components (signed)
|
||||||
|
// - signedness of cos / sin automatically sets negative if south or west
|
||||||
|
float northMeters = cos(bearingFromCenter) * distanceFromCenter;
|
||||||
|
float eastMeters = sin(bearingFromCenter) * distanceFromCenter;
|
||||||
|
|
||||||
|
// Store this as a new marker
|
||||||
|
Marker m;
|
||||||
|
m.eastMeters = eastMeters;
|
||||||
|
m.northMeters = northMeters;
|
||||||
|
m.hasHopsAway = hasHopsAway;
|
||||||
|
m.hopsAway = hopsAway;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a marker on the map for a node, with a shortname label, and backing box
|
||||||
|
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||||
|
{
|
||||||
|
// Find x and y position based on node's position in nodeDB
|
||||||
|
assert(nodeDB->hasValidPosition(node));
|
||||||
|
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||||
|
node->position.longitude_i * 1e-7, // Long, convered from Meshtastic's internal int32 style
|
||||||
|
node->has_hops_away, // Is the hopsAway number valid
|
||||||
|
node->hops_away // Hops away
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to pixel coords
|
||||||
|
int16_t markerX = X(0.5) + (m.eastMeters * metersToPx);
|
||||||
|
int16_t markerY = Y(0.5) - (m.northMeters * metersToPx);
|
||||||
|
|
||||||
|
constexpr uint16_t paddingH = 2;
|
||||||
|
constexpr uint16_t paddingW = 4;
|
||||||
|
uint16_t paddingInnerW = 2; // Zero'd out if no text
|
||||||
|
constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross)
|
||||||
|
constexpr uint16_t markerSizeMin = 5;
|
||||||
|
|
||||||
|
int16_t textX;
|
||||||
|
int16_t textY;
|
||||||
|
uint16_t textW;
|
||||||
|
uint16_t textH;
|
||||||
|
int16_t labelX;
|
||||||
|
int16_t labelY;
|
||||||
|
uint16_t labelW;
|
||||||
|
uint16_t labelH;
|
||||||
|
uint8_t markerSize;
|
||||||
|
|
||||||
|
bool tooManyHops = node->hops_away > config.lora.hop_limit;
|
||||||
|
bool isOurNode = node->num == nodeDB->getNodeNum();
|
||||||
|
bool unknownHops = !node->has_hops_away && !isOurNode;
|
||||||
|
|
||||||
|
// We will draw a left or right hand variant, to place text towards screen center
|
||||||
|
// Hopfully avoid text spilling off screen
|
||||||
|
// Most values are the same, regardless of left-right handedness
|
||||||
|
|
||||||
|
// Pick emblem style
|
||||||
|
if (tooManyHops)
|
||||||
|
markerSize = getTextWidth("!");
|
||||||
|
else if (unknownHops)
|
||||||
|
markerSize = markerSizeMin;
|
||||||
|
else
|
||||||
|
markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin);
|
||||||
|
|
||||||
|
// Common dimensions (left or right variant)
|
||||||
|
textW = getTextWidth(node->user.short_name);
|
||||||
|
if (textW == 0)
|
||||||
|
paddingInnerW = 0; // If no text, no padding for text
|
||||||
|
textH = fontSmall.lineHeight();
|
||||||
|
labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH;
|
||||||
|
labelY = markerY - (labelH / 2);
|
||||||
|
textY = markerY;
|
||||||
|
labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant
|
||||||
|
|
||||||
|
// Left-side variant
|
||||||
|
if (markerX < width() / 2) {
|
||||||
|
labelX = markerX - (markerSize / 2) - paddingW;
|
||||||
|
textX = labelX + paddingW + markerSize + paddingInnerW;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-side variant
|
||||||
|
else {
|
||||||
|
labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW;
|
||||||
|
textX = labelX + paddingW;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backing box
|
||||||
|
fillRect(labelX, labelY, labelW, labelH, WHITE);
|
||||||
|
drawRect(labelX, labelY, labelW, labelH, BLACK);
|
||||||
|
|
||||||
|
// Short name
|
||||||
|
printAt(textX, textY, node->user.short_name, LEFT, MIDDLE);
|
||||||
|
|
||||||
|
// If the label is for our own node,
|
||||||
|
// fade it by overdrawing partially with white
|
||||||
|
if (node == nodeDB->getMeshNode(nodeDB->getNodeNum()))
|
||||||
|
hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE);
|
||||||
|
|
||||||
|
// Draw the marker emblem
|
||||||
|
// - after the fading, because hatching (own node) can align with cross and make it look weird
|
||||||
|
if (tooManyHops)
|
||||||
|
printAt(markerX, markerY, "!", CENTER, MIDDLE);
|
||||||
|
else
|
||||||
|
drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we actually have enough nodes which would be shown on the map
|
||||||
|
// Need at least two, to draw a sensible map
|
||||||
|
bool InkHUD::MapApplet::enoughMarkers()
|
||||||
|
{
|
||||||
|
uint8_t count = 0;
|
||||||
|
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Count nodes
|
||||||
|
if (nodeDB->hasValidPosition(node) && shouldDrawNode(node))
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// We need to find two
|
||||||
|
if (count == 2)
|
||||||
|
return true; // Two nodes is enough for a sensible map
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // No nodes would be drawn (or just the one, uselessly at 0,0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how far north and east of map center each node is
|
||||||
|
// Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode
|
||||||
|
void InkHUD::MapApplet::calculateAllMarkers()
|
||||||
|
{
|
||||||
|
// Clear old markers
|
||||||
|
markers.clear();
|
||||||
|
|
||||||
|
// For each node in db
|
||||||
|
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Skip if no position
|
||||||
|
if (!nodeDB->hasValidPosition(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip if derived applet doesn't want to show this node on the map
|
||||||
|
if (!shouldDrawNode(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip if our own node
|
||||||
|
// - special handling in render()
|
||||||
|
if (node->num == nodeDB->getNodeNum())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Calculate marker and store it
|
||||||
|
markers.push_back(
|
||||||
|
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||||
|
node->position.longitude_i * 1e-7, // Long, convered from Meshtastic's internal int32 style
|
||||||
|
node->has_hops_away, // Is the hopsAway number valid
|
||||||
|
node->hops_away // Hops away
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the conversion factor between metres, and pixels on screen
|
||||||
|
// May be overriden by derived applet, if custom scale required (fixed map size?)
|
||||||
|
void InkHUD::MapApplet::calculateMapScale()
|
||||||
|
{
|
||||||
|
// Aspect ratio of map and screen
|
||||||
|
// - larger = wide, smaller = tall
|
||||||
|
// - used to set scale, so that widest map dimension fits in applet
|
||||||
|
float mapAspectRatio = (float)widthMeters / heightMeters;
|
||||||
|
float appletAspectRatio = (float)width() / height();
|
||||||
|
|
||||||
|
// "Shrink to fit"
|
||||||
|
// Scale the map so that the largest dimension is fully displayed
|
||||||
|
// Because aspect ratio will be maintained, the other dimension will appear "padded"
|
||||||
|
if (mapAspectRatio > appletAspectRatio)
|
||||||
|
metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width.
|
||||||
|
else
|
||||||
|
metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw an x, centered on a specific point
|
||||||
|
// Most markers will draw with this method
|
||||||
|
void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size)
|
||||||
|
{
|
||||||
|
int16_t x0 = x - (size / 2);
|
||||||
|
int16_t y0 = y - (size / 2);
|
||||||
|
int16_t x1 = x0 + size - 1;
|
||||||
|
int16_t y1 = y0 + size - 1;
|
||||||
|
drawLine(x0, y0, x1, y1, BLACK);
|
||||||
|
drawLine(x0, y1, x1, y0, BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
66
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
Normal file
66
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Base class for Applets which show nodes on a map
|
||||||
|
|
||||||
|
Plots position of for a selection of nodes, with north facing up.
|
||||||
|
Size of cross represents hops away.
|
||||||
|
Our own node is identified with a faded label.
|
||||||
|
|
||||||
|
The base applet doesn't handle any events; this is left to the derived applets.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
|
||||||
|
#include "MeshModule.h"
|
||||||
|
#include "gps/GeoCoord.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class MapApplet : public Applet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void onRender() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
||||||
|
virtual void getMapCenter(float *lat, float *lng);
|
||||||
|
virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters);
|
||||||
|
|
||||||
|
bool enoughMarkers(); // Anything to draw?
|
||||||
|
void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Position of markers to be drawn, relative to map center
|
||||||
|
// HopsAway info used to determine marker size
|
||||||
|
struct Marker {
|
||||||
|
float eastMeters = 0; // Meters east of mapCenter. Negative if west.
|
||||||
|
float northMeters = 0; // Meters north of mapCenter. Negative if south.
|
||||||
|
bool hasHopsAway = false;
|
||||||
|
uint8_t hopsAway = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);
|
||||||
|
void calculateAllMarkers();
|
||||||
|
void calculateMapScale(); // Conversion factor for meters to pixels
|
||||||
|
void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers
|
||||||
|
|
||||||
|
float metersToPx = 0; // Conversion factor for meters to pixels
|
||||||
|
float latCenter = 0; // Map center: latitude
|
||||||
|
float lngCenter = 0; // Map center: longitude
|
||||||
|
|
||||||
|
std::list<Marker> markers;
|
||||||
|
uint32_t widthMeters = 0; // Map width: meters
|
||||||
|
uint32_t heightMeters = 0; // Map height: meters
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,283 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "RTC.h"
|
||||||
|
|
||||||
|
#include "GeoCoord.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
|
||||||
|
#include "./NodeListApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name)
|
||||||
|
{
|
||||||
|
// We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule
|
||||||
|
// For all other packets, we manually reimplement isPromiscuous=false in wantPacket
|
||||||
|
MeshModule::isPromiscuous = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we want to process this packet with handleReceived()?
|
||||||
|
bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p)
|
||||||
|
{
|
||||||
|
// Only interested if:
|
||||||
|
return isActive() // Applet is active
|
||||||
|
&& !isFromUs(p) // Packet is incoming (not outgoing)
|
||||||
|
&& (isToUs(p) || isBroadcast(p->to) || // Either: intended for us,
|
||||||
|
p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo
|
||||||
|
|
||||||
|
// Note: special handling of NodeInfo is to match NodeInfoModule
|
||||||
|
// To match the behavior seen in the client apps:
|
||||||
|
// - NodeInfoModule's ProtoBufModule base is "promiscuous"
|
||||||
|
// - All other activity is *not* promiscuous
|
||||||
|
// To achieve this, our MeshModule *is* promiscious, and we're manually reimplementing non-promiscuous behavior here,
|
||||||
|
// to match the code in MeshModule::callModules
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeshModule packets arrive here
|
||||||
|
// Extract the info and pass it to the derived applet
|
||||||
|
// Derived applet will store the CardInfo and perform any required sorting of the CardInfo collection
|
||||||
|
// Derived applet might also need to keep other tallies (active nodes count?)
|
||||||
|
ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
|
{
|
||||||
|
// Abort if applet fully deactivated
|
||||||
|
// Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return
|
||||||
|
if (!isActive())
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
|
// Assemble info: from this event
|
||||||
|
CardInfo c;
|
||||||
|
c.nodeNum = mp.from;
|
||||||
|
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
||||||
|
|
||||||
|
// Assemble info: from nodeDB (needed to detect changes)
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||||
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
if (node) {
|
||||||
|
if (node->has_hops_away)
|
||||||
|
c.hopsAway = node->hops_away;
|
||||||
|
|
||||||
|
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||||
|
// Get lat and long as float
|
||||||
|
// Meshtastic stores these as integers internally
|
||||||
|
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||||
|
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||||
|
float theirLat = node->position.latitude_i * 1e-7;
|
||||||
|
float theirLong = node->position.longitude_i * 1e-7;
|
||||||
|
|
||||||
|
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass to the derived applet
|
||||||
|
// Derived applet is responsible for requesting update, if justified
|
||||||
|
// That request will eventually trigger our class' onRender method
|
||||||
|
handleParsed(c);
|
||||||
|
|
||||||
|
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximum number of cards we may ever need to render, in our tallest layout config
|
||||||
|
// May be slightly in excess of the true value: header not accounted for
|
||||||
|
uint8_t InkHUD::NodeListApplet::maxCards()
|
||||||
|
{
|
||||||
|
// Cache result. Shouldn't change during execution
|
||||||
|
static uint8_t cards = 0;
|
||||||
|
|
||||||
|
if (!cards) {
|
||||||
|
const uint16_t height = Tile::maxDisplayDimension();
|
||||||
|
|
||||||
|
// Use a loop instead of arithmetic, because it's easier for my brain to follow
|
||||||
|
// Add cards one by one, until the latest card (without margin) extends below screen
|
||||||
|
|
||||||
|
uint16_t y = cardH; // First card: no margin above
|
||||||
|
cards = 1;
|
||||||
|
|
||||||
|
while (y < height) {
|
||||||
|
y += cardMarginH;
|
||||||
|
y += cardH;
|
||||||
|
cards++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw using info which derived applet placed into NodeListApplet::cards for us
|
||||||
|
void InkHUD::NodeListApplet::onRender()
|
||||||
|
{
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Draw the standard applet header
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
drawHeader(getHeaderText()); // Ask derived applet for the title
|
||||||
|
|
||||||
|
// Dimensions of the header
|
||||||
|
int16_t headerDivY = getHeaderHeight() - 1;
|
||||||
|
constexpr uint16_t padDivH = 2;
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// Draw the main node list
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
// const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||||
|
// const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH;
|
||||||
|
|
||||||
|
// Imaginary vertical line dividing left-side and right-side info
|
||||||
|
// Long-name will crop here
|
||||||
|
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
|
||||||
|
|
||||||
|
// Y value (top) of the current card. Increases as we draw.
|
||||||
|
uint16_t cardTopY = headerDivY + padDivH;
|
||||||
|
|
||||||
|
// -- Each node in list --
|
||||||
|
for (auto card = cards.begin(); card != cards.end(); ++card) {
|
||||||
|
|
||||||
|
// Gather info
|
||||||
|
// ========================================
|
||||||
|
NodeNum &nodeNum = card->nodeNum;
|
||||||
|
SignalStrength &signal = card->signal;
|
||||||
|
std::string longName; // handled below
|
||||||
|
std::string shortName; // handled below
|
||||||
|
std::string distance; // handled below;
|
||||||
|
uint8_t &hopsAway = card->hopsAway;
|
||||||
|
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||||
|
|
||||||
|
// -- Shortname --
|
||||||
|
// use "?" if unknown
|
||||||
|
if (node && node->has_user)
|
||||||
|
shortName = node->user.short_name;
|
||||||
|
else
|
||||||
|
shortName = "?";
|
||||||
|
|
||||||
|
// -- Longname --
|
||||||
|
// use node id if unknown
|
||||||
|
if (node && node->has_user)
|
||||||
|
longName = node->user.long_name; // Found in nodeDB
|
||||||
|
else {
|
||||||
|
// Not found in nodeDB, show a hex nodeid instead
|
||||||
|
longName = hexifyNodeNum(nodeNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Distance --
|
||||||
|
if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN)
|
||||||
|
distance = localizeDistance(card->distanceMeters);
|
||||||
|
|
||||||
|
// Draw the info
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
// Define two lines of text for the card
|
||||||
|
// We will center our text on these lines
|
||||||
|
uint16_t lineAY = cardTopY + (fontLarge.lineHeight() / 2);
|
||||||
|
uint16_t lineBY = cardTopY + fontLarge.lineHeight() + (fontSmall.lineHeight() / 2);
|
||||||
|
|
||||||
|
// Print the short name
|
||||||
|
setFont(fontLarge);
|
||||||
|
printAt(0, lineAY, shortName, LEFT, MIDDLE);
|
||||||
|
|
||||||
|
// Print the distance
|
||||||
|
setFont(fontSmall);
|
||||||
|
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
|
||||||
|
|
||||||
|
// If we have a direct connection to the node, draw the signal indicator
|
||||||
|
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
|
||||||
|
uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
|
||||||
|
uint16_t signalH = fontLarge.lineHeight() * 0.75;
|
||||||
|
int16_t signalY = lineAY + (fontLarge.lineHeight() / 2) - (fontLarge.lineHeight() * 0.75);
|
||||||
|
int16_t signalX = width() - signalW;
|
||||||
|
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||||
|
}
|
||||||
|
// Otherwise, print "hops away" info, if available
|
||||||
|
else if (hopsAway != CardInfo::HOPS_UNKNOWN) {
|
||||||
|
std::string hopString = to_string(node->hops_away);
|
||||||
|
hopString += " Hop";
|
||||||
|
if (node->hops_away != 1)
|
||||||
|
hopString += "s"; // Append s for "Hops", rather than "Hop"
|
||||||
|
|
||||||
|
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the long name, cropping to prevent overflow onto the right-side info
|
||||||
|
setCrop(0, 0, dividerX - 1, height());
|
||||||
|
printAt(0, lineBY, longName, LEFT, MIDDLE);
|
||||||
|
|
||||||
|
// GFX effect: "hatch" the right edge of longName area
|
||||||
|
// If a longName has been cropped, it will appear to fade out,
|
||||||
|
// creating a soft barrier with the right-side info
|
||||||
|
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
|
||||||
|
const int16_t hatchWidth = fontSmall.lineHeight();
|
||||||
|
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
|
||||||
|
|
||||||
|
// Prepare to draw the next card
|
||||||
|
resetCrop();
|
||||||
|
cardTopY += cardH;
|
||||||
|
|
||||||
|
// Once we've run out of screen, stop drawing cards
|
||||||
|
// Depending on tiles / rotation, this may be before we hit maxCards
|
||||||
|
if (cardTopY > height()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw element: a "mobile phone" style signal indicator
|
||||||
|
// We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc
|
||||||
|
// This prevents issues with premature rounding when rendering tiny elements
|
||||||
|
void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength)
|
||||||
|
{
|
||||||
|
|
||||||
|
/*
|
||||||
|
+-------------------------------------------+
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| barHeightRelative=1.0
|
||||||
|
| +--+ ^ |
|
||||||
|
| gutterW +--+ | | | |
|
||||||
|
| <--> +--+ | | | | | |
|
||||||
|
| +--+ | | | | | | | |
|
||||||
|
| | | | | | | | | | |
|
||||||
|
| <-> +--+ +--+ +--+ +--+ v |
|
||||||
|
| paddingW ^ |
|
||||||
|
| paddingH | |
|
||||||
|
| v |
|
||||||
|
+-------------------------------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
|
constexpr float paddingW = 0.1; // Either side
|
||||||
|
constexpr float paddingH = 0.1; // Above and below
|
||||||
|
constexpr float gutterX = 0.1; // Between bars
|
||||||
|
|
||||||
|
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the talleest
|
||||||
|
constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.
|
||||||
|
|
||||||
|
// Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
|
||||||
|
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterX) + paddingW)) / barCount;
|
||||||
|
float barHMax = 1.0 - (paddingH + paddingH);
|
||||||
|
|
||||||
|
// Draw signal bar rectangles, then placeholder lines once strength reached
|
||||||
|
for (uint8_t i = 0; i < barCount; i++) {
|
||||||
|
// Co-ords for this specific bar
|
||||||
|
float barH = barHMax * barHRel[i];
|
||||||
|
float barX = paddingW + (i * (gutterX + barW));
|
||||||
|
float barY = paddingH + (barHMax - barH);
|
||||||
|
|
||||||
|
// Rasterize to px coords at the last moment
|
||||||
|
int16_t rX = (x + (w * barX)) + 0.5;
|
||||||
|
int16_t rY = (y + (h * barY)) + 0.5;
|
||||||
|
uint16_t rW = (w * barW) + 0.5;
|
||||||
|
uint16_t rH = (h * barH) + 0.5;
|
||||||
|
|
||||||
|
// Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
|
||||||
|
if (i <= strength)
|
||||||
|
drawRect(rX, rY, rW, rH, BLACK);
|
||||||
|
else {
|
||||||
|
// Just draw a placeholder line
|
||||||
|
float lineY = barY + barH;
|
||||||
|
uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
|
||||||
|
drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,71 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Base class for Applets which display a list of nodes
|
||||||
|
Used by the "Recents" and "Heard" applets. Possibly more in future?
|
||||||
|
|
||||||
|
+-------------------------------+
|
||||||
|
| | |
|
||||||
|
| SHRT . | | |
|
||||||
|
| Long name 50km |
|
||||||
|
| |
|
||||||
|
| ABCD 2 Hops |
|
||||||
|
| abcdedfghijk 30km |
|
||||||
|
| |
|
||||||
|
+-------------------------------+
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class NodeListApplet : public Applet, public MeshModule
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
// Info used to draw one card to the node list
|
||||||
|
struct CardInfo {
|
||||||
|
static constexpr uint8_t HOPS_UNKNOWN = -1;
|
||||||
|
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
|
||||||
|
|
||||||
|
NodeNum nodeNum = 0;
|
||||||
|
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
|
||||||
|
uint32_t distanceMeters = DISTANCE_UNKNOWN;
|
||||||
|
uint8_t hopsAway = HOPS_UNKNOWN; // Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
NodeListApplet(const char *name);
|
||||||
|
void onRender() override;
|
||||||
|
|
||||||
|
// MeshModule overrides
|
||||||
|
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||||
|
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void handleParsed(CardInfo c) = 0; // Pass extracted info from a new packet to derived class, for sorting and storage
|
||||||
|
virtual std::string getHeaderText() = 0; // Title for the applet's header. Todo: get this info another way?
|
||||||
|
|
||||||
|
uint8_t maxCards(); // Calculate the maximum number of cards an applet could ever display
|
||||||
|
|
||||||
|
std::deque<CardInfo> cards; // Derived applet places cards here, for this base applet to render
|
||||||
|
|
||||||
|
private:
|
||||||
|
// UI element: a "mobile phone" style signal indicator
|
||||||
|
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength signal);
|
||||||
|
|
||||||
|
// Dimensions for drawing
|
||||||
|
// Used for render, and also for maxCards calc
|
||||||
|
const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||||
|
const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,14 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./BasicExampleApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
// All drawing happens here
|
||||||
|
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||||
|
void InkHUD::BasicExampleApplet::onRender()
|
||||||
|
{
|
||||||
|
print("Hello, World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,36 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
A bare-minimum example of an InkHUD applet.
|
||||||
|
Only prints Hello World.
|
||||||
|
|
||||||
|
In variants/<your device>/nicheGraphics.h:
|
||||||
|
|
||||||
|
- include this .h file
|
||||||
|
- add the following line of code:
|
||||||
|
windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class BasicExampleApplet : public Applet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// You must have an onRender() method
|
||||||
|
// All drawing happens here
|
||||||
|
|
||||||
|
void onRender() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,54 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./NewMsgExampleApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
// We configured MeshModule API to call this method when we receive a new text message
|
||||||
|
ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Abort if applet fully deactivated
|
||||||
|
if (!isActive())
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
|
// Check that this is an incoming message
|
||||||
|
// Outgoing messages (sent by us) will also call handleReceived
|
||||||
|
|
||||||
|
if (!isFromUs(&mp)) {
|
||||||
|
// Store the sender's nodenum
|
||||||
|
// We need to keep this information, so we can re-use it anytime render() is called
|
||||||
|
haveMessage = true;
|
||||||
|
fromWho = mp.from;
|
||||||
|
|
||||||
|
// Tell InkHUD that we have something new to show on the screen
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell MeshModule API to continue informing other firmware components about this message
|
||||||
|
// We're not the only component which is interested in new text messages
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All drawing happens here
|
||||||
|
// We can trigger a render by calling requestUpdate()
|
||||||
|
// Render might be called by some external source
|
||||||
|
// We should always be ready to draw
|
||||||
|
void InkHUD::NewMsgExampleApplet::onRender()
|
||||||
|
{
|
||||||
|
setFont(fontSmall);
|
||||||
|
|
||||||
|
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||||
|
|
||||||
|
int16_t centerX = X(0.5); // Same as width() / 2
|
||||||
|
int16_t centerY = Y(0.5); // Same as height() / 2
|
||||||
|
|
||||||
|
if (haveMessage) {
|
||||||
|
printAt(centerX, centerY, "New Message", CENTER, BOTTOM);
|
||||||
|
printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP);
|
||||||
|
} else {
|
||||||
|
printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,61 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
An example of an InkHUD applet.
|
||||||
|
Tells us when a new text message arrives.
|
||||||
|
|
||||||
|
This applet makes use of the MeshModule API to detect new messages,
|
||||||
|
which is a general part of the Meshtastic firmware, and not part of InkHUD.
|
||||||
|
|
||||||
|
In variants/<your device>/nicheGraphics.h:
|
||||||
|
|
||||||
|
- include this .h file
|
||||||
|
- add the following line of code:
|
||||||
|
windowManager->addApplet("New Msg", new InkHUD::NewMsgExampleApplet);
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
|
||||||
|
#include "mesh/SinglePortModule.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class NewMsgExampleApplet : public Applet, public SinglePortModule
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages.
|
||||||
|
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||||
|
|
||||||
|
// All drawing happens here
|
||||||
|
void onRender() override;
|
||||||
|
|
||||||
|
// Your applet might also want to use some of these
|
||||||
|
// Useful for setting up or tidying up
|
||||||
|
|
||||||
|
/*
|
||||||
|
void onActivate(); // When started
|
||||||
|
void onDeactivate(); // When stopped
|
||||||
|
void onForeground(); // When shown by short-press
|
||||||
|
void onBackground(); // When hidden by short-press
|
||||||
|
*/
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Called when we receive new text messages
|
||||||
|
// Part of the MeshModule API
|
||||||
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
|
// Store info from handleReceived
|
||||||
|
bool haveMessage = false;
|
||||||
|
NodeNum fromWho;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,107 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./BatteryIconApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
void InkHUD::BatteryIconApplet::onActivate()
|
||||||
|
{
|
||||||
|
// Show at boot, if user has previously enabled the feature
|
||||||
|
if (settings.optionalFeatures.batteryIcon)
|
||||||
|
bringToForeground();
|
||||||
|
|
||||||
|
// Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available
|
||||||
|
// This happens whether or not the battery icon feature is enabled
|
||||||
|
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::BatteryIconApplet::onDeactivate()
|
||||||
|
{
|
||||||
|
// Stop having onPowerStatusUpdate called
|
||||||
|
powerStatusObserver.unobserve(&powerStatus->onNewStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We handle power status' even when the feature is disabled,
|
||||||
|
// so that we have up to date data ready if the feature is enabled later.
|
||||||
|
// Otherwise could be 30s before new status update, with weird battery value displayed
|
||||||
|
int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status)
|
||||||
|
{
|
||||||
|
// System applets are always active
|
||||||
|
assert(isActive());
|
||||||
|
|
||||||
|
// This method should only receive power statuses
|
||||||
|
// If we get a different type of status, something has gone weird elsewhere
|
||||||
|
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||||
|
|
||||||
|
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||||
|
|
||||||
|
// Get the new state of charge %, and round to the nearest 10%
|
||||||
|
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||||
|
|
||||||
|
// If rounded value has changed, trigger a display update
|
||||||
|
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||||
|
// Don't trigger an update if the feature is disabled
|
||||||
|
if (this->socRounded != newSocRounded && settings.optionalFeatures.batteryIcon)
|
||||||
|
requestUpdate();
|
||||||
|
|
||||||
|
// Store the new value
|
||||||
|
this->socRounded = newSocRounded;
|
||||||
|
|
||||||
|
return 0; // Tell Observable to continue informing other observers
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::BatteryIconApplet::onRender()
|
||||||
|
{
|
||||||
|
// Fill entire tile
|
||||||
|
// - size of icon controlled by size of tile
|
||||||
|
int16_t l = 0;
|
||||||
|
int16_t t = 0;
|
||||||
|
uint16_t w = width();
|
||||||
|
int16_t h = height();
|
||||||
|
|
||||||
|
// Clear the region beneath the tile
|
||||||
|
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||||
|
// We do need to do this with the battery though, as it is an "overlay"
|
||||||
|
fillRect(l, t, w, h, WHITE);
|
||||||
|
|
||||||
|
// Vertical centerline
|
||||||
|
const int16_t m = t + (h / 2);
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Draw battery outline
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
// Positive terminal "bump"
|
||||||
|
const int16_t &bumpL = l;
|
||||||
|
const uint16_t bumpH = h / 2;
|
||||||
|
const int16_t bumpT = m - (bumpH / 2);
|
||||||
|
constexpr uint16_t bumpW = 2;
|
||||||
|
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||||
|
|
||||||
|
// Main body of battery
|
||||||
|
const int16_t bodyL = bumpL + bumpW;
|
||||||
|
const int16_t &bodyT = t;
|
||||||
|
const int16_t &bodyH = h;
|
||||||
|
const int16_t bodyW = w - bumpW;
|
||||||
|
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||||
|
|
||||||
|
// Erase join between bump and body
|
||||||
|
drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE);
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// Draw battery level
|
||||||
|
// ===================
|
||||||
|
|
||||||
|
constexpr int16_t slicePad = 2;
|
||||||
|
const int16_t sliceL = bodyL + slicePad;
|
||||||
|
const int16_t sliceT = bodyT + slicePad;
|
||||||
|
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||||
|
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||||
|
|
||||||
|
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||||
|
|
||||||
|
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||||
|
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,41 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This applet floats top-left, giving a graphical representation of battery remaining
|
||||||
|
It should be optional, enabled by the on-screen menu
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
|
||||||
|
#include "PowerStatus.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class BatteryIconApplet : public Applet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void onRender() override;
|
||||||
|
|
||||||
|
void onActivate() override;
|
||||||
|
void onDeactivate() override;
|
||||||
|
|
||||||
|
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Get informed when new information about the battery is available (via onPowerStatusUpdate method)
|
||||||
|
CallbackObserver<BatteryIconApplet, const meshtastic::Status *> powerStatusObserver =
|
||||||
|
CallbackObserver<BatteryIconApplet, const meshtastic::Status *>(this, &BatteryIconApplet::onPowerStatusUpdate);
|
||||||
|
|
||||||
|
uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10%
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
108
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
Normal file
108
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./LogoApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
|
||||||
|
{
|
||||||
|
// Don't autostart the runOnce() timer
|
||||||
|
OSThread::disable();
|
||||||
|
|
||||||
|
// Grab the WindowManager singleton, for convenience
|
||||||
|
windowManager = WindowManager::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::LogoApplet::onRender()
|
||||||
|
{
|
||||||
|
// Size of the region which the logo should "scale to fit"
|
||||||
|
uint16_t logoWLimit = X(0.8);
|
||||||
|
uint16_t logoHLimit = Y(0.5);
|
||||||
|
|
||||||
|
// Get the max width and height we can manage within the region, while still maintaining aspect ratio
|
||||||
|
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||||
|
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||||
|
|
||||||
|
// Where to place the center of the logo
|
||||||
|
int16_t logoCX = X(0.5);
|
||||||
|
int16_t logoCY = Y(0.5 - 0.05);
|
||||||
|
|
||||||
|
drawLogo(logoCX, logoCY, logoW, logoH);
|
||||||
|
|
||||||
|
if (!textLeft.empty()) {
|
||||||
|
setFont(fontSmall);
|
||||||
|
printAt(0, 0, textLeft, LEFT, TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!textRight.empty()) {
|
||||||
|
setFont(fontSmall);
|
||||||
|
printAt(X(1), 0, textRight, RIGHT, TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!textTitle.empty()) {
|
||||||
|
int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo
|
||||||
|
setFont(fontTitle);
|
||||||
|
printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::LogoApplet::onForeground()
|
||||||
|
{
|
||||||
|
// If another applet has locked the display, ask it to exit
|
||||||
|
Applet *other = windowManager->whoLocked();
|
||||||
|
if (other != nullptr)
|
||||||
|
other->sendToBackground();
|
||||||
|
|
||||||
|
windowManager->claimFullscreen(this); // Take ownership of fullscreen tile
|
||||||
|
windowManager->lock(this); // Prevent other applets from requesting updates
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::LogoApplet::onBackground()
|
||||||
|
{
|
||||||
|
OSThread::disable(); // Disable auto-dismiss timer, in case applet was dismissed early (sendToBackground from outside class)
|
||||||
|
|
||||||
|
windowManager->releaseFullscreen(); // Relinquish ownership of fullscreen tile
|
||||||
|
windowManager->unlock(this); // Allow normal user applet update requests to resume
|
||||||
|
|
||||||
|
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||||
|
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||||
|
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t InkHUD::LogoApplet::runOnce()
|
||||||
|
{
|
||||||
|
LOG_DEBUG("Sent to background by timer");
|
||||||
|
sendToBackground();
|
||||||
|
return OSThread::disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin displaying the screen which is shown at startup
|
||||||
|
// Suggest EInk::await after calling this method
|
||||||
|
void InkHUD::LogoApplet::showBootScreen()
|
||||||
|
{
|
||||||
|
OSThread::setIntervalFromNow(8 * 1000UL);
|
||||||
|
OSThread::enabled = true;
|
||||||
|
|
||||||
|
textLeft = "";
|
||||||
|
textRight = "";
|
||||||
|
textTitle = xstr(APP_VERSION_SHORT);
|
||||||
|
fontTitle = fontSmall;
|
||||||
|
|
||||||
|
bringToForeground();
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin displaying the screen which is shown at shutdown
|
||||||
|
// Needs EInk::await after calling this method, to ensure display updates before shutdown
|
||||||
|
void InkHUD::LogoApplet::showShutdownScreen()
|
||||||
|
{
|
||||||
|
textLeft = "";
|
||||||
|
textRight = "";
|
||||||
|
textTitle = owner.short_name;
|
||||||
|
fontTitle = fontLarge;
|
||||||
|
|
||||||
|
bringToForeground();
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
47
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
Normal file
47
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Shows the Meshtastic logo fullscreen, with accompanying text
|
||||||
|
Used for boot and shutdown
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class LogoApplet : public Applet, public concurrency::OSThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LogoApplet();
|
||||||
|
void onRender() override;
|
||||||
|
void onForeground() override;
|
||||||
|
void onBackground() override;
|
||||||
|
|
||||||
|
// Note: interacting directly with an applet like this is non-standard
|
||||||
|
// Only permitted because this is a "system applet", which has special behavior and interacts directly with WindowManager
|
||||||
|
|
||||||
|
void showBootScreen();
|
||||||
|
void showShutdownScreen();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int32_t runOnce() override;
|
||||||
|
|
||||||
|
std::string textLeft;
|
||||||
|
std::string textRight;
|
||||||
|
std::string textTitle;
|
||||||
|
AppletFont fontTitle;
|
||||||
|
|
||||||
|
WindowManager *windowManager = nullptr; // For convenience
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
38
src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
Normal file
38
src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Set of end-point actions for the Menu Applet
|
||||||
|
|
||||||
|
Added as menu entries in MenuApplet::showPage
|
||||||
|
Behaviors assigned in MenuApplet::execute
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
enum MenuAction {
|
||||||
|
NO_ACTION,
|
||||||
|
SEND_NODEINFO,
|
||||||
|
SEND_POSITION,
|
||||||
|
SHUTDOWN,
|
||||||
|
NEXT_TILE,
|
||||||
|
TOGGLE_APPLET,
|
||||||
|
ACTIVATE_APPLETS, // Todo: remove? Possible redundant, handled by TOGGLE_APPLET?
|
||||||
|
TOGGLE_AUTOSHOW_APPLET,
|
||||||
|
SET_RECENTS,
|
||||||
|
ROTATE,
|
||||||
|
LAYOUT,
|
||||||
|
TOGGLE_BATTERY_ICON,
|
||||||
|
TOGGLE_NOTIFICATIONS,
|
||||||
|
TOGGLE_BACKLIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
612
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
Normal file
612
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./MenuApplet.h"
|
||||||
|
|
||||||
|
#include "PowerStatus.h"
|
||||||
|
#include "RTC.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes
|
||||||
|
|
||||||
|
// Options for the "Recents" menu
|
||||||
|
// These are offered to users as possible values for settings.recentlyActiveSeconds
|
||||||
|
static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120};
|
||||||
|
|
||||||
|
InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
||||||
|
{
|
||||||
|
// No timer tasks at boot
|
||||||
|
OSThread::disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onActivate()
|
||||||
|
{
|
||||||
|
// Grab pointers to some singleton components which the menu interacts with
|
||||||
|
// We could do this every time we needed them, in place,
|
||||||
|
// but this just makes the code tidier
|
||||||
|
|
||||||
|
this->windowManager = WindowManager::getInstance();
|
||||||
|
|
||||||
|
// Note: don't get instance if we're not actually using the backlight,
|
||||||
|
// or else you will unintentionally instantiate it
|
||||||
|
if (settings.optionalMenuItems.backlight) {
|
||||||
|
backlight = Drivers::LatchingBacklight::getInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onForeground()
|
||||||
|
{
|
||||||
|
// We do need this before we render, but we can optimize by just calculating it once now
|
||||||
|
systemInfoPanelHeight = getSystemInfoPanelHeight();
|
||||||
|
|
||||||
|
// Display initial menu page
|
||||||
|
showPage(MenuPage::ROOT);
|
||||||
|
|
||||||
|
// If device has a backlight which isn't controlled by aux button:
|
||||||
|
// backlight on always when menu opens.
|
||||||
|
// Courtesy to T-Echo users who removed the capacitive touch button
|
||||||
|
if (settings.optionalMenuItems.backlight) {
|
||||||
|
assert(backlight);
|
||||||
|
if (!backlight->isOn())
|
||||||
|
backlight->peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent user applets requested update while menu is open
|
||||||
|
windowManager->lock(this);
|
||||||
|
|
||||||
|
// Begin the auto-close timeout
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
OSThread::enabled = true;
|
||||||
|
|
||||||
|
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
||||||
|
windowManager->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onBackground()
|
||||||
|
{
|
||||||
|
// If device has a backlight which isn't controlled by aux button:
|
||||||
|
// Item in options submenu allows keeping backlight on after menu is closed
|
||||||
|
// If this item is deselected we will turn backlight off again, now that menu is closing
|
||||||
|
if (settings.optionalMenuItems.backlight) {
|
||||||
|
assert(backlight);
|
||||||
|
if (!backlight->isLatched())
|
||||||
|
backlight->off();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the auto-timeout
|
||||||
|
OSThread::disable();
|
||||||
|
|
||||||
|
// Resume normal rendering and button behavior of user applets
|
||||||
|
windowManager->unlock(this);
|
||||||
|
|
||||||
|
// Restore the user applet whose tile we borrowed
|
||||||
|
if (borrowedTileOwner)
|
||||||
|
borrowedTileOwner->bringToForeground();
|
||||||
|
Tile *t = getTile();
|
||||||
|
t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one)
|
||||||
|
borrowedTileOwner = nullptr;
|
||||||
|
|
||||||
|
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||||
|
// We're only updating here to ugrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu
|
||||||
|
windowManager->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the menu
|
||||||
|
// Parameter specifies which user-tile the menu will use
|
||||||
|
// The user applet originally on this tile will be restored when the menu closes
|
||||||
|
void InkHUD::MenuApplet::show(Tile *t)
|
||||||
|
{
|
||||||
|
// Remember who *really* owns this tile
|
||||||
|
borrowedTileOwner = t->getAssignedApplet();
|
||||||
|
|
||||||
|
// Hide the owner, if it is a valid applet
|
||||||
|
if (borrowedTileOwner)
|
||||||
|
borrowedTileOwner->sendToBackground();
|
||||||
|
|
||||||
|
// Break the owner's link with tile
|
||||||
|
// Relink it to menu applet
|
||||||
|
t->assignApplet(this);
|
||||||
|
|
||||||
|
// Show menu
|
||||||
|
bringToForeground();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-exit the menu applet after a period of inactivity
|
||||||
|
// The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open.
|
||||||
|
// By exiting the menu, we prevent users mistakenly believing that the data will update.
|
||||||
|
int32_t InkHUD::MenuApplet::runOnce()
|
||||||
|
{
|
||||||
|
// runOnce's interval is pushed back when a button is pressed
|
||||||
|
// If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC,
|
||||||
|
// so we close the menu.
|
||||||
|
showPage(EXIT);
|
||||||
|
|
||||||
|
// Timer should disable after firing
|
||||||
|
// This is redundant, as onBackground() will also disable
|
||||||
|
return OSThread::disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform action for a menu item, then change page
|
||||||
|
// Behaviors for MenuActions are defined here
|
||||||
|
void InkHUD::MenuApplet::execute(MenuItem item)
|
||||||
|
{
|
||||||
|
// Perform an action
|
||||||
|
// ------------------
|
||||||
|
switch (item.action) {
|
||||||
|
|
||||||
|
// Open a submenu without performing any action
|
||||||
|
// Also handles exit
|
||||||
|
case NO_ACTION:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NEXT_TILE:
|
||||||
|
// Note performed manually;
|
||||||
|
// WindowManager::nextTile is raised by aux button press only, and will interact poorly with the menu
|
||||||
|
settings.userTiles.focused = (settings.userTiles.focused + 1) % settings.userTiles.count;
|
||||||
|
windowManager->changeLayout();
|
||||||
|
cursor = 0; // No menu item selected, for quick exit after tile swap
|
||||||
|
cursorShown = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ROTATE:
|
||||||
|
settings.rotation = (settings.rotation + 1) % 4;
|
||||||
|
windowManager->changeLayout();
|
||||||
|
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LAYOUT:
|
||||||
|
// Todo: smarter incrementing of tile count
|
||||||
|
settings.userTiles.count++;
|
||||||
|
|
||||||
|
if (settings.userTiles.count == 3) // Skip 3 tiles: not done yet
|
||||||
|
settings.userTiles.count++;
|
||||||
|
|
||||||
|
if (settings.userTiles.count > settings.userTiles.maxCount) // Loop around if tile count now too high
|
||||||
|
settings.userTiles.count = 1;
|
||||||
|
|
||||||
|
windowManager->changeLayout();
|
||||||
|
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOGGLE_APPLET:
|
||||||
|
settings.userApplets.active[cursor] = !settings.userApplets.active[cursor];
|
||||||
|
windowManager->changeActivatedApplets();
|
||||||
|
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Select FULL, seeing how this action doesn't auto exit
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTIVATE_APPLETS:
|
||||||
|
// Todo: remove this action? Already handled by TOGGLE_APPLET?
|
||||||
|
windowManager->changeActivatedApplets();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOGGLE_AUTOSHOW_APPLET:
|
||||||
|
// Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage()
|
||||||
|
*items.at(cursor).checkState = !(*items.at(cursor).checkState);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOGGLE_NOTIFICATIONS:
|
||||||
|
settings.optionalFeatures.notifications = !settings.optionalFeatures.notifications;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SET_RECENTS:
|
||||||
|
// Set value of settings.recentlyActiveSeconds
|
||||||
|
// Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file)
|
||||||
|
assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]));
|
||||||
|
settings.recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SHUTDOWN:
|
||||||
|
LOG_INFO("Shutting down from menu");
|
||||||
|
power->shutdown();
|
||||||
|
// Menu is then sent to background via onShutdown
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOGGLE_BATTERY_ICON:
|
||||||
|
windowManager->toggleBatteryIcon();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOGGLE_BACKLIGHT:
|
||||||
|
// Note: backlight is already on in this situation
|
||||||
|
// We're marking that it should *remain* on once menu closes
|
||||||
|
assert(backlight);
|
||||||
|
if (backlight->isLatched())
|
||||||
|
backlight->off();
|
||||||
|
else
|
||||||
|
backlight->latch();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG_WARN("Action not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next page, as defined for the MenuItem
|
||||||
|
showPage(item.nextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a new page of MenuItems
|
||||||
|
// May reload same page, or exit menu applet entirely
|
||||||
|
// Fills the MenuApplet::items vector
|
||||||
|
void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||||
|
{
|
||||||
|
items.clear();
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
case ROOT:
|
||||||
|
// Optional: next applet
|
||||||
|
if (settings.optionalMenuItems.nextTile && settings.userTiles.count > 1)
|
||||||
|
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
|
||||||
|
|
||||||
|
// items.push_back(MenuItem("Send", MenuPage::SEND)); // TODO
|
||||||
|
items.push_back(MenuItem("Options", MenuPage::OPTIONS));
|
||||||
|
// items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO
|
||||||
|
items.push_back(MenuItem("Save & Shutdown", MenuAction::SHUTDOWN));
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEND:
|
||||||
|
items.push_back(MenuItem("Send Message", MenuPage::EXIT));
|
||||||
|
items.push_back(MenuItem("Send NodeInfo", MenuAction::SEND_NODEINFO));
|
||||||
|
items.push_back(MenuItem("Send Position", MenuAction::SEND_POSITION));
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPTIONS:
|
||||||
|
// Optional: backlight
|
||||||
|
if (settings.optionalMenuItems.backlight) {
|
||||||
|
assert(backlight);
|
||||||
|
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
|
||||||
|
MenuAction::TOGGLE_BACKLIGHT, // Action
|
||||||
|
MenuPage::EXIT // Exit once complete
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push_back(MenuItem("Applets", MenuPage::APPLETS));
|
||||||
|
items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW));
|
||||||
|
items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS));
|
||||||
|
if (settings.userTiles.maxCount > 1)
|
||||||
|
items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
|
||||||
|
items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
|
||||||
|
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
|
||||||
|
&settings.optionalFeatures.notifications));
|
||||||
|
items.push_back(
|
||||||
|
MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings.optionalFeatures.batteryIcon));
|
||||||
|
|
||||||
|
// TODO - GPS and Wifi switches
|
||||||
|
/*
|
||||||
|
// Optional: has GPS
|
||||||
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED)
|
||||||
|
items.push_back(MenuItem("Enable GPS", MenuPage::EXIT)); // TODO
|
||||||
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
|
||||||
|
items.push_back(MenuItem("Disable GPS", MenuPage::EXIT)); // TODO
|
||||||
|
|
||||||
|
// Optional: using wifi
|
||||||
|
if (!config.bluetooth.enabled)
|
||||||
|
items.push_back(MenuItem("Enable Bluetooth", MenuPage::EXIT)); // TODO: escape hatch if wifi configured wrong
|
||||||
|
*/
|
||||||
|
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case APPLETS:
|
||||||
|
populateAppletPage();
|
||||||
|
items.push_back(MenuItem("Exit", MenuAction::ACTIVATE_APPLETS));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AUTOSHOW:
|
||||||
|
populateAutoshowPage();
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RECENTS:
|
||||||
|
populateRecentsPage();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EXIT:
|
||||||
|
sendToBackground(); // Menu applet dismissed, allow normal behavior to resume
|
||||||
|
// requestUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG_WARN("Page not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the cursor, unless reloading same page
|
||||||
|
// (or now out-of-bounds)
|
||||||
|
if (page != currentPage || cursor >= items.size()) {
|
||||||
|
cursor = 0;
|
||||||
|
|
||||||
|
// ROOT menu has special handling: unselected at first, to emphasise the system info panel
|
||||||
|
if (page == ROOT)
|
||||||
|
cursorShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember which page we are on now
|
||||||
|
currentPage = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onRender()
|
||||||
|
{
|
||||||
|
if (items.size() == 0)
|
||||||
|
LOG_ERROR("Empty Menu");
|
||||||
|
|
||||||
|
// Testing only
|
||||||
|
setFont(fontSmall);
|
||||||
|
|
||||||
|
// Dimensions for the slots where we will draw menuItems
|
||||||
|
const float padding = 0.05;
|
||||||
|
const uint16_t itemH = fontSmall.lineHeight() * 2;
|
||||||
|
const int16_t itemW = width() - X(padding) - X(padding);
|
||||||
|
const int16_t itemL = X(padding);
|
||||||
|
const int16_t itemR = X(1 - padding);
|
||||||
|
int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu.
|
||||||
|
|
||||||
|
// How many full menuItems will fit on screen
|
||||||
|
uint8_t slotCount = (height() - itemT) / itemH;
|
||||||
|
|
||||||
|
// System info panel at the top of the menu
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground
|
||||||
|
const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel
|
||||||
|
|
||||||
|
// System info - top
|
||||||
|
// Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen.
|
||||||
|
// This is the same behavior we expect from the non-root menus.
|
||||||
|
// Implementing this with the systemp panel is slightly annoying though,
|
||||||
|
// and required adding the MenuApplet::getSystemInfoPanelHeight method
|
||||||
|
int16_t siT;
|
||||||
|
if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count)
|
||||||
|
siT = 0;
|
||||||
|
else
|
||||||
|
siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH);
|
||||||
|
|
||||||
|
// If showing ROOT menu,
|
||||||
|
// and the panel isn't yet scrolled off screen top
|
||||||
|
if (currentPage == ROOT) {
|
||||||
|
drawSystemInfoPanel(0, siT, width()); // Draw the panel.
|
||||||
|
itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw menu items
|
||||||
|
// ===================
|
||||||
|
|
||||||
|
// Which item will be drawn to the top-most slot?
|
||||||
|
// Initially, this is the item 0, but may increase once we begin scrolling
|
||||||
|
uint8_t firstItem;
|
||||||
|
if (cursor < slotCount)
|
||||||
|
firstItem = 0;
|
||||||
|
else
|
||||||
|
firstItem = cursor - (slotCount - 1);
|
||||||
|
|
||||||
|
// Which item will be drawn to the bottom-most slot?
|
||||||
|
// This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow
|
||||||
|
// This may be less than the slot-count, if we are reaching the end of the menuItems
|
||||||
|
uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1);
|
||||||
|
|
||||||
|
// -- Loop: draw each (visible) menu item --
|
||||||
|
for (uint8_t i = firstItem; i <= lastItem; i++) {
|
||||||
|
// Grab the menuItem
|
||||||
|
MenuItem item = items.at(i);
|
||||||
|
|
||||||
|
// Center-line for the text
|
||||||
|
int16_t center = itemT + (itemH / 2);
|
||||||
|
|
||||||
|
if (cursorShown && i == cursor)
|
||||||
|
drawRect(itemL, itemT, itemW, itemH, BLACK);
|
||||||
|
printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE);
|
||||||
|
|
||||||
|
// Testing only: circle instead of check box
|
||||||
|
if (item.checkState) {
|
||||||
|
const uint16_t cbWH = fontSmall.lineHeight(); // Checbox: width / height
|
||||||
|
const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
|
||||||
|
const int16_t cbT = center - (cbWH / 2); // Checkbox : top
|
||||||
|
// Checkbox ticked
|
||||||
|
if (*(item.checkState)) {
|
||||||
|
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
|
||||||
|
// First point of tick: pen down
|
||||||
|
const int16_t t1Y = center;
|
||||||
|
const int16_t t1X = cbL + 3;
|
||||||
|
// Second point of tick: base
|
||||||
|
const int16_t t2Y = center + (cbWH / 2) - 2;
|
||||||
|
const int16_t t2X = cbL + (cbWH / 2);
|
||||||
|
// Third point of tick: end of tail
|
||||||
|
const int16_t t3Y = center - (cbWH / 2) - 2;
|
||||||
|
const int16_t t3X = cbL + cbWH + 2;
|
||||||
|
// Draw twice: faux bold
|
||||||
|
drawLine(t1X, t1Y, t2X, t2Y, BLACK);
|
||||||
|
drawLine(t2X, t2Y, t3X, t3Y, BLACK);
|
||||||
|
drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK);
|
||||||
|
drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK);
|
||||||
|
}
|
||||||
|
// Checkbox ticked
|
||||||
|
else
|
||||||
|
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the y value (top) as we go
|
||||||
|
itemT += itemH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onButtonShortPress()
|
||||||
|
{
|
||||||
|
// Push the auto-close timer back
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
|
// Move menu cursor to next entry, then update
|
||||||
|
if (cursorShown)
|
||||||
|
cursor = (cursor + 1) % items.size();
|
||||||
|
else
|
||||||
|
cursorShown = true;
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onButtonLongPress()
|
||||||
|
{
|
||||||
|
// Push the auto-close timer back
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
|
if (cursorShown)
|
||||||
|
execute(items.at(cursor));
|
||||||
|
else
|
||||||
|
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||||
|
|
||||||
|
// If we didn't already request a specialized update, when handling a menu action,
|
||||||
|
// then perform the usual fast update.
|
||||||
|
// FAST keeps things responsive: important because we're dealing with user input
|
||||||
|
if (!wantsToRender())
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
|
||||||
|
void InkHUD::MenuApplet::populateAppletPage()
|
||||||
|
{
|
||||||
|
assert(items.size() == 0);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) {
|
||||||
|
const char *name = windowManager->getAppletName(i);
|
||||||
|
bool *isActive = &(settings.userApplets.active[i]);
|
||||||
|
items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data
|
||||||
|
// We only populate this menu page with applets which are actually active
|
||||||
|
// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient.
|
||||||
|
void InkHUD::MenuApplet::populateAutoshowPage()
|
||||||
|
{
|
||||||
|
assert(items.size() == 0);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) {
|
||||||
|
// Only add a menu item if applet is active
|
||||||
|
if (settings.userApplets.active[i]) {
|
||||||
|
const char *name = windowManager->getAppletName(i);
|
||||||
|
bool *isActive = &(settings.userApplets.autoshow[i]);
|
||||||
|
items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::populateRecentsPage()
|
||||||
|
{
|
||||||
|
// How many values are shown for use to choose from
|
||||||
|
constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]);
|
||||||
|
|
||||||
|
// Create an entry for each item in RECENTS_OPTIONS_MINUTES array
|
||||||
|
// (Defined at top of this file)
|
||||||
|
for (uint8_t i = 0; i < optionCount; i++) {
|
||||||
|
std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins";
|
||||||
|
items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the panel shown at the top of the root menu.
|
||||||
|
// Displays the clock, and several other pieces of instantaneous system info,
|
||||||
|
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||||
|
void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight)
|
||||||
|
{
|
||||||
|
// Reset the height
|
||||||
|
// We'll add to this as we add elements
|
||||||
|
uint16_t height = 0;
|
||||||
|
|
||||||
|
// Clock (potentially)
|
||||||
|
// ====================
|
||||||
|
std::string clockString = getTimeString();
|
||||||
|
if (clockString.length() > 0) {
|
||||||
|
setFont(fontLarge);
|
||||||
|
printAt(width / 2, top, clockString, CENTER, TOP);
|
||||||
|
|
||||||
|
height += fontLarge.lineHeight();
|
||||||
|
height += fontLarge.lineHeight() * 0.1; // Padding below clock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
// ===================
|
||||||
|
|
||||||
|
setFont(fontSmall);
|
||||||
|
|
||||||
|
// Position of the label row for the system info
|
||||||
|
const int16_t labelT = top + height;
|
||||||
|
height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing
|
||||||
|
|
||||||
|
// Position of the data row for the system info
|
||||||
|
const int16_t valT = top + height;
|
||||||
|
height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider)
|
||||||
|
|
||||||
|
// Position of divider between the info panel and the menu entries
|
||||||
|
const int16_t divY = top + height;
|
||||||
|
height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item)
|
||||||
|
|
||||||
|
// Create a variable number of columns
|
||||||
|
// Either 3 or 4, depending on whether we have GPS
|
||||||
|
// Todo
|
||||||
|
constexpr uint8_t N_COL = 3;
|
||||||
|
int16_t colL[N_COL];
|
||||||
|
int16_t colC[N_COL];
|
||||||
|
int16_t colR[N_COL];
|
||||||
|
for (uint8_t i = 0; i < N_COL; i++) {
|
||||||
|
colL[i] = left + ((width / N_COL) * i);
|
||||||
|
colC[i] = colL[i] + ((width / N_COL) / 2);
|
||||||
|
colR[i] = colL[i] + (width / N_COL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info blocks, left to right
|
||||||
|
|
||||||
|
// Voltage
|
||||||
|
float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
|
||||||
|
char voltageStr[6]; // "XX.XV"
|
||||||
|
sprintf(voltageStr, "%.1fV", voltage);
|
||||||
|
printAt(colC[0], labelT, "Bat", CENTER, TOP);
|
||||||
|
printAt(colC[0], valT, voltageStr, CENTER, TOP);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
for (int16_t y = valT; y <= divY; y += 3)
|
||||||
|
drawPixel(colR[0], y, BLACK);
|
||||||
|
|
||||||
|
// Channel Util
|
||||||
|
char chUtilStr[4]; // "XX%"
|
||||||
|
sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent());
|
||||||
|
printAt(colC[1], labelT, "Ch", CENTER, TOP);
|
||||||
|
printAt(colC[1], valT, chUtilStr, CENTER, TOP);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
for (int16_t y = valT; y <= divY; y += 3)
|
||||||
|
drawPixel(colR[1], y, BLACK);
|
||||||
|
|
||||||
|
// Duty Cycle (AirTimeTx)
|
||||||
|
char dutyUtilStr[4]; // "XX%"
|
||||||
|
sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent());
|
||||||
|
printAt(colC[2], labelT, "Duty", CENTER, TOP);
|
||||||
|
printAt(colC[2], valT, dutyUtilStr, CENTER, TOP);
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Divider
|
||||||
|
for (int16_t y = valT; y <= divY; y += 3)
|
||||||
|
drawPixel(colR[2], y, BLACK);
|
||||||
|
|
||||||
|
// GPS satellites - todo
|
||||||
|
printAt(colC[3], labelT, "Sats", CENTER, TOP);
|
||||||
|
printAt(colC[3], valT, "ToDo", CENTER, TOP);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Horizontal divider, at bottom of system info panel
|
||||||
|
for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item
|
||||||
|
drawPixel(x, divY, BLACK);
|
||||||
|
|
||||||
|
if (renderedHeight != nullptr)
|
||||||
|
*renderedHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the height of the the panel drawn at the top of the menu
|
||||||
|
// This is inefficient, as we do actually have to render the panel to determine the height
|
||||||
|
// It solves a catch-22 situtation, where slotCount needs to know panel height, and panel height needs to know slotCount
|
||||||
|
uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
|
||||||
|
{
|
||||||
|
// Render *waay* off screen
|
||||||
|
uint16_t height = 0;
|
||||||
|
drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height);
|
||||||
|
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
60
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
Normal file
60
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
|
||||||
|
#include "graphics/niche/InkHUD/Applet.h"
|
||||||
|
#include "graphics/niche/InkHUD/WindowManager.h"
|
||||||
|
|
||||||
|
#include "./MenuItem.h"
|
||||||
|
#include "./MenuPage.h"
|
||||||
|
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class Applet;
|
||||||
|
|
||||||
|
class MenuApplet : public Applet, public concurrency::OSThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MenuApplet();
|
||||||
|
void onActivate() override;
|
||||||
|
void onForeground() override;
|
||||||
|
void onBackground() override;
|
||||||
|
void onButtonShortPress() override;
|
||||||
|
void onButtonLongPress() override;
|
||||||
|
void onRender() override;
|
||||||
|
|
||||||
|
void show(Tile *t); // Open the menu, onto a user tile
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int32_t runOnce() override;
|
||||||
|
|
||||||
|
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||||
|
void showPage(MenuPage page); // Load and display a MenuPage
|
||||||
|
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||||
|
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||||
|
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||||
|
uint16_t getSystemInfoPanelHeight();
|
||||||
|
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||||
|
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||||
|
|
||||||
|
MenuPage currentPage;
|
||||||
|
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||||
|
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||||
|
|
||||||
|
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||||
|
|
||||||
|
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||||
|
|
||||||
|
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||||
|
|
||||||
|
WindowManager *windowManager = nullptr; // Convenient access to the InkHUD::WindowManager singleton
|
||||||
|
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
47
src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h
Normal file
47
src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
One item of a MenuPage, in InkHUD::MenuApplet
|
||||||
|
|
||||||
|
Added to MenuPages in InkHUD::showPage
|
||||||
|
|
||||||
|
- May open a submenu or exit
|
||||||
|
- May perform an action
|
||||||
|
- May toggle a bool value, shown by a checkbox
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./MenuAction.h"
|
||||||
|
#include "./MenuPage.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
// One item of a MenuPage
|
||||||
|
class MenuItem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string label;
|
||||||
|
MenuAction action = NO_ACTION;
|
||||||
|
MenuPage nextPage = EXIT;
|
||||||
|
bool *checkState = nullptr;
|
||||||
|
|
||||||
|
// Various constructors, depending on the intended function of the item
|
||||||
|
|
||||||
|
MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {}
|
||||||
|
MenuItem(const char *label, MenuAction action) : label(label), action(action) {}
|
||||||
|
MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {}
|
||||||
|
MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState)
|
||||||
|
: label(label), action(action), nextPage(nextPage), checkState(checkState)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
30
src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h
Normal file
30
src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Sub-menu for InkHUD::MenuApplet
|
||||||
|
Structure of the menu is defined in InkHUD::showPage
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
// Sub-menu for MenuApplet
|
||||||
|
enum MenuPage : uint8_t {
|
||||||
|
ROOT, // Initial menu page
|
||||||
|
SEND,
|
||||||
|
OPTIONS,
|
||||||
|
APPLETS,
|
||||||
|
AUTOSHOW,
|
||||||
|
RECENTS, // Select length of "recentlyActiveSeconds"
|
||||||
|
EXIT, // Dismiss the menu applet
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,40 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
A notification which might be displayed by the NotificationApplet
|
||||||
|
|
||||||
|
An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification.
|
||||||
|
An Applet should veto a notification if it is already displaying the same info which the notification would convey.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class Notification
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type;
|
||||||
|
|
||||||
|
uint32_t timestamp;
|
||||||
|
|
||||||
|
uint8_t getChannel() { return channel; }
|
||||||
|
uint32_t getSender() { return sender; }
|
||||||
|
uint8_t getBatteryPercentage() { return batteryPercentage; }
|
||||||
|
|
||||||
|
friend class NotificationApplet;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint8_t channel;
|
||||||
|
uint32_t sender;
|
||||||
|
uint8_t batteryPercentage;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,219 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
#include "./NotificationApplet.h"
|
||||||
|
|
||||||
|
#include "./Notification.h"
|
||||||
|
|
||||||
|
#include "RTC.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
void InkHUD::NotificationApplet::onActivate()
|
||||||
|
{
|
||||||
|
textMessageObserver.observe(textMessageModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This applet probably won't ever be deactivated
|
||||||
|
void InkHUD::NotificationApplet::onDeactivate()
|
||||||
|
{
|
||||||
|
textMessageObserver.unobserve(textMessageModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect meta-info about the text message, and ask for approval for the notification
|
||||||
|
// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render()
|
||||||
|
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||||
|
{
|
||||||
|
// System applets are always active
|
||||||
|
assert(isActive());
|
||||||
|
|
||||||
|
// Abort if feature disabled
|
||||||
|
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
|
||||||
|
if (!settings.optionalFeatures.notifications)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Abort if this is an outgoing message
|
||||||
|
if (getFrom(p) == nodeDB->getNodeNum())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Abort if message was only an "emoji reaction"
|
||||||
|
// Possibly some implemetation of this in future?
|
||||||
|
if (p->decoded.emoji)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
Notification n;
|
||||||
|
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||||
|
|
||||||
|
// Gather info: in-channel message
|
||||||
|
if (isBroadcast(p->to)) {
|
||||||
|
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||||
|
n.channel = p->channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather info: DM
|
||||||
|
else {
|
||||||
|
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
|
||||||
|
n.sender = p->from;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should display the notification
|
||||||
|
// A foreground applet might already be displaying this info
|
||||||
|
hasNotification = true;
|
||||||
|
currentNotification = n;
|
||||||
|
if (isApproved()) {
|
||||||
|
bringToForeground();
|
||||||
|
WindowManager::getInstance()->forceUpdate();
|
||||||
|
} else
|
||||||
|
hasNotification = false; // Clear the pending notification: it was rejected
|
||||||
|
|
||||||
|
// Return zero: no issues here, carry on notifying other observers!
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::NotificationApplet::onRender()
|
||||||
|
{
|
||||||
|
// Clear the region beneath the tile
|
||||||
|
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||||
|
// We do need to do this with the battery though, as it is an "overlay"
|
||||||
|
fillRect(0, 0, width(), height(), WHITE);
|
||||||
|
|
||||||
|
setFont(fontSmall);
|
||||||
|
|
||||||
|
// Padding (horizontal)
|
||||||
|
const uint16_t padW = 4;
|
||||||
|
|
||||||
|
// Main border
|
||||||
|
drawRect(0, 0, width(), height(), BLACK);
|
||||||
|
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
|
||||||
|
|
||||||
|
// Timestamp (potentially)
|
||||||
|
// ====================
|
||||||
|
std::string ts = getTimeString(currentNotification.timestamp);
|
||||||
|
uint16_t tsW = 0;
|
||||||
|
int16_t divX = 0;
|
||||||
|
|
||||||
|
// Timestamp available
|
||||||
|
if (ts.length() > 0) {
|
||||||
|
tsW = getTextWidth(ts);
|
||||||
|
divX = padW + tsW + padW;
|
||||||
|
|
||||||
|
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
|
||||||
|
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
|
||||||
|
|
||||||
|
setCrop(1, 1, divX - 1, height() - 2);
|
||||||
|
|
||||||
|
// Drop shadow
|
||||||
|
setTextColor(WHITE);
|
||||||
|
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
|
||||||
|
|
||||||
|
// Bold text
|
||||||
|
setTextColor(BLACK);
|
||||||
|
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main text
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
// Background fill
|
||||||
|
// - medium dark (1/3)
|
||||||
|
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
|
||||||
|
|
||||||
|
uint16_t availableWidth = width() - divX - padW;
|
||||||
|
std::string text = getNotificationText(availableWidth);
|
||||||
|
|
||||||
|
int16_t textM = divX + padW + (getTextWidth(text) / 2);
|
||||||
|
|
||||||
|
// Restrict area for printing
|
||||||
|
// - don't overlap border, or diveder
|
||||||
|
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
|
||||||
|
|
||||||
|
// Drop shadow
|
||||||
|
// - thick white text
|
||||||
|
setTextColor(WHITE);
|
||||||
|
printThick(textM, height() / 2, text, 4, 4);
|
||||||
|
|
||||||
|
// Main text
|
||||||
|
// - faux bold: double width
|
||||||
|
setTextColor(BLACK);
|
||||||
|
printThick(textM, height() / 2, text, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
||||||
|
// Called internally when we first get a "notifiable event", and then again before render,
|
||||||
|
// in case autoshow swapped which applet was displayed
|
||||||
|
bool InkHUD::NotificationApplet::isApproved()
|
||||||
|
{
|
||||||
|
// Instead of an assert
|
||||||
|
if (!hasNotification) {
|
||||||
|
LOG_WARN("No notif to approve");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WindowManager::getInstance()->approveNotification(currentNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that the notification should no-longer be rendered
|
||||||
|
// In addition to calling thing method, code needs to request a re-render of all applets
|
||||||
|
void InkHUD::NotificationApplet::dismiss()
|
||||||
|
{
|
||||||
|
sendToBackground();
|
||||||
|
hasNotification = false;
|
||||||
|
// Not requesting update directly from this method,
|
||||||
|
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a string for the main body text of a notification
|
||||||
|
// Formatted to suit screen width
|
||||||
|
// Takes info from InkHUD::currentNotification
|
||||||
|
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable)
|
||||||
|
{
|
||||||
|
assert(hasNotification);
|
||||||
|
|
||||||
|
std::string text;
|
||||||
|
|
||||||
|
// Text message
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT,
|
||||||
|
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
||||||
|
|
||||||
|
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
||||||
|
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||||
|
|
||||||
|
// Pick source of message
|
||||||
|
MessageStore::Message *message = isBroadcast ? &latestMessage.broadcast : &latestMessage.dm;
|
||||||
|
|
||||||
|
// Find info about the sender
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||||
|
|
||||||
|
// Leading tag (channel vs. DM)
|
||||||
|
text += isBroadcast ? "From:" : "DM: ";
|
||||||
|
|
||||||
|
// Sender id
|
||||||
|
if (node && node->has_user)
|
||||||
|
text += node->user.short_name;
|
||||||
|
else
|
||||||
|
text += hexifyNodeNum(message->sender);
|
||||||
|
|
||||||
|
// Check if text fits
|
||||||
|
// - use a longer string, if we have the space
|
||||||
|
if (getTextWidth(text) < widthAvailable * 0.5) {
|
||||||
|
text.clear();
|
||||||
|
|
||||||
|
// Leading tag (channel vs. DM)
|
||||||
|
text += isBroadcast ? "Msg from " : "DM from ";
|
||||||
|
|
||||||
|
// Sender id
|
||||||
|
if (node && node->has_user)
|
||||||
|
text += node->user.short_name;
|
||||||
|
else
|
||||||
|
text += hexifyNodeNum(message->sender);
|
||||||
|
|
||||||
|
text += ": ";
|
||||||
|
text += message->text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user