mirror of
https://github.com/meshtastic/firmware.git
synced 2025-04-23 17:13:38 +00:00
Merge branch 'master' into nomad-gemini
This commit is contained in:
commit
9c3ceaf6e9
@ -1,9 +1,10 @@
|
||||
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
|
||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12
|
||||
|
||||
USER root
|
||||
|
||||
# trunk-ignore(terrascan/AC_DOCKER_0002): Known terrascan issue
|
||||
# trunk-ignore(hadolint/DL3008): Use latest version of packages
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
ca-certificates \
|
||||
@ -27,9 +28,11 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
hwdata \
|
||||
gpg \
|
||||
gnupg2 \
|
||||
libusb-1.0-0-dev \
|
||||
libi2c-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pipx install platformio==6.1.15
|
||||
RUN pipx install platformio
|
||||
|
||||
COPY 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
2
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
labels: [bug, triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
2
.github/ISSUE_TEMPLATE/New Board.yml
vendored
2
.github/ISSUE_TEMPLATE/New Board.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: New Board
|
||||
description: Request us to support new hardware
|
||||
title: "[Board]: "
|
||||
labels: ["enhancement", "triage"]
|
||||
labels: [enhancement, triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: Feature Request
|
||||
description: Request a new feature
|
||||
title: "[Feature Request]: "
|
||||
labels: ["enhancement"]
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
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:
|
||||
description: Processor arch name
|
||||
required: true
|
||||
default: "esp32"
|
||||
default: esp32
|
||||
|
||||
runs:
|
||||
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"
|
||||
description: "Base build actions for Meshtastic Platform IO steps"
|
||||
name: Setup Build Base Composite Action
|
||||
description: Base build actions for Meshtastic Platform IO steps
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: composite
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
|
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
|
||||
updates:
|
||||
- package-ecosystem: docker
|
||||
directory: devcontainer
|
||||
directory: /.devcontainer
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
||||
time: "05:00"
|
||||
timezone: US/Pacific
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
||||
time: "05:00"
|
||||
timezone: US/Pacific
|
||||
- package-ecosystem: gitsubmodule
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
||||
time: "05:00"
|
||||
timezone: US/Pacific
|
||||
- package-ecosystem: github-actions
|
||||
directory: /.github/workflows
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "05:00" # trunk-ignore(yamllint/quoted-strings): required by dependabot syntax check
|
||||
time: "05:00"
|
||||
timezone: US/Pacific
|
||||
|
2
.github/workflows/build_nrf52.yml
vendored
2
.github/workflows/build_nrf52.yml
vendored
@ -7,6 +7,8 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-nrf52:
|
||||
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
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-rpi2040:
|
||||
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
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-stm32:
|
||||
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
|
||||
secrets: inherit
|
||||
|
||||
package-pio-deps-native:
|
||||
package-pio-deps-native-tft:
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
with:
|
||||
pio_env: native
|
||||
pio_env: native-tft
|
||||
secrets: inherit
|
||||
|
||||
test-native:
|
||||
@ -288,7 +288,7 @@ jobs:
|
||||
needs:
|
||||
- gather-artifacts
|
||||
- build-debian-src
|
||||
- package-pio-deps-native
|
||||
- package-pio-deps-native-tft
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@ -324,10 +324,10 @@ jobs:
|
||||
merge-multiple: true
|
||||
path: ./output/debian-src
|
||||
|
||||
- name: Download native pio deps
|
||||
- name: Download `native-tft` pio deps
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: platformio-deps-native-${{ steps.version.outputs.long }}
|
||||
pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output/pio-deps-native
|
||||
|
||||
@ -352,6 +352,12 @@ jobs:
|
||||
run: >-
|
||||
bin/bump_version.py
|
||||
|
||||
- name: Ensure debian deps are installed
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -y --fix-missing
|
||||
sudo apt-get install -y devscripts
|
||||
|
||||
- name: Update debian changelog
|
||||
run: >-
|
||||
debian/ci_changelog.sh
|
||||
|
8
.github/workflows/nightly.yml
vendored
8
.github/workflows/nightly.yml
vendored
@ -9,7 +9,7 @@ permissions: read-all
|
||||
jobs:
|
||||
trunk_check:
|
||||
name: Trunk Check and Upload
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -21,8 +21,9 @@ jobs:
|
||||
trunk-token: ${{ secrets.TRUNK_TOKEN }}
|
||||
|
||||
trunk_upgrade:
|
||||
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
||||
name: Trunk Upgrade (PR)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write # For trunk to create PRs
|
||||
pull-requests: write # For trunk to create PRs
|
||||
@ -30,6 +31,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# See https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
||||
- name: Trunk Upgrade
|
||||
uses: trunk-io/trunk-action/upgrade@v1
|
||||
with:
|
||||
base: master
|
||||
|
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:
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: "0 1 * * 6"
|
||||
- cron: 0 1 * * 6
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
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
|
||||
on: pull_request
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
semgrep-diff:
|
||||
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:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
exempt-issue-labels: pinned,3.0
|
||||
exempt-pr-labels: pinned,3.0
|
||||
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -2,9 +2,11 @@ name: End to end tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # Run every day at midnight
|
||||
- cron: 0 0 * * * # Run every day at midnight
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
native-tests:
|
||||
uses: ./.github/workflows/test_native.yml
|
||||
|
@ -11,7 +11,7 @@ permissions: read-all
|
||||
jobs:
|
||||
trunk_check:
|
||||
name: Trunk Code Quality Annotate
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
checks: write # For trunk to post annotations
|
||||
contents: read # For repo checkout
|
4
.github/workflows/trunk_check.yml
vendored
4
.github/workflows/trunk_check.yml
vendored
@ -9,7 +9,7 @@ permissions: read-all
|
||||
jobs:
|
||||
trunk_check:
|
||||
name: Trunk Check Runner
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
checks: write # For trunk to post annotations
|
||||
contents: read # For repo checkout
|
||||
@ -20,3 +20,5 @@ jobs:
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
with:
|
||||
save-annotations: true
|
||||
|
6
.github/workflows/trunk_format_pr.yml
vendored
6
.github/workflows/trunk_format_pr.yml
vendored
@ -4,11 +4,15 @@ on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
trunk-fmt:
|
||||
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
6
.github/workflows/update_protobufs.yml
vendored
6
.github/workflows/update_protobufs.yml
vendored
@ -1,10 +1,14 @@
|
||||
name: Update protobufs and regenerate classes
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
update-protobufs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,6 +1,9 @@
|
||||
[submodule "protobufs"]
|
||||
path = protobufs
|
||||
url = https://github.com/meshtastic/protobufs.git
|
||||
[submodule "lib/device-ui"]
|
||||
path = lib/device-ui
|
||||
url = https://github.com/meshtastic/device-ui.git
|
||||
[submodule "meshtestic"]
|
||||
path = meshtestic
|
||||
url = https://github.com/meshtastic/meshTestic
|
||||
|
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
|
||||
url: false
|
||||
whitespace: false
|
||||
headings: false
|
||||
|
@ -1,37 +1,36 @@
|
||||
version: 0.1
|
||||
cli:
|
||||
version: 1.22.8
|
||||
version: 1.22.10
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.6.6
|
||||
ref: v1.6.7
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- prettier@3.4.2
|
||||
- trufflehog@3.86.1
|
||||
- prettier@3.5.2
|
||||
- trufflehog@3.88.14
|
||||
- yamllint@1.35.1
|
||||
- bandit@1.8.0
|
||||
- checkov@3.2.334
|
||||
- bandit@1.8.3
|
||||
- checkov@3.2.378
|
||||
- terrascan@1.19.9
|
||||
- trivy@0.58.0
|
||||
#- trufflehog@3.63.2-rc0
|
||||
- trivy@0.59.1
|
||||
- taplo@0.9.3
|
||||
- ruff@0.8.3
|
||||
- isort@5.13.2
|
||||
- markdownlint@0.43.0
|
||||
- oxipng@9.1.3
|
||||
- ruff@0.9.7
|
||||
- isort@6.0.1
|
||||
- markdownlint@0.44.0
|
||||
- oxipng@9.1.4
|
||||
- svgo@3.3.2
|
||||
- actionlint@1.7.4
|
||||
- flake8@7.1.1
|
||||
- actionlint@1.7.7
|
||||
- flake8@7.1.2
|
||||
- hadolint@2.12.1-beta
|
||||
- shfmt@3.6.0
|
||||
- shellcheck@0.10.0
|
||||
- black@24.10.0
|
||||
- black@25.1.0
|
||||
- git-diff-check
|
||||
- gitleaks@8.21.2
|
||||
- gitleaks@8.24.0
|
||||
- clang-format@16.0.3
|
||||
#- prettier@3.3.3
|
||||
- clang-tidy@16.0.3
|
||||
ignore:
|
||||
- linters: [ALL]
|
||||
paths:
|
||||
|
21
Dockerfile
21
Dockerfile
@ -1,21 +1,23 @@
|
||||
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
|
||||
# trunk-ignore-all(hadolint/DL3008): Use latest version of apt packages for buildchain
|
||||
# trunk-ignore-all(trivy/DS002): We must run as root for this container
|
||||
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
|
||||
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
|
||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
|
||||
FROM python:3.12-bookworm AS builder
|
||||
FROM python:3.13-bookworm AS builder
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Etc/UTC
|
||||
|
||||
# Install Dependencies
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y wget g++ zip git ca-certificates \
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
wget g++ zip git ca-certificates \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||
pip install --no-cache-dir -U platformio==6.1.16 && \
|
||||
mkdir /tmp/firmware
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
|
||||
# Copy source code
|
||||
WORKDIR /tmp/firmware
|
||||
@ -35,8 +37,9 @@ ENV TZ=Etc/UTC
|
||||
# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
&& mkdir -p /etc/meshtasticd/ssl
|
||||
|
@ -1,14 +1,18 @@
|
||||
# trunk-ignore-all(trivy/DS002): We must run as root for this container
|
||||
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
|
||||
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
|
||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
|
||||
FROM python:3.12-alpine3.21 AS builder
|
||||
FROM python:3.13-alpine3.21 AS builder
|
||||
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
RUN apk add bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
libusb-dev i2c-tools-dev openssl-dev pkgconf argp-standalone && \
|
||||
pip install --no-cache-dir -U platformio==6.1.16 && \
|
||||
mkdir /tmp/firmware
|
||||
RUN apk --no-cache add \
|
||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
libusb-dev i2c-tools-dev openssl-dev pkgconf argp-standalone \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
|
||||
WORKDIR /tmp/firmware
|
||||
COPY . /tmp/firmware
|
||||
@ -27,7 +31,9 @@ FROM alpine:3.21
|
||||
# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root
|
||||
USER root
|
||||
|
||||
RUN apk add libstdc++ libgpiod yaml-cpp libusb i2c-tools \
|
||||
RUN apk --no-cache add \
|
||||
libstdc++ libgpiod yaml-cpp libusb i2c-tools \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
&& mkdir -p /etc/meshtasticd/ssl
|
||||
|
@ -2,7 +2,7 @@
|
||||
[esp32_base]
|
||||
extends = arduino_base
|
||||
custom_esp32_kind = esp32
|
||||
platform = platformio/espressif32@6.9.0
|
||||
platform = platformio/espressif32@6.10.0
|
||||
|
||||
build_src_filter =
|
||||
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
||||
@ -37,6 +37,7 @@ build_flags =
|
||||
-DLIBPAX_ARDUINO
|
||||
-DLIBPAX_WIFI
|
||||
-DLIBPAX_BLE
|
||||
-DHAS_UDP_MULTICAST=1
|
||||
;-DDEBUG_HEAP
|
||||
|
||||
lib_deps =
|
||||
@ -45,9 +46,9 @@ lib_deps =
|
||||
${environmental_base.lib_deps}
|
||||
${radiolib_base.lib_deps}
|
||||
https://github.com/meshtastic/esp32_https_server.git#23665b3adc080a311dcbb586ed5941b5f94d6ea2
|
||||
h2zero/NimBLE-Arduino@^1.4.2
|
||||
h2zero/NimBLE-Arduino@^1.4.3
|
||||
https://github.com/dbinfrago/libpax.git#3cdc0371c375676a97967547f4065607d4c53fd1
|
||||
lewisxhe/XPowersLib@^0.2.6
|
||||
lewisxhe/XPowersLib@^0.2.7
|
||||
https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f
|
||||
rweather/Crypto@^0.4.0
|
||||
|
||||
@ -65,4 +66,4 @@ lib_ignore =
|
||||
|
||||
; customize the partition table
|
||||
; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables
|
||||
board_build.partitions = partition-table.csv
|
||||
board_build.partitions = partition-table.csv
|
||||
|
@ -24,7 +24,7 @@ lib_deps =
|
||||
${networking_base.lib_deps}
|
||||
${environmental_base.lib_deps}
|
||||
${radiolib_base.lib_deps}
|
||||
lewisxhe/XPowersLib@^0.2.6
|
||||
lewisxhe/XPowersLib@^0.2.7
|
||||
https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f
|
||||
rweather/Crypto@^0.4.0
|
||||
|
||||
@ -38,4 +38,4 @@ lib_ignore =
|
||||
NonBlockingRTTTL
|
||||
NimBLE-Arduino
|
||||
libpax
|
||||
|
||||
|
||||
|
@ -4,8 +4,8 @@ platform = platformio/nordicnrf52@^10.7.0
|
||||
extends = arduino_base
|
||||
platform_packages =
|
||||
; our custom Git version until they merge our PR
|
||||
framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf
|
||||
toolchain-gccarmnoneeabi@~1.90301.0
|
||||
platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf
|
||||
platformio/toolchain-gccarmnoneeabi@~1.90301.0
|
||||
|
||||
build_type = debug
|
||||
build_flags =
|
||||
|
@ -18,6 +18,7 @@ build_src_filter =
|
||||
|
||||
lib_ignore =
|
||||
BluetoothOTA
|
||||
lvgl
|
||||
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
|
@ -1,8 +1,8 @@
|
||||
; Common settings for rp2040 Processor based targets
|
||||
[rp2350_base]
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#19e30129fb1428b823be585c787dcb4ac0d9014c ; For arduino-pico >=4.2.1
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 ; For arduino-pico >= 4.4.3
|
||||
extends = arduino_base
|
||||
platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#6024e9a7e82a72e38dd90f42029ba3748835eb2e ; 4.3.0 with fix MDNS
|
||||
platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#4.4.3
|
||||
|
||||
board_build.core = earlephilhower
|
||||
board_build.filesystem_size = 0.5m
|
||||
@ -10,7 +10,6 @@ build_flags =
|
||||
${arduino_base.build_flags} -Wno-unused-variable -Wcast-align
|
||||
-Isrc/platform/rp2xx0
|
||||
-D__PLAT_RP2350__
|
||||
# -D _POSIX_THREADS
|
||||
build_src_filter =
|
||||
${arduino_base.build_src_filter} -<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]
|
||||
extends = arduino_base
|
||||
platform = ststm32
|
||||
platform_packages = platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32.git#ea74156acd823b6d14739f389e6cdc648f8ee36e
|
||||
platform = platformio/ststm32
|
||||
platform_packages = platformio/framework-arduinoststm32@^4.20900.0
|
||||
|
||||
build_type = release
|
||||
|
||||
|
@ -35,11 +35,11 @@ cp $SRCBIN $OUTDIR/$basename-update.bin
|
||||
|
||||
echo "Building Filesystem for ESP32 targets"
|
||||
pio run --environment $1 -t buildfs
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$VERSION.bin
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
||||
# Remove webserver files from the filesystem and rebuild
|
||||
ls -l data/static # Diagnostic list of files
|
||||
rm -rf data/static
|
||||
pio run --environment $1 -t buildfs
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$VERSION.bin
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||
cp bin/device-install.* $OUTDIR
|
||||
cp bin/device-update.* $OUTDIR
|
||||
cp bin/device-update.* $OUTDIR
|
@ -24,7 +24,7 @@ mkdir -p $OUTDIR/
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
platformio pkg update --environment native || platformioFailed
|
||||
pio pkg update --environment native || platformioFailed
|
||||
pio run --environment native || platformioFailed
|
||||
cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||
cp bin/native-install.* $OUTDIR
|
||||
|
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)
|
||||
else:
|
||||
outlist.append(section)
|
||||
# Add the TFT variants if the base variant is selected
|
||||
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||
outlist.append(section)
|
||||
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||
outlist.append(section)
|
||||
if "board_check" in config[config[c].name]:
|
||||
if (config[config[c].name]["board_check"] == "true") & (
|
||||
"check" in options
|
||||
@ -43,4 +48,4 @@ for subdir, dirs, files in os.walk(rootdir):
|
||||
if ("quick" in options) & (len(outlist) > 3):
|
||||
print(json.dumps(random.sample(outlist, 3)))
|
||||
else:
|
||||
print(json.dumps(outlist))
|
||||
print(json.dumps(outlist))
|
1
debian/control
vendored
1
debian/control
vendored
@ -3,6 +3,7 @@ Section: misc
|
||||
Priority: optional
|
||||
Maintainer: Austin Lane <vidplace7@gmail.com>
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
lsb-release,
|
||||
tar,
|
||||
gzip,
|
||||
platformio,
|
||||
|
9
debian/rules
vendored
9
debian/rules
vendored
@ -11,6 +11,15 @@ PIO_ENV:=\
|
||||
PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
|
||||
PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||
|
||||
# Raspbian armhf builds should be compatible with armv6-hardfloat
|
||||
# https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags
|
||||
ifneq (,$(findstring Raspbian,$(shell lsb_release -is)))
|
||||
ifeq ($(DEB_BUILD_ARCH),armhf)
|
||||
PIO_ENV+=\
|
||||
PLATFORMIO_BUILD_FLAGS="-mfloat-abi=hard -mfpu=vfp -march=armv6zk"
|
||||
endif
|
||||
endif
|
||||
|
||||
override_dh_auto_build:
|
||||
# Extract tarballs within source deb
|
||||
tar -xf pio.tar
|
||||
|
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>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -60,7 +60,7 @@ lib_deps =
|
||||
https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159
|
||||
https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4
|
||||
https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0
|
||||
nanopb/Nanopb@0.4.9
|
||||
nanopb/Nanopb@0.4.91
|
||||
erriez/ErriezCRC32@1.0.1
|
||||
|
||||
; Used for the code analysis in PIO Home / Inspect
|
||||
|
@ -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 "modules/ExternalNotificationModule.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
@ -99,6 +100,13 @@ ButtonThread::ButtonThread() : OSThread("Button")
|
||||
userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click?
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
// Used to detach and reattach interrupts
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
attachButtonInterrupts();
|
||||
#endif
|
||||
}
|
||||
@ -320,6 +328,26 @@ void ButtonThread::detachButtonInterrupts()
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int ButtonThread::beforeLightSleep(void *unused)
|
||||
{
|
||||
detachButtonInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachButtonInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Watch a GPIO and if we get an IRQ, wake the main thread.
|
||||
* Use to add wake on button press
|
||||
|
@ -37,6 +37,12 @@ class ButtonThread : public concurrency::OSThread
|
||||
void detachButtonInterrupts();
|
||||
void storeClickCount();
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
||||
static OneButton userButton; // Static - accessed from an interrupt
|
||||
@ -48,6 +54,14 @@ class ButtonThread : public concurrency::OSThread
|
||||
OneButton userButtonTouch;
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<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
|
||||
static volatile ButtonEventType btnEvent;
|
||||
|
||||
|
@ -23,6 +23,10 @@ SPIClass SPI1(HSPI);
|
||||
#define SDHandler SPI
|
||||
#endif
|
||||
|
||||
#ifndef SD_SPI_FREQUENCY
|
||||
#define SD_SPI_FREQUENCY 4000000U
|
||||
#endif
|
||||
|
||||
#endif // HAS_SDCARD
|
||||
|
||||
#if defined(ARCH_STM32WL)
|
||||
@ -361,8 +365,7 @@ void setupSDCard()
|
||||
#ifdef HAS_SDCARD
|
||||
concurrency::LockGuard g(spiLock);
|
||||
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
|
||||
|
||||
if (!SD.begin(SDCARD_CS, SDHandler)) {
|
||||
if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) {
|
||||
LOG_DEBUG("No SD_MMC card detected");
|
||||
return;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
#define STATUS_TYPE_POWER 1
|
||||
#define STATUS_TYPE_GPS 2
|
||||
#define STATUS_TYPE_NODE 3
|
||||
#define STATUS_TYPE_BLUETOOTH 4
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
|
@ -244,6 +244,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("BMP-388", (uint8_t)addr.address);
|
||||
type = BMP_3XX;
|
||||
break;
|
||||
case 0x60: // BMP-390 should be 0x60
|
||||
logFoundDevice("BMP-390", (uint8_t)addr.address);
|
||||
type = BMP_3XX;
|
||||
break;
|
||||
case 0x58: // BMP-280 should be 0x58
|
||||
default:
|
||||
logFoundDevice("BMP-280", (uint8_t)addr.address);
|
||||
@ -521,4 +525,4 @@ void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address)
|
||||
{
|
||||
LOG_INFO("%s found at address 0x%x", device, address);
|
||||
}
|
||||
#endif
|
||||
#endif
|
@ -6,28 +6,28 @@
|
||||
|
||||
void d_writeCommand(uint8_t c)
|
||||
{
|
||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||
SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||
if (PIN_EINK_DC >= 0)
|
||||
digitalWrite(PIN_EINK_DC, LOW);
|
||||
if (PIN_EINK_CS >= 0)
|
||||
digitalWrite(PIN_EINK_CS, LOW);
|
||||
SPI.transfer(c);
|
||||
SPI1.transfer(c);
|
||||
if (PIN_EINK_CS >= 0)
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
if (PIN_EINK_DC >= 0)
|
||||
digitalWrite(PIN_EINK_DC, HIGH);
|
||||
SPI.endTransaction();
|
||||
SPI1.endTransaction();
|
||||
}
|
||||
|
||||
void d_writeData(uint8_t d)
|
||||
{
|
||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||
SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||
if (PIN_EINK_CS >= 0)
|
||||
digitalWrite(PIN_EINK_CS, LOW);
|
||||
SPI.transfer(d);
|
||||
SPI1.transfer(d);
|
||||
if (PIN_EINK_CS >= 0)
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
SPI.endTransaction();
|
||||
SPI1.endTransaction();
|
||||
}
|
||||
|
||||
unsigned long d_waitWhileBusy(uint16_t busy_time)
|
||||
@ -53,7 +53,7 @@ unsigned long d_waitWhileBusy(uint16_t busy_time)
|
||||
|
||||
void scanEInkDevice(void)
|
||||
{
|
||||
SPI.begin();
|
||||
SPI1.begin();
|
||||
d_writeCommand(0x22);
|
||||
d_writeData(0x83);
|
||||
d_writeCommand(0x20);
|
||||
@ -62,6 +62,6 @@ void scanEInkDevice(void)
|
||||
LOG_DEBUG("EInk display found");
|
||||
else
|
||||
LOG_DEBUG("EInk display not found");
|
||||
SPI.end();
|
||||
SPI1.end();
|
||||
}
|
||||
#endif
|
100
src/gps/GPS.cpp
100
src/gps/GPS.cpp
@ -48,8 +48,6 @@ HardwareSerial *GPS::_serial_gps = nullptr;
|
||||
|
||||
GPS *gps = nullptr;
|
||||
|
||||
static const char *ACK_SUCCESS_MESSAGE = "Get ack success!";
|
||||
|
||||
static GPSUpdateScheduling scheduling;
|
||||
|
||||
/// Multiple GPS instances might use the same serial port (in sequence), but we can
|
||||
@ -437,6 +435,10 @@ static const int serialSpeeds[3] = {9600, 115200, 38400};
|
||||
static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE};
|
||||
#endif
|
||||
|
||||
#ifndef GPS_PROBETRIES
|
||||
#define GPS_PROBETRIES 2
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Setup the GPS based on the model detected.
|
||||
* We detect the GPS by cycling through a set of baud rates, first common then rare.
|
||||
@ -460,11 +462,7 @@ bool GPS::setup()
|
||||
digitalWrite(PIN_GPS_EN, HIGH);
|
||||
delay(1000);
|
||||
#endif
|
||||
#ifdef TRACKER_T1000_E
|
||||
if (probeTries < 5) {
|
||||
#else
|
||||
if (probeTries < 2) {
|
||||
#endif
|
||||
if (probeTries < GPS_PROBETRIES) {
|
||||
LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]);
|
||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
@ -475,11 +473,7 @@ bool GPS::setup()
|
||||
}
|
||||
}
|
||||
// Rare Serial Speeds
|
||||
#ifdef TRACKER_T1000_E
|
||||
if (probeTries == 5) {
|
||||
#else
|
||||
if (probeTries == 2) {
|
||||
#endif
|
||||
if (probeTries == GPS_PROBETRIES) {
|
||||
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
|
||||
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
@ -1043,14 +1037,6 @@ int32_t GPS::runOnce()
|
||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
return disable();
|
||||
}
|
||||
// ONCE we will factory reset the GPS for bug #327
|
||||
if (!devicestate.did_gps_reset) {
|
||||
LOG_WARN("GPS FactoryReset requested");
|
||||
if (gps->factoryReset()) { // If we don't succeed try again next time
|
||||
devicestate.did_gps_reset = true;
|
||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
||||
}
|
||||
}
|
||||
GPSInitFinished = true;
|
||||
publishUpdate();
|
||||
}
|
||||
@ -1063,24 +1049,6 @@ int32_t GPS::runOnce()
|
||||
if (whileActive()) {
|
||||
// if we have received valid NMEA claim we are connected
|
||||
setConnected();
|
||||
} else {
|
||||
if ((config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) &&
|
||||
IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9,
|
||||
GNSS_MODEL_UBLOX10)) {
|
||||
// reset the GPS on next bootup
|
||||
if (devicestate.did_gps_reset && scheduling.elapsedSearchMs() > 60 * 1000UL && !hasFlow()) {
|
||||
LOG_DEBUG("GPS is not found, try factory reset on next boot");
|
||||
devicestate.did_gps_reset = false;
|
||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
||||
return disable(); // Stop the GPS thread as it can do nothing useful until next reboot.
|
||||
}
|
||||
}
|
||||
}
|
||||
// At least one GPS has a bad habit of losing its mind from time to time
|
||||
if (rebootsSeen > 2) {
|
||||
rebootsSeen = 0;
|
||||
LOG_DEBUG("Would normally factoryReset()");
|
||||
// gps->factoryReset();
|
||||
}
|
||||
|
||||
// If we're due for an update, wake the GPS
|
||||
@ -1415,62 +1383,6 @@ static int32_t toDegInt(RawDegrees d)
|
||||
return r;
|
||||
}
|
||||
|
||||
bool GPS::factoryReset()
|
||||
{
|
||||
#ifdef PIN_GPS_REINIT
|
||||
// The L76K GNSS on the T-Echo requires the RESET pin to be pulled LOW
|
||||
pinMode(PIN_GPS_REINIT, OUTPUT);
|
||||
digitalWrite(PIN_GPS_REINIT, 0);
|
||||
delay(150); // The L76K datasheet calls for at least 100MS delay
|
||||
digitalWrite(PIN_GPS_REINIT, 1);
|
||||
#endif
|
||||
|
||||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
||||
byte _message_reset1[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1C, 0xA2};
|
||||
_serial_gps->write(_message_reset1, sizeof(_message_reset1));
|
||||
if (getACK(0x05, 0x01, 10000)) {
|
||||
LOG_DEBUG(ACK_SUCCESS_MESSAGE);
|
||||
}
|
||||
delay(100);
|
||||
byte _message_reset2[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1B, 0xA1};
|
||||
_serial_gps->write(_message_reset2, sizeof(_message_reset2));
|
||||
if (getACK(0x05, 0x01, 10000)) {
|
||||
LOG_DEBUG(ACK_SUCCESS_MESSAGE);
|
||||
}
|
||||
delay(100);
|
||||
byte _message_reset3[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0x1D, 0xB3};
|
||||
_serial_gps->write(_message_reset3, sizeof(_message_reset3));
|
||||
if (getACK(0x05, 0x01, 10000)) {
|
||||
LOG_DEBUG(ACK_SUCCESS_MESSAGE);
|
||||
}
|
||||
} else if (gnssModel == GNSS_MODEL_MTK) {
|
||||
// send the CAS10 to perform a factory restart of the device (and other device that support PCAS statements)
|
||||
LOG_INFO("GNSS Factory Reset via PCAS10,3");
|
||||
_serial_gps->write("$PCAS10,3*1F\r\n");
|
||||
delay(100);
|
||||
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
|
||||
LOG_INFO("Factory Reset via CAS-CFG-RST");
|
||||
uint8_t msglen = makeCASPacket(0x06, 0x02, sizeof(_message_CAS_CFG_RST_FACTORY), _message_CAS_CFG_RST_FACTORY);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
delay(100);
|
||||
} else {
|
||||
// fire this for good measure, if we have an L76B - won't harm other devices.
|
||||
_serial_gps->write("$PMTK104*37\r\n");
|
||||
// No PMTK_ACK for this command.
|
||||
delay(100);
|
||||
// send the UBLOX Factory Reset Command regardless of detect state, something is very wrong, just assume it's
|
||||
// UBLOX. Factory Reset
|
||||
byte _message_reset[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFB, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x17, 0x2B, 0x7E};
|
||||
_serial_gps->write(_message_reset, sizeof(_message_reset));
|
||||
}
|
||||
delay(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any processing that should be done only while the GPS is awake and looking for a fix.
|
||||
* Override this method to check for new locations
|
||||
|
@ -101,8 +101,6 @@ class GPS : private concurrency::OSThread
|
||||
// Empty the input buffer as quickly as possible
|
||||
void clearBuffer();
|
||||
|
||||
virtual bool factoryReset();
|
||||
|
||||
// Creates an instance of the GPS class.
|
||||
// Returns the new instance or null if the GPS is not present.
|
||||
static GPS *createGps();
|
||||
|
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