Merge branch 'master' into nomad-gemini

This commit is contained in:
Thomas Göttgens 2025-03-02 12:01:43 +01:00 committed by GitHub
commit 9c3ceaf6e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
240 changed files with 14401 additions and 2127 deletions

View File

@ -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

View File

@ -1,7 +1,7 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug", "triage"]
labels: [bug, triage]
body:
- type: markdown
attributes:

View File

@ -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:

View File

@ -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
View 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

View File

@ -34,7 +34,7 @@ inputs:
arch:
description: Processor arch name
required: true
default: "esp32"
default: esp32
runs:
using: composite

View File

@ -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}}

View File

@ -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

View File

@ -7,6 +7,8 @@ on:
required: true
type: string
permissions: read-all
jobs:
build-nrf52:
runs-on: ubuntu-latest

View File

@ -7,6 +7,8 @@ on:
required: true
type: string
permissions: read-all
jobs:
build-rpi2040:
runs-on: ubuntu-latest

View File

@ -7,6 +7,8 @@ on:
required: true
type: string
permissions: read-all
jobs:
build-stm32:
runs-on: ubuntu-latest

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -2,6 +2,8 @@
name: Semgrep Differential Scan
on: pull_request
permissions: read-all
jobs:
semgrep-diff:
runs-on: ubuntu-22.04

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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

View 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

View File

@ -8,3 +8,4 @@ line_length: false
spaces: false
url: false
whitespace: false
headings: false

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -18,6 +18,7 @@ build_src_filter =
lib_ignore =
BluetoothOTA
lvgl
lib_deps =
${arduino_base.lib_deps}

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
Display:
Panel: X11
Width: 480
Height: 480

View 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

View 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

View File

@ -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
View File

@ -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
View File

@ -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

@ -0,0 +1 @@
Subproject commit 5c6156d2aa10d62cca3e57ffc117b934ef2fbffe

View File

@ -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");

View File

@ -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
View 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;

View File

@ -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(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
#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

View File

@ -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;

View File

@ -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;
}

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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(&notifyDeepSleep);
}
// 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

View 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

View File

@ -0,0 +1 @@
#include "./DEPG0154BNS800.h"

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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.

View 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

View 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

View File

@ -0,0 +1,3 @@
# NicheGraphics - Drivers
Common drivers which can be used by various NicheGrapihcs UIs

View 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

View 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

View 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};

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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