mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-14 14:52:32 +00:00
Compare commits
77 Commits
meshtastic
...
multicast-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8e6b39bc9 | ||
|
|
bf7afd657a | ||
|
|
ca951caa38 | ||
|
|
16a1c9f148 | ||
|
|
af8b64e84e | ||
|
|
96ba94843b | ||
|
|
2d565c2921 | ||
|
|
2525111c39 | ||
|
|
64b9cfe199 | ||
|
|
dc100e4d3e | ||
|
|
1640fb105d | ||
|
|
99e42b4d22 | ||
|
|
79233fe99d | ||
|
|
f66784ed2a | ||
|
|
f198d5d49f | ||
|
|
4d34b3d73c | ||
|
|
8efe8a2ea3 | ||
|
|
499ea56e3b | ||
|
|
2473af6995 | ||
|
|
508ab171d6 | ||
|
|
ec59f7d7dd | ||
|
|
f4c79530ec | ||
|
|
e9effb9fff | ||
|
|
cb6dfb66d2 | ||
|
|
8795a63427 | ||
|
|
186e509607 | ||
|
|
7c3eddebc2 | ||
|
|
78b4eff568 | ||
|
|
3c1f92ce84 | ||
|
|
5de6bc1851 | ||
|
|
c54fc5b7c5 | ||
|
|
94de2315c1 | ||
|
|
7f17747d8c | ||
|
|
16a0dce83c | ||
|
|
3fd47d9713 | ||
|
|
284598ed56 | ||
|
|
2a3e1f904d | ||
|
|
60e46cd765 | ||
|
|
563747c5cd | ||
|
|
5c77d42345 | ||
|
|
f0a2ae9ff3 | ||
|
|
f7afa9a81e | ||
|
|
c8bd6c32cc | ||
|
|
f6a9e7d741 | ||
|
|
e6a98b1d6b | ||
|
|
b2ef92a328 | ||
|
|
b25db1f42c | ||
|
|
a924b9d94a | ||
|
|
f5e0e282b6 | ||
|
|
a3a9b2fe84 | ||
|
|
6c8058e1d8 | ||
|
|
445efe9e21 | ||
|
|
b96b027926 | ||
|
|
239e5412b3 | ||
|
|
ede3f7b702 | ||
|
|
f0f2cd0e0e | ||
|
|
fdbadc992c | ||
|
|
2391982c1d | ||
|
|
41875d245e | ||
|
|
95bcd7ab0b | ||
|
|
050f0016c4 | ||
|
|
6715662281 | ||
|
|
b6562e175f | ||
|
|
f89f916f96 | ||
|
|
43a6e711da | ||
|
|
63b20e358f | ||
|
|
12fde696c1 | ||
|
|
5c8f1fb46b | ||
|
|
ce38ac10d1 | ||
|
|
d5ec205572 | ||
|
|
9893d24c62 | ||
|
|
ab61cd65d1 | ||
|
|
baef8dce79 | ||
|
|
99d3e5eb70 | ||
|
|
088fce7d11 | ||
|
|
b46bf16385 | ||
|
|
1c827f5512 |
@@ -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,15 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
hwdata \
|
||||
gpg \
|
||||
gnupg2 \
|
||||
libusb-1.0-0-dev \
|
||||
libuv1-dev \
|
||||
libi2c-dev \
|
||||
libxcb-xkb-dev \
|
||||
libxkbcommon-dev \
|
||||
libinput-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
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
git submodule update --init
|
||||
|
||||
pip install --no-cache-dir setuptools
|
||||
pipx install esptool
|
||||
|
||||
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,4 +1,5 @@
|
||||
* text=auto eol=lf
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
*.{sh,[sS][hH]} text eol=lf
|
||||
|
||||
2
.github/actions/setup-base/action.yml
vendored
2
.github/actions/setup-base/action.yml
vendored
@@ -20,7 +20,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update --fix-missing
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev libuv1-dev lsb-release
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
2
.github/actions/setup-native/action.yml
vendored
2
.github/actions/setup-native/action.yml
vendored
@@ -11,4 +11,4 @@ runs:
|
||||
- name: Install libs needed for native build
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -19,6 +19,8 @@ updates:
|
||||
interval: daily
|
||||
time: "05:00"
|
||||
timezone: US/Pacific
|
||||
ignore:
|
||||
- dependency-name: protobufs
|
||||
- package-ecosystem: github-actions
|
||||
directory: /.github/workflows
|
||||
schedule:
|
||||
|
||||
2
.github/workflows/build_debian_src.yml
vendored
2
.github/workflows/build_debian_src.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
PPA_GPG_PRIVATE_KEY:
|
||||
required: true
|
||||
required: false
|
||||
inputs:
|
||||
series:
|
||||
description: Ubuntu/Debian series to target
|
||||
|
||||
39
.github/workflows/main_matrix.yml
vendored
39
.github/workflows/main_matrix.yml
vendored
@@ -135,10 +135,11 @@ jobs:
|
||||
build_location: local
|
||||
secrets: inherit
|
||||
|
||||
package-pio-deps-native:
|
||||
package-pio-deps-native-tft:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
with:
|
||||
pio_env: native
|
||||
pio_env: native-tft
|
||||
secrets: inherit
|
||||
|
||||
test-native:
|
||||
@@ -288,7 +289,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,18 +325,18 @@ 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
|
||||
path: ./output/pio-deps-native-tft
|
||||
|
||||
- name: Zip linux sources
|
||||
working-directory: output
|
||||
run: |
|
||||
zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src
|
||||
zip -9 -r ./platformio-deps-native-${{ steps.version.outputs.long }}.zip ./pio-deps-native
|
||||
zip -9 -r ./platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip ./pio-deps-native-tft
|
||||
|
||||
# For diagnostics
|
||||
- name: Display structure of downloaded files
|
||||
@@ -344,32 +345,10 @@ jobs:
|
||||
- name: Add linux sources to release
|
||||
run: |
|
||||
gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip
|
||||
gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-${{ steps.version.outputs.long }}.zip
|
||||
gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Bump version.properties
|
||||
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
|
||||
|
||||
- name: Create version.properties pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
title: Bump version.properties
|
||||
add-paths: |
|
||||
version.properties
|
||||
debian/changelog
|
||||
|
||||
release-firmware:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
46
.github/workflows/release_channels.yml
vendored
46
.github/workflows/release_channels.yml
vendored
@@ -43,3 +43,49 @@ jobs:
|
||||
copr_project: |-
|
||||
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
|
||||
secrets: inherit
|
||||
|
||||
# Create a PR to bump version when a release is Published
|
||||
bump-version:
|
||||
if: ${{ github.event.release.published }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Get release version string
|
||||
run: |
|
||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
env:
|
||||
BUILD_LOCATION: local
|
||||
|
||||
- name: Bump version.properties
|
||||
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
|
||||
|
||||
- name: Create version.properties pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
title: Bump version.properties
|
||||
add-paths: |
|
||||
version.properties
|
||||
debian/changelog
|
||||
|
||||
7
.github/workflows/sec_sast_semgrep_cron.yml
vendored
7
.github/workflows/sec_sast_semgrep_cron.yml
vendored
@@ -6,11 +6,14 @@ on:
|
||||
schedule:
|
||||
- cron: 0 1 * * 6
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
semgrep-full:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
|
||||
|
||||
1
.github/workflows/stale_bot.yml
vendored
1
.github/workflows/stale_bot.yml
vendored
@@ -18,5 +18,6 @@ jobs:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
days-before-stale: 45
|
||||
exempt-issue-labels: pinned,3.0
|
||||
exempt-pr-labels: pinned,3.0
|
||||
|
||||
2
.github/workflows/test_native.yml
vendored
2
.github/workflows/test_native.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Test Report
|
||||
uses: dorny/test-reporter@v1.9.1
|
||||
uses: dorny/test-reporter@v2.0.0
|
||||
with:
|
||||
name: PlatformIO Tests
|
||||
path: testreport.xml
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: 0.1
|
||||
cli:
|
||||
version: 1.22.10
|
||||
version: 1.22.11
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
@@ -8,16 +8,16 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- prettier@3.5.2
|
||||
- trufflehog@3.88.12
|
||||
- yamllint@1.35.1
|
||||
- prettier@3.5.3
|
||||
- trufflehog@3.88.17
|
||||
- yamllint@1.36.0
|
||||
- bandit@1.8.3
|
||||
- checkov@3.2.373
|
||||
- checkov@3.2.386
|
||||
- terrascan@1.19.9
|
||||
- trivy@0.59.1
|
||||
- trivy@0.60.0
|
||||
- taplo@0.9.3
|
||||
- ruff@0.9.7
|
||||
- isort@6.0.0
|
||||
- ruff@0.10.0
|
||||
- isort@6.0.1
|
||||
- markdownlint@0.44.0
|
||||
- oxipng@9.1.4
|
||||
- svgo@3.3.2
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -7,5 +7,8 @@
|
||||
"cmake.configureOnOpen": false,
|
||||
"[cpp]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[powershell]": {
|
||||
"editor.defaultFormatter": "ms-vscode.powershell"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ ENV TZ=Etc/UTC
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
wget g++ zip git ca-certificates \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-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 \
|
||||
@@ -38,7 +38,7 @@ ENV TZ=Etc/UTC
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 \
|
||||
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev liborcania2.3 libulfius2.7 libssl3 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
|
||||
@@ -9,7 +9,7 @@ FROM python:3.13-alpine3.21 AS builder
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
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 \
|
||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
@@ -32,7 +32,7 @@ FROM alpine:3.21
|
||||
USER root
|
||||
|
||||
RUN apk --no-cache add \
|
||||
libstdc++ libgpiod yaml-cpp libusb i2c-tools \
|
||||
libstdc++ libgpiod yaml-cpp libusb i2c-tools libuv \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
|
||||
@@ -37,6 +37,7 @@ build_flags =
|
||||
-DLIBPAX_ARDUINO
|
||||
-DLIBPAX_WIFI
|
||||
-DLIBPAX_BLE
|
||||
-DHAS_UDP_MULTICAST=1
|
||||
;-DDEBUG_HEAP
|
||||
|
||||
lib_deps =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
; The Portduino based 'native' environment. Currently supported on Linux targets with real LoRa hardware (or simulated).
|
||||
[portduino_base]
|
||||
platform = https://github.com/meshtastic/platform-native.git#562d189828f09fbf4c4093b3c0104bae9d8e9ff9
|
||||
platform = https://github.com/Jorropo/platform-native.git#17fa89daec4402af491512f75278a7fec8a5818c
|
||||
framework = arduino
|
||||
|
||||
build_src_filter =
|
||||
@@ -34,10 +34,12 @@ build_flags =
|
||||
-Isrc/platform/portduino
|
||||
-DRADIOLIB_EEPROM_UNSUPPORTED
|
||||
-DPORTDUINO_LINUX_HARDWARE
|
||||
-DHAS_UDP_MULTICAST
|
||||
-lpthread
|
||||
-lstdc++fs
|
||||
-lbluetooth
|
||||
-lgpiod
|
||||
-lyaml-cpp
|
||||
-li2c
|
||||
-luv
|
||||
-std=c++17
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
; Common settings for rp2040 Processor based targets
|
||||
[rp2040_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
|
||||
@@ -18,6 +18,7 @@ build_src_filter =
|
||||
|
||||
lib_ignore =
|
||||
BluetoothOTA
|
||||
lvgl
|
||||
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
|
||||
@@ -35,11 +35,11 @@ cp $SRCBIN $OUTDIR/$basename-update.bin
|
||||
|
||||
echo "Building Filesystem for ESP32 targets"
|
||||
pio run --environment $1 -t buildfs
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$VERSION.bin
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
||||
# Remove webserver files from the filesystem and rebuild
|
||||
ls -l data/static # Diagnostic list of files
|
||||
rm -rf data/static
|
||||
pio run --environment $1 -t buildfs
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$VERSION.bin
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||
cp bin/device-install.* $OUTDIR
|
||||
cp bin/device-update.* $OUTDIR
|
||||
cp bin/device-update.* $OUTDIR
|
||||
@@ -24,7 +24,7 @@ mkdir -p $OUTDIR/
|
||||
rm -r $OUTDIR/* || true
|
||||
|
||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||
platformio pkg update --environment native || platformioFailed
|
||||
pio pkg update --environment native || platformioFailed
|
||||
pio run --environment native || platformioFailed
|
||||
cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||
cp bin/native-install.* $OUTDIR
|
||||
|
||||
4
bin/config.d/MUI/X11_480x480.yaml
Normal file
4
bin/config.d/MUI/X11_480x480.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
Display:
|
||||
Panel: X11
|
||||
Width: 480
|
||||
Height: 480
|
||||
49
bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml
Normal file
49
bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
Lora:
|
||||
|
||||
### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx
|
||||
### https://github.com/markbirss/rock-2f
|
||||
### https://github.com/markbirss/lora-starter-edition-sx1262-i2c
|
||||
### https://github.com/radxa-pkg/radxa-overlays/blob/main/arch/arm64/boot/dts/rockchip/overlays/rk3528-spi0-cs1-spidev.dts
|
||||
### Require install of https://github.com/radxa-pkg/radxa-overlays and rk3528-spi0-cs1-spidev.dtbo copied to /boot/dtb/rockchip/overlay and enabled
|
||||
### in /boot/armbianEnv.txt - overlays=rk3528-spi0-cs1-spidev
|
||||
### The Radxa Rock 2F employs multiple gpio chips.
|
||||
### Each gpio pin must be unique, but can be assigned to a specific gpio chip and line.
|
||||
### In case solely a no. is given, the default gpio chip and pin == line will be employed.
|
||||
###
|
||||
Module: sx1262 # Radxa Rock 2F + Starter Edition SX1262 HAT by Mark Birss
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
spidev: spidev0.1
|
||||
CS: # NSS PIN_24 -> chip 4, line 14
|
||||
pin: 24
|
||||
gpiochip: 4
|
||||
line: 14
|
||||
SCK: # SCK PIN_23 -> chip 4, line 12
|
||||
pin: 23
|
||||
gpiochip: 4
|
||||
line: 12
|
||||
Busy: # BUSY PIN_7 -> chip 4, line 6
|
||||
pin: 7
|
||||
gpiochip: 4
|
||||
line: 6
|
||||
MOSI: # MOSI PIN_19 -> chip 4, line 10
|
||||
pin: 19
|
||||
gpiochip: 4
|
||||
line: 10
|
||||
MISO: # MISO PIN_21 -> chip 4, line 11
|
||||
pin: 21
|
||||
gpiochip: 4
|
||||
line: 11
|
||||
Reset: # NRST PIN_12 -> chip 1, line 13
|
||||
pin: 12
|
||||
gpiochip: 1
|
||||
line: 13
|
||||
IRQ: # DIO1 PIN_15 -> chip 4, line 22
|
||||
pin: 15
|
||||
gpiochip: 4
|
||||
line: 22
|
||||
# RXen: # RXEN PIN_22 -> chip 3!, line 17
|
||||
# pin: 22
|
||||
# gpiochip: 3
|
||||
# line: 17
|
||||
# TXen: RADIOLIB_NC # TXEN no PIN, no line, fallback to default gpio chip
|
||||
10
bin/config.d/lora-starter-edition-sx1262-i2c.yaml
Normal file
10
bin/config.d/lora-starter-edition-sx1262-i2c.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# https://www.waveshare.com/core1262-868m.htm
|
||||
# https://github.com/markbirss/lora-starter-edition-sx1262-i2c
|
||||
Lora:
|
||||
Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 8
|
||||
IRQ: 22
|
||||
Busy: 4
|
||||
Reset: 18
|
||||
10
bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml
Normal file
10
bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
|
||||
# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter
|
||||
Lora:
|
||||
Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 21
|
||||
IRQ: 16
|
||||
Busy: 20
|
||||
Reset: 18
|
||||
@@ -1,72 +1,296 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
TITLE Meshtastic device-install
|
||||
|
||||
set PYTHON=python
|
||||
set WEB_APP=0
|
||||
SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "PYTHON="
|
||||
SET "WEB_APP=0"
|
||||
SET "TFT_BUILD=0"
|
||||
SET "TFT8=0"
|
||||
SET "TFT16=0"
|
||||
SET "ESPTOOL_BAUD=115200"
|
||||
SET "ESPTOOL_CMD="
|
||||
SET "LOGCOUNTER=0"
|
||||
|
||||
:: Determine the correct esptool command to use
|
||||
where esptool >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
set "ESPTOOL_CMD=esptool"
|
||||
) else (
|
||||
set "ESPTOOL_CMD=%PYTHON% -m esptool"
|
||||
)
|
||||
GOTO getopts
|
||||
:help
|
||||
ECHO Flash image file to device, but first erasing and writing system information.
|
||||
ECHO.
|
||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web)
|
||||
ECHO.
|
||||
ECHO Options:
|
||||
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
|
||||
ECHO The file must be located in this current directory.
|
||||
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
|
||||
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
||||
ECHO If supplied the script will use python.
|
||||
ECHO If not supplied the script will try to find esptool in Path.
|
||||
ECHO --web Enable WebUI. (default: false)
|
||||
ECHO.
|
||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
|
||||
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web
|
||||
GOTO eof
|
||||
|
||||
goto GETOPTS
|
||||
:HELP
|
||||
echo Usage: %~nx0 [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME^|FILENAME] [--web]
|
||||
echo Flash image file to device, but first erasing and writing system information
|
||||
echo.
|
||||
echo -h Display this help and exit
|
||||
echo -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerrous).
|
||||
echo -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: %PYTHON%)
|
||||
echo -f FILENAME The .bin file to flash. Custom to your device type and region.
|
||||
echo --web Flash WEB APP.
|
||||
goto EOF
|
||||
:version
|
||||
ECHO %SCRIPT_NAME% [Version 2.6.0]
|
||||
ECHO Meshtastic
|
||||
GOTO eof
|
||||
|
||||
:GETOPTS
|
||||
if /I "%1"=="-h" goto HELP
|
||||
if /I "%1"=="--help" goto HELP
|
||||
if /I "%1"=="-F" set "FILENAME=%2" & SHIFT
|
||||
if /I "%1"=="-p" set ESPTOOL_PORT=%2 & SHIFT
|
||||
if /I "%1"=="-P" set PYTHON=%2 & SHIFT
|
||||
if /I "%1"=="--web" set WEB_APP=1 & SHIFT
|
||||
:getopts
|
||||
IF "%~1"=="" GOTO endopts
|
||||
IF /I "%~1"=="-?" GOTO help
|
||||
IF /I "%~1"=="-h" GOTO help
|
||||
IF /I "%~1"=="--help" GOTO help
|
||||
IF /I "%~1"=="-v" GOTO version
|
||||
IF /I "%~1"=="--version" GOTO version
|
||||
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
|
||||
IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
|
||||
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||
IF /I "%~1"=="--web" SET "WEB_APP=1"
|
||||
SHIFT
|
||||
IF NOT "__%1__"=="____" goto GETOPTS
|
||||
GOTO getopts
|
||||
:endopts
|
||||
|
||||
IF "__%FILENAME%__" == "____" (
|
||||
echo "Missing FILENAME"
|
||||
goto HELP
|
||||
)
|
||||
IF EXIST %FILENAME% IF x%FILENAME:update=%==x%FILENAME% (
|
||||
echo Trying to flash update %FILENAME%, but first erasing and writing system information"
|
||||
%ESPTOOL_CMD% --baud 115200 erase_flash
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x00 %FILENAME%
|
||||
|
||||
@REM Account for S3 and C3 board's different OTA partition
|
||||
IF x%FILENAME:s3=%==x%FILENAME% IF x%FILENAME:v3=%==x%FILENAME% IF x%FILENAME:t-deck=%==x%FILENAME% IF x%FILENAME:wireless-paper=%==x%FILENAME% IF x%FILENAME:wireless-tracker=%==x%FILENAME% IF x%FILENAME:station-g2=%==x%FILENAME% IF x%FILENAME:unphone=%==x%FILENAME% (
|
||||
IF x%FILENAME:esp32c3=%==x%FILENAME% (
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x260000 bleota.bin
|
||||
) else (
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x260000 bleota-c3.bin
|
||||
)
|
||||
) else (
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x260000 bleota-s3.bin
|
||||
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
||||
IF "__!FILENAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
||||
GOTO help
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!"
|
||||
IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
|
||||
GOTO help
|
||||
)
|
||||
IF %WEB_APP%==1 (
|
||||
for %%f in (littlefswebui-*.bin) do (
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x300000 %%f
|
||||
)
|
||||
) else (
|
||||
for %%f in (littlefs-*.bin) do (
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x300000 %%f
|
||||
)
|
||||
IF "__!FILENAME:firmware-=!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename must be a firmware-* file."
|
||||
GOTO help
|
||||
)
|
||||
) else (
|
||||
echo "Invalid file: %FILENAME%"
|
||||
goto HELP
|
||||
) else (
|
||||
echo "Invalid file: %FILENAME%"
|
||||
goto HELP
|
||||
@REM Remove ".\" or "./" file prefix if present.
|
||||
SET "FILENAME=!FILENAME:.\=!"
|
||||
SET "FILENAME=!FILENAME:./=!"
|
||||
)
|
||||
|
||||
:EOF
|
||||
CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..."
|
||||
IF NOT EXIST !FILENAME! (
|
||||
CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating."
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
IF NOT "!FILENAME:update=!"=="!FILENAME!" (
|
||||
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
|
||||
CALL :LOG_MESSAGE INFO "Use script device-update.bat to flash update !FILENAME!."
|
||||
GOTO eof
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
|
||||
WHERE esptool >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 0 (
|
||||
@REM WHERE exits with code 0 if esptool is found.
|
||||
SET "ESPTOOL_CMD=esptool"
|
||||
) ELSE (
|
||||
SET "ESPTOOL_CMD=python -m esptool"
|
||||
CALL :RESET_ERROR
|
||||
)
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
IF %ERRORLEVEL% GTR 2 (
|
||||
@REM esptool exits with code 1 if help is displayed.
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
GOTO eof
|
||||
)
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!"
|
||||
IF "__!ESPTOOL_PORT!__" == "____" (
|
||||
CALL :LOG_MESSAGE WARN "Using esptool port: UNSET."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!."
|
||||
)
|
||||
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
|
||||
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
|
||||
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
|
||||
CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
|
||||
IF %WEB_APP% EQU 1 (
|
||||
CALL :LOG_MESSAGE ERROR "Cannot enable WebUI (--web) and MUI." & GOTO eof
|
||||
)
|
||||
SET "TFT_BUILD=1"
|
||||
GOTO tft
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!"
|
||||
GOTO no_tft
|
||||
)
|
||||
|
||||
:tft
|
||||
SET "TFT8MB=picomputer-s3 unphone seeed-sensecap-indicator"
|
||||
FOR %%a IN (%TFT8MB%) DO (
|
||||
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
|
||||
@REM We are working with any of %TFT8MB%.
|
||||
SET "TFT8=1"
|
||||
GOTO end_loop_tft8mb
|
||||
)
|
||||
)
|
||||
:end_loop_tft8mb
|
||||
|
||||
SET "TFT16MB=t-deck"
|
||||
FOR %%a IN (%TFT16MB%) DO (
|
||||
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
|
||||
@REM We are working with any of %TFT16MB%.
|
||||
SET "TFT16=1"
|
||||
GOTO end_loop_tft16mb
|
||||
)
|
||||
)
|
||||
:end_loop_tft16mb
|
||||
|
||||
IF %TFT8% EQU 1 CALL :LOG_MESSAGE INFO "tft and MUI 8mb selected."
|
||||
IF %TFT16% EQU 1 CALL :LOG_MESSAGE INFO "tft and MUI 16mb selected."
|
||||
|
||||
:no_tft
|
||||
|
||||
@REM Extract BASENAME from %FILENAME% for later use.
|
||||
SET "BASENAME=!FILENAME:firmware-=!"
|
||||
CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!"
|
||||
|
||||
@REM Account for S3 and C3 board's different OTA partition.
|
||||
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
|
||||
FOR %%a IN (%S3%) DO (
|
||||
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
|
||||
@REM We are working with any of %S3%.
|
||||
SET "OTA_FILENAME=bleota-s3.bin"
|
||||
GOTO :end_loop_s3
|
||||
)
|
||||
)
|
||||
|
||||
SET "C3=esp32c3"
|
||||
FOR %%a IN (%C3%) DO (
|
||||
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
|
||||
@REM We are working with any of %C3%.
|
||||
SET "OTA_FILENAME=bleota-c3.bin"
|
||||
GOTO :end_loop_c3
|
||||
)
|
||||
)
|
||||
|
||||
@REM Everything else
|
||||
SET "OTA_FILENAME=bleota.bin"
|
||||
:end_loop_s3
|
||||
:end_loop_c3
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||
|
||||
@REM Check if (--web) is enabled and prefix BASENAME with "littlefswebui-" else "littlefs-".
|
||||
IF %WEB_APP% EQU 1 (
|
||||
CALL :LOG_MESSAGE INFO "WebUI selected."
|
||||
SET "SPIFFS_FILENAME=littlefswebui-%BASENAME%"
|
||||
) ELSE (
|
||||
SET "SPIFFS_FILENAME=littlefs-%BASENAME%"
|
||||
)
|
||||
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
|
||||
|
||||
@REM Default offsets.
|
||||
@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
|
||||
SET "OTA_OFFSET=0x260000"
|
||||
SET "SPIFFS_OFFSET=0x300000"
|
||||
|
||||
@REM Offsets for MUI 8mb.
|
||||
IF %TFT8% EQU 1 IF %TFT_BUILD% EQU 1 (
|
||||
SET "OTA_OFFSET=0x340000"
|
||||
SET "SPIFFS_OFFSET=0x670000"
|
||||
)
|
||||
|
||||
@REM Offsets for MUI 16mb.
|
||||
IF %TFT16% EQU 1 IF %TFT_BUILD% EQU 1 (
|
||||
SET "OTA_OFFSET=0x650000"
|
||||
SET "SPIFFS_OFFSET=0xc90000"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!"
|
||||
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!"
|
||||
|
||||
@REM Ensure target files exist before flashing operations.
|
||||
IF NOT EXIST !FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
|
||||
IF NOT EXIST !OTA_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!OTA_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
|
||||
IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!SPIFFS_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
|
||||
:RUN_ESPTOOL
|
||||
@REM Subroutine used to run ESPTOOL_CMD with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
EXIT /B %ERRORLEVEL%
|
||||
)
|
||||
GOTO :eof
|
||||
|
||||
:LOG_MESSAGE
|
||||
@REM Subroutine used to print log messages in four different levels.
|
||||
@REM DEBUG messages only get printed if [-d] flag is passed to script.
|
||||
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
|
||||
@REM.
|
||||
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
|
||||
SET /A LOGCOUNTER=LOGCOUNTER+1
|
||||
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO [91m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[91m%~2[0m
|
||||
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO [32m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[32m%~2[0m
|
||||
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO [33m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[33m%~2[0m
|
||||
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO [34m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[34m%~2[0m
|
||||
GOTO :eof
|
||||
|
||||
:GET_TIMESTAMP
|
||||
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
|
||||
@REM CALL :GET_TIMESTAMP
|
||||
@REM.
|
||||
@REM Updates: !TIMESTAMP!
|
||||
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
|
||||
SET "HH=%%a"
|
||||
SET "MM=%%b"
|
||||
SET "ss=%%c"
|
||||
)
|
||||
SET "TIMESTAMP=!HH!:!MM!:!ss!"
|
||||
GOTO :eof
|
||||
|
||||
:RESET_ERROR
|
||||
@REM Subroutine to reset %ERRORLEVEL% to 0.
|
||||
@REM CALL :RESET_ERROR
|
||||
@REM.
|
||||
@REM Updates: %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
GOTO :eof
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
||||
WEB_APP=false
|
||||
TFT8=false
|
||||
TFT16=false
|
||||
TFT_BUILD=false
|
||||
|
||||
# Determine the correct esptool command to use
|
||||
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="$PYTHON -m esptool"
|
||||
ESPTOOL_CMD="$PYTHON -m esptool"
|
||||
elif command -v esptool >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="esptool"
|
||||
ESPTOOL_CMD="esptool"
|
||||
elif command -v esptool.py >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="esptool.py"
|
||||
ESPTOOL_CMD="esptool.py"
|
||||
else
|
||||
echo "Error: esptool not found"
|
||||
exit 1
|
||||
echo "Error: esptool not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
@@ -20,76 +23,139 @@ set -e
|
||||
# Usage info
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--web]
|
||||
Flash image file to device, but first erasing and writing system information"
|
||||
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--web]
|
||||
Flash image file to device, but first erasing and writing system information.
|
||||
|
||||
-h Display this help and exit
|
||||
-h Display this help and exit.
|
||||
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
||||
-f FILENAME The .bin file to flash. Custom to your device type and region.
|
||||
--web Flash WEB APP.
|
||||
-f FILENAME The firmware .bin file to flash. Custom to your device type and region.
|
||||
--web Enable WebUI. (Default: false)
|
||||
|
||||
EOF
|
||||
}
|
||||
# Preprocess long options like --web
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--web)
|
||||
WEB_APP=true
|
||||
shift # Remove this argument from the list
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
while getopts ":hp:P:f:" opt; do
|
||||
case "${opt}" in
|
||||
h)
|
||||
# Parse arguments using a single while loop
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
p)
|
||||
export ESPTOOL_PORT=${OPTARG}
|
||||
-p)
|
||||
ESPTOOL_PORT="$2"
|
||||
shift # Shift past the option argument
|
||||
;;
|
||||
P)
|
||||
PYTHON=${OPTARG}
|
||||
-P)
|
||||
PYTHON="$2"
|
||||
shift
|
||||
;;
|
||||
f)
|
||||
FILENAME=${OPTARG}
|
||||
-f)
|
||||
FILENAME="$2"
|
||||
shift
|
||||
;;
|
||||
--web)
|
||||
WEB_APP=true
|
||||
;;
|
||||
--) # Stop parsing options
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid flag."
|
||||
show_help >&2
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift # Move to the next argument
|
||||
done
|
||||
shift "$((OPTIND - 1))"
|
||||
|
||||
[ -z "$FILENAME" -a -n "$1" ] && {
|
||||
FILENAME=$1
|
||||
shift
|
||||
}
|
||||
|
||||
if [[ $FILENAME != firmware-* ]]; then
|
||||
echo "Filename must be a firmware-* file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
|
||||
if [[ ${FILENAME//-tft-/} != "$FILENAME" ]]; then
|
||||
TFT_BUILD=true
|
||||
if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then
|
||||
echo "Cannot enable WebUI (--web) and MUI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $FILENAME == *"picomputer-s3"* || $FILENAME == *"unphone"* || $FILENAME == *"seeed-sensecap-indicator"* ]]; then
|
||||
TFT8=true
|
||||
fi
|
||||
|
||||
if [[ $FILENAME == *"t-deck"* ]]; then
|
||||
TFT16=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract BASENAME from %FILENAME% for later use.
|
||||
BASENAME="${FILENAME/firmware-/}"
|
||||
|
||||
if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||
$ESPTOOL_CMD erase_flash
|
||||
$ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
|
||||
# Default littlefs* offset (--web).
|
||||
OFFSET=0x300000
|
||||
|
||||
# Default OTA Offset
|
||||
OTA_OFFSET=0x260000
|
||||
|
||||
# littlefs* offset for MUI 8mb and OTA OFFSET.
|
||||
if [ "$TFT8" = true ] && [ "$TFT_BUILD" = true ]; then
|
||||
OFFSET=0x670000
|
||||
OTA_OFFSET=0x340000
|
||||
fi
|
||||
|
||||
# littlefs* offset for MUI 16mb and OTA OFFSET.
|
||||
if [ "$TFT16" = true ] && [ "$TFT_BUILD" = true ]; then
|
||||
OFFSET=0xc90000
|
||||
OTA_OFFSET=0x650000
|
||||
fi
|
||||
|
||||
# Account for S3 board's different OTA partition
|
||||
if [ -n "${FILENAME##*"s3"*}" ] && [ -n "${FILENAME##*"-v3"*}" ] && [ -n "${FILENAME##*"t-deck"*}" ] && [ -n "${FILENAME##*"wireless-paper"*}" ] && [ -n "${FILENAME##*"wireless-tracker"*}" ] && [ -n "${FILENAME##*"station-g2"*}" ] && [ -n "${FILENAME##*"unphone"*}" ]; then
|
||||
if [ -n "${FILENAME##*"esp32c3"*}" ]; then
|
||||
$ESPTOOL_CMD write_flash 0x260000 bleota.bin
|
||||
OTAFILE=bleota.bin
|
||||
else
|
||||
$ESPTOOL_CMD write_flash 0x260000 bleota-c3.bin
|
||||
OTAFILE=bleota-c3.bin
|
||||
fi
|
||||
else
|
||||
$ESPTOOL_CMD write_flash 0x260000 bleota-s3.bin
|
||||
OTAFILE=bleota-s3.bin
|
||||
fi
|
||||
|
||||
# Check if WEB_APP (--web) is enabled and add "littlefswebui-" to BASENAME else "littlefs-".
|
||||
if [ "$WEB_APP" = true ]; then
|
||||
$ESPTOOL_CMD write_flash 0x300000 littlefswebui-*.bin
|
||||
SPIFFSFILE=littlefswebui-${BASENAME}
|
||||
else
|
||||
$ESPTOOL_CMD write_flash 0x300000 littlefs-*.bin
|
||||
SPIFFSFILE=littlefs-${BASENAME}
|
||||
fi
|
||||
|
||||
if [[ ! -f $FILENAME ]]; then
|
||||
echo "Error: file ${FILENAME} wasn't found. Terminating."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f $OTAFILE ]]; then
|
||||
echo "Error: file ${OTAFILE} wasn't found. Terminating."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f $SPIFFSFILE ]]; then
|
||||
echo "Error: file ${SPIFFSFILE} wasn't found. Terminating."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||
$ESPTOOL_CMD erase_flash
|
||||
$ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
|
||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
|
||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||
$ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
|
||||
|
||||
else
|
||||
show_help
|
||||
echo "Invalid file: ${FILENAME}"
|
||||
|
||||
112
bin/device-install_test.ps1
Normal file
112
bin/device-install_test.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Unit-test for .\device-install.bat.
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs a positive unit-test on .\device-install.bat by creating the expected .bin
|
||||
files for a device followed by running the .bat script without flashing the firmware (--debug).
|
||||
If any errors are hit they are presented in the standard output. Investigate accordingly.
|
||||
|
||||
This script needs to be placed in the same directory as .\device-install.bat.
|
||||
|
||||
.EXAMPLE
|
||||
.\device-install_test.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\device-install_test.ps1 -Verbose
|
||||
|
||||
.LINK
|
||||
.\device-install.bat --help
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
function New-EmptyFile() {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Position = 0, Mandatory = $true)]
|
||||
# Specifies the file name.
|
||||
[string]$FileName,
|
||||
[Parameter(Position = 1)]
|
||||
# Specifies the target path. (Get-Location).Path is the default.
|
||||
[string]$Directory = (Get-Location).Path
|
||||
)
|
||||
|
||||
$filePath = Join-Path -Path $Directory -ChildPath $FileName
|
||||
|
||||
Write-Verbose -Message "Create empty test file if it doesn't exist: $($FileName)"
|
||||
New-Item -Path "$filePath" -ItemType File -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
|
||||
function Remove-EmptyFile() {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Position = 0, Mandatory = $true)]
|
||||
# Specifies the file name.
|
||||
[string]$FileName,
|
||||
[Parameter(Position = 1)]
|
||||
# Specifies the target path. (Get-Location).Path is the default.
|
||||
[string]$Directory = (Get-Location).Path
|
||||
)
|
||||
|
||||
$filePath = Join-Path -Path $Directory -ChildPath $FileName
|
||||
|
||||
Write-Verbose -Message "Deleted empty test file: $($FileName)"
|
||||
Remove-Item -Path "$filePath" | Out-Null
|
||||
}
|
||||
|
||||
|
||||
$TestCases = New-Object -TypeName PSObject -Property @{
|
||||
# Use this PSObject to define testcases according to this syntax:
|
||||
# "testname" = @("firmware-testname","bleota","littlefs-testname","args")
|
||||
"t-deck" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-2.6.0.0b106d4.bin", "")
|
||||
"t-deck_web" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-t-deck-2.6.0.0b106d4.bin", "--web")
|
||||
"t-deck-tft" = @("firmware-t-deck-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-tft-2.6.0.0b106d4.bin", "")
|
||||
"heltec-ht62-esp32c3" = @("firmware-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "bleota-c3.bin", "littlefs-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "")
|
||||
"tlora-c6" = @("firmware-tlora-c6-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-tlora-c6-2.6.0.0b106d4.bin", "")
|
||||
"heltec-v3_web" = @("firmware-heltec-v3-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-heltec-v3-2.6.0.0b106d4.bin", "--web")
|
||||
"seeed-sensecap-indicator-tft" = @("firmware-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "")
|
||||
"picomputer-s3-tft" = @("firmware-picomputer-s3-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-picomputer-s3-tft-2.6.0.0b106d4.bin", "")
|
||||
}
|
||||
|
||||
foreach ($TestCase in $TestCases.PSObject.Properties) {
|
||||
$Name = $TestCase.Name
|
||||
$Files = $TestCase.Value
|
||||
$Errors = $null
|
||||
$Counter = 0
|
||||
|
||||
Write-Host -Object "Testcase: $Name`:" -ForegroundColor Green
|
||||
foreach ($File in $Files) {
|
||||
if ($File.EndsWith(".bin")) {
|
||||
New-EmptyFile -FileName $File
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host -Object "Performing test on $Name..." -ForegroundColor Blue
|
||||
$Test = Invoke-Expression -Command "cmd /c .\device-install.bat --debug -f $($TestCases."$Name"[0]) $($TestCases."$Name"[3])"
|
||||
|
||||
foreach ($Line in $Test) {
|
||||
if ($Line -match "Set OTA_OFFSET to" -or `
|
||||
$Line -match "Set SPIFFS_OFFSET to") {
|
||||
Write-Host -Object "$($Line -replace "^.*?Set","Set")" -ForegroundColor Blue
|
||||
}
|
||||
elseif ($VerbosePreference -eq "Continue") {
|
||||
Write-Host -Object $Line
|
||||
}
|
||||
if ($Line -match "ERROR") {
|
||||
$Errors += $Line
|
||||
$Counter++
|
||||
}
|
||||
}
|
||||
if ($null -ne $Errors) {
|
||||
Write-Host -Object "$Counter ERROR(s) detected!" -ForegroundColor Red
|
||||
if (-not ($VerbosePreference -eq "Continue")) { Write-Host -Object $Errors }
|
||||
}
|
||||
|
||||
foreach ($File in $Files) {
|
||||
if ($File.EndsWith(".bin")) {
|
||||
Remove-EmptyFile -FileName $File
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,175 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
TITLE Meshtastic device-update
|
||||
|
||||
set PYTHON=python
|
||||
SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "PYTHON="
|
||||
SET "ESPTOOL_BAUD=115200"
|
||||
SET "ESPTOOL_CMD="
|
||||
SET "LOGCOUNTER=0"
|
||||
|
||||
:: Determine the correct esptool command to use
|
||||
where esptool >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
set "ESPTOOL_CMD=esptool"
|
||||
) else (
|
||||
set "ESPTOOL_CMD=%PYTHON% -m esptool"
|
||||
)
|
||||
GOTO getopts
|
||||
:help
|
||||
ECHO Flash image file to device, but leave existing system intact.
|
||||
ECHO.
|
||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python]
|
||||
ECHO.
|
||||
ECHO Options:
|
||||
ECHO -f filename The .bin file to flash. Custom to your device type and region. (required)
|
||||
ECHO The file must be located in this current directory.
|
||||
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
|
||||
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
|
||||
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
||||
ECHO If supplied the script will use python.
|
||||
ECHO If not supplied the script will try to find esptool in Path.
|
||||
ECHO.
|
||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11
|
||||
GOTO eof
|
||||
|
||||
goto GETOPTS
|
||||
:HELP
|
||||
echo Usage: %~nx0 [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME^|FILENAME]
|
||||
echo Flash image file to device, leave existing system intact.
|
||||
echo.
|
||||
echo -h Display this help and exit
|
||||
echo -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerrous).
|
||||
echo -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: %PYTHON%)
|
||||
echo -f FILENAME The *update.bin file to flash. Custom to your device type.
|
||||
goto EOF
|
||||
:version
|
||||
ECHO %SCRIPT_NAME% [Version 2.6.0]
|
||||
ECHO Meshtastic
|
||||
GOTO eof
|
||||
|
||||
:GETOPTS
|
||||
if /I "%1"=="-h" goto HELP
|
||||
if /I "%1"=="--help" goto HELP
|
||||
if /I "%1"=="-F" set "FILENAME=%2" & SHIFT
|
||||
if /I "%1"=="-p" set ESPTOOL_PORT=%2 & SHIFT
|
||||
if /I "%1"=="-P" set PYTHON=%2 & SHIFT
|
||||
:getopts
|
||||
IF "%~1"=="" GOTO endopts
|
||||
IF /I "%~1"=="-?" GOTO help
|
||||
IF /I "%~1"=="-h" GOTO help
|
||||
IF /I "%~1"=="--help" GOTO help
|
||||
IF /I "%~1"=="-v" GOTO version
|
||||
IF /I "%~1"=="--version" GOTO version
|
||||
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
|
||||
IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
|
||||
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||
SHIFT
|
||||
IF NOT "__%1__"=="____" goto GETOPTS
|
||||
GOTO getopts
|
||||
:endopts
|
||||
|
||||
IF "__%FILENAME%__" == "____" (
|
||||
echo "Missing FILENAME"
|
||||
goto HELP
|
||||
)
|
||||
IF EXIST %FILENAME% IF NOT x%FILENAME:update=%==x%FILENAME% (
|
||||
echo Trying to flash update %FILENAME%
|
||||
%ESPTOOL_CMD% --baud 115200 write_flash 0x10000 %FILENAME%
|
||||
) else (
|
||||
echo "Invalid file: %FILENAME%"
|
||||
goto HELP
|
||||
) else (
|
||||
echo "Invalid file: %FILENAME%"
|
||||
goto HELP
|
||||
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
||||
IF "__!FILENAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
||||
GOTO help
|
||||
) ELSE (
|
||||
IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
|
||||
GOTO help
|
||||
)
|
||||
@REM Remove ".\" or "./" file prefix if present.
|
||||
SET "FILENAME=!FILENAME:.\=!"
|
||||
SET "FILENAME=!FILENAME:./=!"
|
||||
)
|
||||
|
||||
:EOF
|
||||
CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!"
|
||||
CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..."
|
||||
IF NOT EXIST !FILENAME! (
|
||||
CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating."
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
IF "!FILENAME:update=!"=="!FILENAME!" (
|
||||
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
|
||||
CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash update !FILENAME!."
|
||||
GOTO eof
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
|
||||
WHERE esptool >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 0 (
|
||||
@REM WHERE exits with code 0 if esptool is found.
|
||||
SET "ESPTOOL_CMD=esptool"
|
||||
) ELSE (
|
||||
SET "ESPTOOL_CMD=python -m esptool"
|
||||
CALL :RESET_ERROR
|
||||
)
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
IF %ERRORLEVEL% GTR 2 (
|
||||
@REM esptool exits with code 1 if help is displayed.
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
GOTO eof
|
||||
)
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!"
|
||||
IF "__!ESPTOOL_PORT!__" == "____" (
|
||||
CALL :LOG_MESSAGE WARN "Using esptool port: UNSET."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!."
|
||||
)
|
||||
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
|
||||
:RUN_ESPTOOL
|
||||
@REM Subroutine used to run ESPTOOL_CMD with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
EXIT /B %ERRORLEVEL%
|
||||
)
|
||||
GOTO :eof
|
||||
|
||||
:LOG_MESSAGE
|
||||
@REM Subroutine used to print log messages in four different levels.
|
||||
@REM DEBUG messages only get printed if [-d] flag is passed to script.
|
||||
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
|
||||
@REM.
|
||||
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
|
||||
SET /A LOGCOUNTER=LOGCOUNTER+1
|
||||
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO [91m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[91m%~2[0m
|
||||
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO [32m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[32m%~2[0m
|
||||
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO [33m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[33m%~2[0m
|
||||
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO [34m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[34m%~2[0m
|
||||
GOTO :eof
|
||||
|
||||
:GET_TIMESTAMP
|
||||
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
|
||||
@REM CALL :GET_TIMESTAMP
|
||||
@REM.
|
||||
@REM Updates: !TIMESTAMP!
|
||||
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
|
||||
SET "HH=%%a"
|
||||
SET "MM=%%b"
|
||||
SET "ss=%%c"
|
||||
)
|
||||
SET "TIMESTAMP=!HH!:!MM!:!ss!"
|
||||
GOTO :eof
|
||||
|
||||
:RESET_ERROR
|
||||
@REM Subroutine to reset %ERRORLEVEL% to 0.
|
||||
@REM CALL :RESET_ERROR
|
||||
@REM.
|
||||
@REM Updates: %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
GOTO :eof
|
||||
|
||||
@@ -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))
|
||||
@@ -125,4 +125,9 @@ for flag in flags:
|
||||
|
||||
projenv.Append(
|
||||
CCFLAGS=flags,
|
||||
)
|
||||
)
|
||||
|
||||
for lb in env.GetLibBuilders():
|
||||
if lb.name == "meshtastic-device-ui":
|
||||
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
||||
break
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
cd protobufs && ..\nanopb-0.4.9\generator-bin\protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:..\src\mesh\generated" -I=..\protobufs\ ..\protobufs\meshtastic\*.proto
|
||||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
cd protobufs
|
||||
..\nanopb-0.4.9\generator-bin\protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:..\src\mesh\generated" -I=..\protobufs\ ..\protobufs\meshtastic\*.proto
|
||||
GOTO eof
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
@@ -1,2 +1,124 @@
|
||||
@echo off
|
||||
if [%1]==[] (echo "Please specify a platformio NRF target (i.e. rak4631) as the first argument.") else (python3 .\bin\uf2conv.py .\.pio\build\%1\firmware.hex -c -o .\.pio\build\%1\firmware.uf2 -f 0xADA52840)
|
||||
@ECHO OFF
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
TITLE Meshtastic uf2-convert
|
||||
|
||||
SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "NRF=0"
|
||||
SET "UF2CONV_CMD=python3 .\bin\uf2conv.py"
|
||||
|
||||
GOTO getopts
|
||||
:help
|
||||
ECHO.
|
||||
ECHO Usage: %SCRIPT_NAME% -t [t-echo^|rak4631^|nano-g2-ultra^|wio-tracker-wm1110^|canaryone^|
|
||||
ECHO heltec-mesh-node-t114^|tracker-t1000-e^|rak_wismeshtap^|rak2560^|
|
||||
ECHO nrf52_promicro_diy_tcxo]
|
||||
ECHO.
|
||||
ECHO Options:
|
||||
ECHO -t target Specify a platformio NRF target to build for. (required)
|
||||
ECHO.
|
||||
ECHO Example: %SCRIPT_NAME% -t rak4631
|
||||
GOTO eof
|
||||
|
||||
:version
|
||||
ECHO %SCRIPT_NAME% [Version 2.6.0]
|
||||
ECHO Meshtastic
|
||||
GOTO eof
|
||||
|
||||
:getopts
|
||||
IF "%~1"=="" GOTO endopts
|
||||
IF /I "%~1"=="-?" GOTO help
|
||||
IF /I "%~1"=="-h" GOTO help
|
||||
IF /I "%~1"=="--help" GOTO help
|
||||
IF /I "%~1"=="-v" GOTO version
|
||||
IF /I "%~1"=="--version" GOTO version
|
||||
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
|
||||
IF /I "%~1"=="-t" SET "TARGETNAME=%~2" & SHIFT
|
||||
IF /I "%~1"=="--target" SET "TARGETNAME=%~2" & SHIFT
|
||||
SHIFT
|
||||
GOTO getopts
|
||||
:endopts
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking TARGETNAME parameter..."
|
||||
IF "__!TARGETNAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE DEBUG "Missing -t target input."
|
||||
GOTO help
|
||||
)
|
||||
|
||||
IF %DEBUG% EQU 1 SET "UF2CONV_CMD=REM python3 .\bin\uf2conv.py"
|
||||
|
||||
SET "NRFTARGETS=t-echo rak4631 nano-g2-ultra wio-tracker-wm1110 canaryone heltec-mesh-node-t114 tracker-t1000-e rak_wismeshtap rak2560 nrf52_promicro_diy_tcxo"
|
||||
FOR %%a IN (%NRFTARGETS%) DO (
|
||||
IF /I "%%a"=="!TARGETNAME!" (
|
||||
@REM We are working with any of %NRFTARGETS%.
|
||||
SET "NRF=1"
|
||||
GOTO end_loop_nrf
|
||||
)
|
||||
)
|
||||
:end_loop_nrf
|
||||
|
||||
@REM Building operations.
|
||||
IF !NRF! EQU 1 (
|
||||
CALL :LOG_MESSAGE INFO "Trying to build for !TARGETNAME!..."
|
||||
CALL :RUN_UF2CONV !TARGETNAME! || GOTO eof
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE WARN "!TARGETNAME! is not supported..."
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
|
||||
:eof
|
||||
ENDLOCAL
|
||||
EXIT /B %ERRORLEVEL%
|
||||
|
||||
|
||||
:RUN_UF2CONV
|
||||
@REM Subroutine used to run .\bin\uf2conv.py with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_UF2CONV [target]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_UF2CONV rak4631
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840"
|
||||
CALL :RESET_ERROR
|
||||
!UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840"
|
||||
EXIT /B %ERRORLEVEL%
|
||||
)
|
||||
GOTO :eof
|
||||
|
||||
:LOG_MESSAGE
|
||||
@REM Subroutine used to print log messages in four different levels.
|
||||
@REM DEBUG messages only get printed if [-d] flag is passed to script.
|
||||
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
|
||||
@REM.
|
||||
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
|
||||
SET /A LOGCOUNTER=LOGCOUNTER+1
|
||||
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO [91m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[91m%~2[0m
|
||||
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO [32m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[32m%~2[0m
|
||||
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO [33m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[33m%~2[0m
|
||||
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO [34m%1 [0m[37m^| !TIMESTAMP! !LOGCOUNTER! [0m[34m%~2[0m
|
||||
GOTO :eof
|
||||
|
||||
:GET_TIMESTAMP
|
||||
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
|
||||
@REM CALL :GET_TIMESTAMP
|
||||
@REM.
|
||||
@REM Updates: !TIMESTAMP!
|
||||
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
|
||||
SET "HH=%%a"
|
||||
SET "MM=%%b"
|
||||
SET "ss=%%c"
|
||||
)
|
||||
SET "TIMESTAMP=!HH!:!MM!:!ss!"
|
||||
GOTO :eof
|
||||
|
||||
:RESET_ERROR
|
||||
@REM Subroutine to reset %ERRORLEVEL% to 0.
|
||||
@REM CALL :RESET_ERROR
|
||||
@REM.
|
||||
@REM Updates: %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
GOTO :eof
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_ESP32S3_DEV",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
|
||||
56
boards/seeed_xiao_nrf52840_kit.json
Normal file
56
boards/seeed_xiao_nrf52840_kit.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v7.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x2886", "0x0166"]
|
||||
],
|
||||
"usb_product": "XIAO-BOOT",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "seeed_xiao_nrf52840_kit",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "7.3.0",
|
||||
"sd_fwid": "0x0123"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "seeed_xiao_nrf52840_kit",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink",
|
||||
"cmsis-dap",
|
||||
"blackmagic"
|
||||
],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://www.seeedstudio.com/XIAO-nRF52840-Wio-SX1262-Kit-for-Meshtastic-p-6400.html",
|
||||
"vendor": "seeed"
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"earlephilhower": {
|
||||
"boot2_source": "boot2_w25q080_2_padded_checksum.S",
|
||||
"usb_vid": "0x2E8A",
|
||||
"usb_pid": "0x000A"
|
||||
}
|
||||
},
|
||||
"core": "earlephilhower",
|
||||
"cpu": "cortex-m0plus",
|
||||
"extra_flags": "-DARDUINO_GENERIC_RP2040 -DRASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040 -DUSBD_MAX_POWER_MA=250",
|
||||
"f_cpu": "133000000L",
|
||||
"hwids": [
|
||||
["0x2E8A", "0x00C0"],
|
||||
["0x2E8A", "0x000A"]
|
||||
],
|
||||
"mcu": "rp2040",
|
||||
"variant": "WisBlock_RAK11300_Board"
|
||||
},
|
||||
"debug": {
|
||||
"jlink_device": "RP2040_M0_0",
|
||||
"openocd_target": "rp2040.cfg",
|
||||
"svd_path": "rp2040.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "WisBlock RAK11300",
|
||||
"upload": {
|
||||
"maximum_ram_size": 270336,
|
||||
"maximum_size": 2097152,
|
||||
"require_upload_port": true,
|
||||
"native_usb": true,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": false,
|
||||
"protocol": "picotool",
|
||||
"protocols": ["cmsis-dap", "raspberrypi-swd", "picotool", "picoprobe"]
|
||||
},
|
||||
"url": "https://docs.rakwireless.com/",
|
||||
"vendor": "RAKwireless"
|
||||
}
|
||||
1
debian/control
vendored
1
debian/control
vendored
@@ -17,6 +17,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
libbluetooth-dev,
|
||||
libusb-1.0-0-dev,
|
||||
libi2c-dev,
|
||||
libuv1-dev,
|
||||
openssl,
|
||||
libssl-dev,
|
||||
libulfius-dev,
|
||||
|
||||
@@ -36,6 +36,7 @@ BuildRequires: pkgconfig(libgpiod)
|
||||
BuildRequires: pkgconfig(bluez)
|
||||
BuildRequires: pkgconfig(libusb-1.0)
|
||||
BuildRequires: libi2c-devel
|
||||
BuildRequires: pkgconfig(libuv)
|
||||
# Web components:
|
||||
BuildRequires: pkgconfig(openssl)
|
||||
BuildRequires: pkgconfig(liborcania)
|
||||
|
||||
@@ -7,6 +7,8 @@ default_envs = tbeam
|
||||
extra_configs =
|
||||
arch/*/*.ini
|
||||
variants/*/platformio.ini
|
||||
src/graphics/niche/InkHUD/PlatformioConfig.ini
|
||||
|
||||
description = Meshtastic
|
||||
|
||||
[env]
|
||||
@@ -58,7 +60,7 @@ lib_deps =
|
||||
mathertel/OneButton@2.6.1
|
||||
https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159
|
||||
https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4
|
||||
https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0
|
||||
https://github.com/meshtastic/ArduinoThread.git#7c3ee9e1951551b949763b1f5280f8db1fa4068d
|
||||
nanopb/Nanopb@0.4.91
|
||||
erriez/ErriezCRC32@1.0.1
|
||||
|
||||
@@ -77,7 +79,7 @@ lib_deps =
|
||||
${env.lib_deps}
|
||||
end2endzone/NonBlockingRTTTL@1.3.0
|
||||
build_flags = ${env.build_flags} -Os
|
||||
build_src_filter = ${env.build_src_filter} -<platform/portduino/>
|
||||
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
|
||||
|
||||
; Common libs for communicating over TCP/IP networks such as MQTT
|
||||
[networking_base]
|
||||
@@ -90,6 +92,10 @@ lib_deps =
|
||||
lib_deps =
|
||||
jgromes/RadioLib@7.1.2
|
||||
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
https://github.com/meshtastic/device-ui.git#74e739ed4532ca10393df9fc89ae5a22f0bab2b1
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
; (not included in native / portduino)
|
||||
[environmental_base]
|
||||
@@ -100,6 +106,7 @@ lib_deps =
|
||||
adafruit/Adafruit BMP085 Library@1.2.4
|
||||
adafruit/Adafruit BME280 Library@2.2.4
|
||||
adafruit/Adafruit BMP3XX Library@2.1.5
|
||||
adafruit/Adafruit DPS310@1.1.5
|
||||
adafruit/Adafruit MCP9808 Library@2.0.2
|
||||
adafruit/Adafruit INA260 Library@1.5.2
|
||||
adafruit/Adafruit INA219@1.2.3
|
||||
|
||||
Submodule protobufs updated: e2790151f0...14ec205865
105
src/BluetoothStatus.h
Normal file
105
src/BluetoothStatus.h
Normal file
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
#include "Status.h"
|
||||
#include "assert.h"
|
||||
#include "configuration.h"
|
||||
#include "meshUtils.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
|
||||
// Describes the state of the Bluetooth connection
|
||||
// Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code
|
||||
class BluetoothStatus : public Status
|
||||
{
|
||||
public:
|
||||
enum class ConnectionState {
|
||||
DISCONNECTED,
|
||||
PAIRING,
|
||||
CONNECTED,
|
||||
};
|
||||
|
||||
private:
|
||||
CallbackObserver<BluetoothStatus, const BluetoothStatus *> statusObserver =
|
||||
CallbackObserver<BluetoothStatus, const BluetoothStatus *>(this, &BluetoothStatus::updateStatus);
|
||||
|
||||
ConnectionState state = ConnectionState::DISCONNECTED;
|
||||
std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero
|
||||
|
||||
public:
|
||||
BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; }
|
||||
|
||||
// New BluetoothStatus: connected or disconnected
|
||||
explicit 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
|
||||
explicit BluetoothStatus(const std::string &passkey) : Status()
|
||||
{
|
||||
statusType = STATUS_TYPE_BLUETOOTH;
|
||||
this->state = ConnectionState::PAIRING;
|
||||
this->passkey = passkey;
|
||||
}
|
||||
|
||||
ConnectionState getConnectionState() const { return this->state; }
|
||||
|
||||
std::string getPasskey() const
|
||||
{
|
||||
assert(state == ConnectionState::PAIRING);
|
||||
return this->passkey;
|
||||
}
|
||||
|
||||
void observe(Observable<const BluetoothStatus *> *source) { statusObserver.observe(source); }
|
||||
|
||||
bool matches(const BluetoothStatus *newStatus) const
|
||||
{
|
||||
if (this->state == newStatus->getConnectionState()) {
|
||||
// Same state: CONNECTED / DISCONNECTED
|
||||
if (this->state != ConnectionState::PAIRING)
|
||||
return true;
|
||||
// Same state: PAIRING, and passkey matches
|
||||
else if (this->getPasskey() == newStatus->getPasskey())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int updateStatus(const BluetoothStatus *newStatus)
|
||||
{
|
||||
// Has the status changed?
|
||||
if (!matches(newStatus)) {
|
||||
// Copy the members
|
||||
state = newStatus->getConnectionState();
|
||||
if (state == ConnectionState::PAIRING)
|
||||
passkey = newStatus->getPasskey();
|
||||
|
||||
// Tell anyone interested that we have an update
|
||||
onNewStatus.notifyObservers(this);
|
||||
|
||||
// Debug only:
|
||||
switch (state) {
|
||||
case ConnectionState::PAIRING:
|
||||
LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str());
|
||||
break;
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace meshtastic
|
||||
|
||||
extern meshtastic::BluetoothStatus *bluetoothStatus;
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "main.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
@@ -99,6 +100,13 @@ ButtonThread::ButtonThread() : OSThread("Button")
|
||||
userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click?
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
// Used to detach and reattach interrupts
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
attachButtonInterrupts();
|
||||
#endif
|
||||
}
|
||||
@@ -320,6 +328,26 @@ void ButtonThread::detachButtonInterrupts()
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int ButtonThread::beforeLightSleep(void *unused)
|
||||
{
|
||||
detachButtonInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachButtonInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Watch a GPIO and if we get an IRQ, wake the main thread.
|
||||
* Use to add wake on button press
|
||||
|
||||
@@ -37,6 +37,12 @@ class ButtonThread : public concurrency::OSThread
|
||||
void detachButtonInterrupts();
|
||||
void storeClickCount();
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
||||
static OneButton userButton; // Static - accessed from an interrupt
|
||||
@@ -48,6 +54,14 @@ class ButtonThread : public concurrency::OSThread
|
||||
OneButton userButtonTouch;
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<ButtonThread, void *> lsObserver =
|
||||
CallbackObserver<ButtonThread, void *>(this, &ButtonThread::beforeLightSleep);
|
||||
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t>(this, &ButtonThread::afterLightSleep);
|
||||
#endif
|
||||
|
||||
// set during IRQ
|
||||
static volatile ButtonEventType btnEvent;
|
||||
|
||||
|
||||
@@ -121,10 +121,15 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
// Default Bluetooth PIN
|
||||
#define defaultBLEPin 123456
|
||||
|
||||
#if HAS_ETHERNET
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#include <RAK13800_W5100S.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#if HAS_WIFI
|
||||
#include <WiFi.h>
|
||||
#endif // HAS_WIFI
|
||||
@@ -164,4 +169,4 @@ class Syslog
|
||||
bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0)));
|
||||
};
|
||||
|
||||
#endif // HAS_ETHERNET || HAS_WIFI
|
||||
#endif // HAS_NETWORKING
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef DELAY_FOREVER
|
||||
|
||||
@@ -11,12 +11,18 @@ static File openFile(const char *filename, bool fullAtomic)
|
||||
FSCom.remove(filename);
|
||||
return FSCom.open(filename, FILE_O_WRITE);
|
||||
#endif
|
||||
if (!fullAtomic)
|
||||
if (!fullAtomic) {
|
||||
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
|
||||
}
|
||||
|
||||
String filenameTmp = filename;
|
||||
filenameTmp += ".tmp";
|
||||
|
||||
// FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now
|
||||
// if (fullAtomic) {
|
||||
// FSCom.remove(filename);
|
||||
// }
|
||||
|
||||
// clear any previous LFS errors
|
||||
return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -135,6 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define LPS22HB_ADDR 0x5C
|
||||
#define LPS22HB_ADDR_ALT 0x5D
|
||||
#define SHT31_4x_ADDR 0x44
|
||||
#define SHT31_4x_ADDR_ALT 0x45
|
||||
#define PMSA0031_ADDR 0x12
|
||||
#define QMA6100P_ADDR 0x12
|
||||
#define AHT10_ADDR 0x38
|
||||
@@ -150,6 +151,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MAX30102_ADDR 0x57
|
||||
#define MLX90614_ADDR_DEF 0x5A
|
||||
#define CGRADSENS_ADDR 0x66
|
||||
#define LTR390UV_ADDR 0x53
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ACCELEROMETER
|
||||
|
||||
@@ -67,6 +67,8 @@ class ScanI2C
|
||||
INA226,
|
||||
NXP_SE050,
|
||||
DFROBOT_RAIN,
|
||||
DPS310,
|
||||
LTR390UV,
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -237,6 +237,16 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("BMP085/BMP180", (uint8_t)addr.address);
|
||||
type = BMP_085;
|
||||
break;
|
||||
case 0x00:
|
||||
// do we have a DPS310 instead?
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1);
|
||||
switch (registerValue) {
|
||||
case 0x10:
|
||||
logFoundDevice("DPS310", (uint8_t)addr.address);
|
||||
type = DPS310;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID
|
||||
switch (registerValue) {
|
||||
@@ -339,7 +349,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SHT31_4x_ADDR:
|
||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
|
||||
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c) {
|
||||
type = SHT4X;
|
||||
@@ -412,11 +423,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(OPT3001_ADDR, OPT3001, "OPT3001", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||
#ifdef HAS_TPS65233
|
||||
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#include <cstring> // Include for strstr
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "Default.h"
|
||||
@@ -1100,12 +1104,16 @@ int32_t GPS::runOnce()
|
||||
return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000;
|
||||
}
|
||||
|
||||
// clear the GPS rx buffer as quickly as possible
|
||||
// clear the GPS rx/tx buffer as quickly as possible
|
||||
void GPS::clearBuffer()
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
_serial_gps->flush(false);
|
||||
#else
|
||||
int x = _serial_gps->available();
|
||||
while (x--)
|
||||
_serial_gps->read();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs
|
||||
@@ -1117,7 +1125,7 @@ int GPS::prepareDeepSleep(void *unused)
|
||||
}
|
||||
|
||||
static const char *PROBE_MESSAGE = "Trying %s (%s)...";
|
||||
static const char *DETECTED_MESSAGE = "%s detected, using %s Module";
|
||||
static const char *DETECTED_MESSAGE = "%s detected";
|
||||
|
||||
#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \
|
||||
do { \
|
||||
@@ -1125,11 +1133,22 @@ static const char *DETECTED_MESSAGE = "%s detected, using %s Module";
|
||||
clearBuffer(); \
|
||||
_serial_gps->write(TOWRITE "\r\n"); \
|
||||
if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \
|
||||
LOG_INFO(DETECTED_MESSAGE, CHIP, #DRIVER); \
|
||||
LOG_INFO(DETECTED_MESSAGE, CHIP); \
|
||||
return DRIVER; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \
|
||||
do { \
|
||||
LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \
|
||||
clearBuffer(); \
|
||||
_serial_gps->write(COMMAND "\r\n"); \
|
||||
GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \
|
||||
if (detectedDriver != GNSS_MODEL_UNKNOWN) { \
|
||||
return detectedDriver; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
GnssModel_t GPS::probe(int serialSpeed)
|
||||
{
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
|
||||
@@ -1160,31 +1179,34 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
delay(20);
|
||||
|
||||
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
|
||||
PROBE_SIMPLE("UC6580", "$PDTINFO", "UC6580", GNSS_MODEL_UC6580, 500);
|
||||
PROBE_SIMPLE("UM600", "$PDTINFO", "UM600", GNSS_MODEL_UC6580, 500);
|
||||
PROBE_SIMPLE("ATGM336H", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H, 500);
|
||||
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS))
|
||||
based on AT6558 */
|
||||
PROBE_SIMPLE("ATGM332D", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H, 500);
|
||||
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
|
||||
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
|
||||
|
||||
std::vector<ChipInfo> atgm = {
|
||||
{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
|
||||
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
|
||||
{"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
|
||||
PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
|
||||
|
||||
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
|
||||
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
|
||||
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
|
||||
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
|
||||
PROBE_SIMPLE("AG3335", "$PAIR021*39", "$PAIR021,AG3335", GNSS_MODEL_AG3335, 500);
|
||||
PROBE_SIMPLE("AG3352", "$PAIR021*39", "$PAIR021,AG3352", GNSS_MODEL_AG3352, 500);
|
||||
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
|
||||
std::vector<ChipInfo> airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
|
||||
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
|
||||
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
|
||||
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
|
||||
|
||||
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
|
||||
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
|
||||
|
||||
// Close all NMEA sentences, valid for L76B MTK platform (Waveshare Pico GPS)
|
||||
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n");
|
||||
delay(20);
|
||||
|
||||
PROBE_SIMPLE("L76B", "$PMTK605*31", "Quectel-L76B", GNSS_MODEL_MTK_L76B, 500);
|
||||
PROBE_SIMPLE("PA1616S", "$PMTK605*31", "1616S", GNSS_MODEL_MTK_PA1616S, 500);
|
||||
|
||||
PROBE_SIMPLE("LS20031", "$PMTK605*31", "MC-1513", GNSS_MODEL_LS20031, 500);
|
||||
std::vector<ChipInfo> mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B},
|
||||
{"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S},
|
||||
{"LS20031", "MC-1513", GNSS_MODEL_LS20031}};
|
||||
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500);
|
||||
|
||||
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||
UBXChecksum(cfg_rate, sizeof(cfg_rate));
|
||||
@@ -1281,6 +1303,38 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
return GNSS_MODEL_UNKNOWN;
|
||||
}
|
||||
|
||||
GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap)
|
||||
{
|
||||
String response = "";
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < timeout) {
|
||||
if (_serial_gps->available()) {
|
||||
response += (char)_serial_gps->read();
|
||||
|
||||
if (response.endsWith(",") || response.endsWith("\r\n")) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.c_str());
|
||||
#endif
|
||||
// check if we can see our chips
|
||||
for (const auto &chipInfo : responseMap) {
|
||||
if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) {
|
||||
LOG_INFO("%s detected", chipInfo.chipName.c_str());
|
||||
return chipInfo.driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.endsWith("\r\n")) {
|
||||
response.trim();
|
||||
response = ""; // Reset the response string for the next potential message
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.c_str());
|
||||
#endif
|
||||
return GNSS_MODEL_UNKNOWN; // Return empty string on timeout
|
||||
}
|
||||
|
||||
GPS *GPS::createGps()
|
||||
{
|
||||
int8_t _rx_gpio = config.position.rx_gpio;
|
||||
|
||||
@@ -48,6 +48,11 @@ enum GPSPowerState : uint8_t {
|
||||
GPS_OFF // Powered off indefinitely
|
||||
};
|
||||
|
||||
struct ChipInfo {
|
||||
String chipName; // The name of the chip (for logging)
|
||||
String detectionString; // The string to match in the response
|
||||
GnssModel_t driver; // The driver to use
|
||||
};
|
||||
/**
|
||||
* A gps class that only reads from the GPS periodically and keeps the gps powered down except when reading
|
||||
*
|
||||
@@ -230,6 +235,8 @@ class GPS : private concurrency::OSThread
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap);
|
||||
|
||||
// Get GNSS model
|
||||
GnssModel_t probe(int serialSpeed);
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ bool EInkDisplay::connect()
|
||||
}
|
||||
|
||||
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) || \
|
||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER)
|
||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER)
|
||||
{
|
||||
// Start HSPI
|
||||
hspi = new SPIClass(HSPI);
|
||||
@@ -182,6 +182,9 @@ bool EInkDisplay::connect()
|
||||
// Init GxEPD2
|
||||
adafruitDisplay->init();
|
||||
adafruitDisplay->setRotation(3);
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER)
|
||||
adafruitDisplay->setRotation(0);
|
||||
#endif
|
||||
}
|
||||
#elif defined(PCA10059) || defined(ME25LS01)
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ class EInkDisplay : public OLEDDisplay
|
||||
|
||||
// If display uses HSPI
|
||||
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
|
||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER)
|
||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER)
|
||||
SPIClass *hspi = NULL;
|
||||
#endif
|
||||
|
||||
@@ -77,4 +77,4 @@ class EInkDisplay : public OLEDDisplay
|
||||
uint32_t lastDrawMsec = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -324,6 +324,14 @@ void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
|
||||
if (refresh != UNSPECIFIED)
|
||||
return;
|
||||
|
||||
// Bypass limit if UNLIMITED_FAST mode is active
|
||||
if (frameFlags & UNLIMITED_FAST) {
|
||||
refresh = FAST;
|
||||
reason = NO_OBJECTIONS;
|
||||
LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags);
|
||||
return;
|
||||
}
|
||||
|
||||
// If too many FAST refreshes consecutively - force a FULL refresh
|
||||
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
|
||||
refresh = FULL;
|
||||
|
||||
@@ -23,6 +23,10 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
|
||||
EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
|
||||
~EInkDynamicDisplay();
|
||||
|
||||
// Methods to enable or disable unlimited fast refresh mode
|
||||
void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); }
|
||||
void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); }
|
||||
|
||||
// What kind of frame is this
|
||||
enum frameFlagTypes : uint8_t {
|
||||
BACKGROUND = (1 << 0), // For frames via display()
|
||||
@@ -30,6 +34,7 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
|
||||
COSMETIC = (1 << 2), // For splashes
|
||||
DEMAND_FAST = (1 << 3), // Special case only
|
||||
BLOCKING = (1 << 4), // Modifier - block while refresh runs
|
||||
UNLIMITED_FAST = (1 << 5)
|
||||
};
|
||||
void addFrameFlag(frameFlagTypes flag);
|
||||
|
||||
|
||||
@@ -73,6 +73,16 @@
|
||||
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
|
||||
#endif
|
||||
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER)
|
||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||
#undef FONT_SMALL
|
||||
#undef FONT_MEDIUM
|
||||
#undef FONT_LARGE
|
||||
#define FONT_SMALL FONT_LARGE_LOCAL // Height: 30
|
||||
#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 30
|
||||
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 30
|
||||
#endif
|
||||
|
||||
#define _fontHeight(font) ((font)[1] + 1) // height is position 1
|
||||
|
||||
#define FONT_HEIGHT_SMALL _fontHeight(FONT_SMALL)
|
||||
|
||||
1184
src/graphics/fonts/EinkDisplayFonts.cpp
Normal file
1184
src/graphics/fonts/EinkDisplayFonts.cpp
Normal file
File diff suppressed because it is too large
Load Diff
14
src/graphics/fonts/EinkDisplayFonts.h
Normal file
14
src/graphics/fonts/EinkDisplayFonts.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef EINKDISPLAYFONTS_h
|
||||
#define EINKDISPLAYFONTS_h
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
#elif __MBED__
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Monospaced Plain 30
|
||||
*/
|
||||
extern const uint8_t Monospaced_plain_30[] PROGMEM;
|
||||
#endif
|
||||
108
src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp
Normal file
108
src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "./LatchingBacklight.h"
|
||||
|
||||
#include "assert.h"
|
||||
|
||||
#include "sleep.h"
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Private constructor
|
||||
// Called by getInstance
|
||||
LatchingBacklight::LatchingBacklight()
|
||||
{
|
||||
// Attach the deep sleep callback
|
||||
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
LatchingBacklight *LatchingBacklight::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static LatchingBacklight *const singletonInstance = new LatchingBacklight;
|
||||
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Which pin controls the backlight?
|
||||
// Is the light active HIGH (default) or active LOW?
|
||||
void LatchingBacklight::setPin(uint8_t pin, bool activeWhen)
|
||||
{
|
||||
this->pin = pin;
|
||||
this->logicActive = activeWhen;
|
||||
|
||||
pinMode(pin, OUTPUT);
|
||||
off(); // Explicit off seem required by T-Echo?
|
||||
}
|
||||
|
||||
// Called when device is shutting down
|
||||
// Ensures the backlight is off
|
||||
int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
{
|
||||
// Contingency only
|
||||
// - pin wasn't set
|
||||
if (pin != (uint8_t)-1) {
|
||||
off();
|
||||
pinMode(pin, INPUT); // High impedance - unnecessary?
|
||||
} else
|
||||
LOG_WARN("LatchingBacklight instantiated, but pin not set");
|
||||
return 0; // Continue with deep sleep
|
||||
}
|
||||
|
||||
// Turn the backlight on *temporarily*
|
||||
// This should be used for momentary illumination, such as while a button is held
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::peek()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = false;
|
||||
}
|
||||
|
||||
// Turn the backlight on, and keep it on
|
||||
// This should be used when the backlight should remain active, even after user input ends
|
||||
// e.g. when enabled via the menu
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::latch()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
|
||||
// Blink if moving from peek to latch
|
||||
// Indicates to user that the transition has taken place
|
||||
if (on && !latched) {
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
delay(25);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
}
|
||||
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = true;
|
||||
}
|
||||
|
||||
// Turn the backlight off
|
||||
// Suitable for ending both peek and latch
|
||||
void LatchingBacklight::off()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
on = false;
|
||||
latched = false;
|
||||
}
|
||||
|
||||
bool LatchingBacklight::isOn()
|
||||
{
|
||||
return on;
|
||||
}
|
||||
|
||||
bool LatchingBacklight::isLatched()
|
||||
{
|
||||
return latched;
|
||||
}
|
||||
|
||||
#endif
|
||||
50
src/graphics/niche/Drivers/Backlight/LatchingBacklight.h
Normal file
50
src/graphics/niche/Drivers/Backlight/LatchingBacklight.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
|
||||
Singleton class
|
||||
On-demand control of a display's backlight, connected to a GPIO
|
||||
Initial use case is control of T-Echo's frontlight, via the capacitive touch button
|
||||
|
||||
- momentary on
|
||||
- latched on
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "Observer.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class LatchingBacklight
|
||||
{
|
||||
public:
|
||||
static LatchingBacklight *getInstance(); // Create or get the singleton instance
|
||||
void setPin(uint8_t pin, bool activeWhen = HIGH);
|
||||
|
||||
int beforeDeepSleep(void *unused); // Callback for auto-shutoff
|
||||
|
||||
void peek(); // Backlight on temporarily, e.g. while button held
|
||||
void latch(); // Backlight on permanently, e.g. toggled via menu
|
||||
void off(); // Backlight off. Suitable for both peek and latch
|
||||
|
||||
bool isOn(); // Either peek or latch
|
||||
bool isLatched();
|
||||
|
||||
private:
|
||||
LatchingBacklight(); // Constructor made private: force use of getInstance
|
||||
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||
|
||||
uint8_t pin = (uint8_t)-1;
|
||||
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||
|
||||
bool on = false; // Is light on (either peek or latched)
|
||||
bool latched = false; // Is light latched on
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
1
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp
Normal file
1
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "./DEPG0154BNS800.h"
|
||||
34
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h
Normal file
34
src/graphics/niche/Drivers/EInk/DEPG0154BNS800.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- DEPG0154BNS800
|
||||
- Manufacturer: DKE
|
||||
- Size: 1.54 inch
|
||||
- Resolution: 152px x 152px
|
||||
- Flex connector marking: FPC7525
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class DEPG0154BNS800 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 152;
|
||||
static constexpr uint32_t height = 152;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL);
|
||||
|
||||
public:
|
||||
DEPG0154BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
120
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp
Normal file
120
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "./DEPG0290BNS800.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Describes the operation performed when a "fast refresh" is performed
|
||||
// Source: custom, with DEPG0150BNS810 as a reference
|
||||
static const uint8_t LUT_FAST[] = {
|
||||
// 1 2 3 4
|
||||
0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels)
|
||||
0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels)
|
||||
0x00, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels)
|
||||
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM
|
||||
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1. Tap existing black pixels back into place
|
||||
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. Move new pixels
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. New pixels, and also existing black pixels
|
||||
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // 4. All pixels, then cooldown
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
|
||||
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// How strongly the pixels are pulled and pushed
|
||||
void DEPG0290BNS800::configVoltages()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
// Listed as "typical" in datasheet
|
||||
sendCommand(0x04);
|
||||
sendData(0x41); // VSH1 15V
|
||||
sendData(0x00); // VSH2 NA
|
||||
sendData(0x32); // VSL -15V
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load settings about how the pixels are moved from old state to new state during a refresh
|
||||
// - manually specified,
|
||||
// - or with stored values from displays OTP memory
|
||||
void DEPG0290BNS800::configWaveform()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x60); // Actively hold screen border during update
|
||||
|
||||
sendCommand(0x32); // Write LUT register from MCU:
|
||||
sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh)
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Describes the sequence of events performed by the displays controller IC during a refresh
|
||||
// Includes "power up", "load settings from memory", "update the pixels", etc
|
||||
void DEPG0290BNS800::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xCF); // Differential, use manually loaded waveform
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Once the refresh operation has been started,
|
||||
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
|
||||
// Only used when refresh is "async"
|
||||
void DEPG0290BNS800::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 450); // At least 450ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 3000); // At least 3 seconds for full refresh
|
||||
}
|
||||
}
|
||||
|
||||
// For this display, we do not need to re-write the new image.
|
||||
// We're overriding SSD16XX::finalizeUpdate to make this small optimization.
|
||||
// The display does also work just fine with the generic SSD16XX method, though.
|
||||
void DEPG0290BNS800::finalizeUpdate()
|
||||
{
|
||||
// Put a copy of the image into the "old memory".
|
||||
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||
if (updateType != FULL) {
|
||||
// writeNewImage(); // Not required for this display
|
||||
writeOldImage();
|
||||
sendCommand(0x7F); // Terminate image write without update
|
||||
wait();
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
42
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h
Normal file
42
src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- DEPG0290BNS800
|
||||
- Manufacturer: DKE
|
||||
- Size: 2.9 inch
|
||||
- Resolution: 128px x 296px
|
||||
- Flex connector marking: FPC-7519 rev.b
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class DEPG0290BNS800 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 128;
|
||||
static constexpr uint32_t height = 296;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
|
||||
protected:
|
||||
void configVoltages() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
void finalizeUpdate() override; // Only overriden for a slight optimization
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
70
src/graphics/niche/Drivers/EInk/EInk.cpp
Normal file
70
src/graphics/niche/Drivers/EInk/EInk.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "./EInk.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants
|
||||
EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported)
|
||||
: concurrency::OSThread("E-Ink Driver"), width(width), height(height), supportedUpdateTypes(supported)
|
||||
{
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Used by NicheGraphics implementations to check if a display supports a specific refresh operation.
|
||||
// Whether or not the update type is supported is specified in the constructor
|
||||
bool EInk::supports(UpdateTypes type)
|
||||
{
|
||||
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.
|
||||
if (supportedUpdateTypes & type)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
// Begins using the OSThread to detect when a display update is complete
|
||||
// This allows the refresh operation to run "asynchronously".
|
||||
// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin
|
||||
// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes.
|
||||
// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration",
|
||||
// provided its isUpdateDone() override always returns true.
|
||||
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
|
||||
{
|
||||
updateRunning = true;
|
||||
updateBegunAt = millis();
|
||||
pollingInterval = interval;
|
||||
|
||||
// To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take
|
||||
// By default, expectedDuration is 0, and we'll start polling immediately
|
||||
OSThread::setIntervalFromNow(expectedDuration);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
|
||||
// Meshtastic's pseudo-threading layer
|
||||
// We're using this as a timer, to periodically check if an update is complete
|
||||
// This is what allows us to update the display asynchronously
|
||||
int32_t EInk::runOnce()
|
||||
{
|
||||
if (!isUpdateDone())
|
||||
return pollingInterval; // Poll again in a few ms
|
||||
|
||||
// If update done:
|
||||
finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc
|
||||
updateRunning = false; // Change what we report via EInk::busy()
|
||||
return disable(); // Stop polling
|
||||
}
|
||||
|
||||
// Wait for an in progress update to complete before continuing
|
||||
// Run a normal (async) update first, *then* call await
|
||||
void EInk::await()
|
||||
{
|
||||
// Stop our concurrency thread
|
||||
OSThread::disable();
|
||||
|
||||
// Sit and block until the update is complete
|
||||
while (updateRunning) {
|
||||
runOnce();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
56
src/graphics/niche/Drivers/EInk/EInk.h
Normal file
56
src/graphics/niche/Drivers/EInk/EInk.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
|
||||
Base class for E-Ink display drivers
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "configuration.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <SPI.h>
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class EInk : private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
// Different possible operations used to update an E-Ink display
|
||||
// Some displays will not support all operations
|
||||
// Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType)
|
||||
enum UpdateTypes : uint8_t {
|
||||
UNSPECIFIED = 0,
|
||||
FULL = 1 << 0,
|
||||
FAST = 1 << 1,
|
||||
};
|
||||
|
||||
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
|
||||
void await(); // Wait for an in-progress update to complete before proceeding
|
||||
bool supports(UpdateTypes type); // Can display perform 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 = 0; // For initial pause before polling for update completion
|
||||
uint32_t pollingInterval = 0; // How often to check if update complete (ms)
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
61
src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp
Normal file
61
src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "./GDEY0154D67.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void GDEY0154D67::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xC7);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
|
||||
// To-do: delete this method?
|
||||
// Values set here might be redundant: C7, 00, 00 seems to be default
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void GDEY0154D67::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
void GDEY0154D67::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Once the refresh operation has been started,
|
||||
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
|
||||
// Only used when refresh is "async"
|
||||
void GDEY0154D67::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
42
src/graphics/niche/Drivers/EInk/GDEY0154D67.h
Normal file
42
src/graphics/niche/Drivers/EInk/GDEY0154D67.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- GDEY0154D67
|
||||
- Manufacturer: Goodisplay
|
||||
- Size: 1.54 inch
|
||||
- Resolution: 200px x 200px
|
||||
- Flex connector marking: FPC-B001
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class GDEY0154D67 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 200;
|
||||
static constexpr uint32_t height = 200;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
295
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp
Normal file
295
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#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();
|
||||
}
|
||||
|
||||
// Display an image on the display
|
||||
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)
|
||||
{
|
||||
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
|
||||
71
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h
Normal file
71
src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
|
||||
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(); // Used for "differential update", aka FAST refresh
|
||||
|
||||
void detachFromUpdate();
|
||||
bool isUpdateDone();
|
||||
void finalizeUpdate();
|
||||
|
||||
protected:
|
||||
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize = 0; // In bytes. Rows * Columns
|
||||
uint8_t *buffer = nullptr;
|
||||
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
|
||||
|
||||
uint8_t pin_dc = -1;
|
||||
uint8_t pin_cs = -1;
|
||||
uint8_t pin_busy = -1;
|
||||
uint8_t pin_rst = -1;
|
||||
SPIClass *spi = nullptr;
|
||||
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
85
src/graphics/niche/Drivers/EInk/README.md
Normal file
85
src/graphics/niche/Drivers/EInk/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 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 the 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)`
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### `await()`
|
||||
|
||||
Wait for an in-progress update to complete before continuing
|
||||
|
||||
### `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 portrait. 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.
|
||||
220
src/graphics/niche/Drivers/EInk/SSD16XX.cpp
Normal file
220
src/graphics/niche/Drivers/EInk/SSD16XX.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
#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 Solomon 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)
|
||||
{
|
||||
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
|
||||
65
src/graphics/niche/Drivers/EInk/SSD16XX.h
Normal file
65
src/graphics/niche/Drivers/EInk/SSD16XX.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
|
||||
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(); // Image which can be used at *next* update for "differential refresh"
|
||||
|
||||
virtual void detachFromUpdate();
|
||||
virtual bool isUpdateDone() override;
|
||||
virtual void finalizeUpdate() override;
|
||||
|
||||
protected:
|
||||
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize = 0; // In bytes. Rows * Columns
|
||||
uint8_t *buffer = nullptr;
|
||||
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
|
||||
|
||||
uint8_t pin_dc = -1;
|
||||
uint8_t pin_cs = -1;
|
||||
uint8_t pin_busy = -1;
|
||||
uint8_t pin_rst = -1;
|
||||
SPIClass *spi = nullptr;
|
||||
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
3
src/graphics/niche/Drivers/README.md
Normal file
3
src/graphics/niche/Drivers/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# NicheGraphics - Drivers
|
||||
|
||||
Common drivers which can be used by various NicheGraphics UIs
|
||||
140
src/graphics/niche/FlashData.h
Normal file
140
src/graphics/niche/FlashData.h
Normal file
@@ -0,0 +1,140 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
/*
|
||||
|
||||
Re-usable NicheGraphics tool
|
||||
|
||||
Save settings / data to flash, without use of the Meshtastic Protobufs
|
||||
Avoid bloating everyone's protobuf code for our one-off UI implementations
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "SafeFile.h"
|
||||
|
||||
namespace NicheGraphics
|
||||
{
|
||||
|
||||
template <typename T> class FlashData
|
||||
{
|
||||
private:
|
||||
static std::string getFilename(const char *label)
|
||||
{
|
||||
std::string filename;
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".data";
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
static uint32_t getHash(T *data)
|
||||
{
|
||||
uint32_t hash = 0;
|
||||
|
||||
// Sum all bytes of the image buffer together
|
||||
for (uint32_t i = 0; i < sizeof(T); i++)
|
||||
hash ^= ((uint8_t *)data)[i] + 1;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public:
|
||||
static bool load(T *data, const char *label)
|
||||
{
|
||||
// Set false if we run into issues
|
||||
bool okay = true;
|
||||
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
okay = false;
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str());
|
||||
|
||||
// Create an object which will received data from flash
|
||||
// We read here first, so we can verify the checksum, without committing to overwriting the *data object
|
||||
// Allows us to retain any defaults that might be set after we declared *data, but before loading settings,
|
||||
// in case the flash values are corrupt
|
||||
T flashData;
|
||||
|
||||
// Read the actual data
|
||||
f.readBytes((char *)&flashData, sizeof(T));
|
||||
|
||||
// Read the hash
|
||||
uint32_t savedHash = 0;
|
||||
f.readBytes((char *)&savedHash, sizeof(savedHash));
|
||||
|
||||
// Calculate hash of the loaded data, then compare with the saved hash
|
||||
// If hash looks good, copy the values to the main data object
|
||||
uint32_t calculatedHash = getHash(&flashData);
|
||||
if (savedHash != calculatedHash) {
|
||||
LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str());
|
||||
okay = false;
|
||||
} else
|
||||
*data = flashData;
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
okay = false;
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
okay = false;
|
||||
#endif
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Save module's custom data (settings?) to flash. Does use protobufs
|
||||
static void save(T *data, const char *label)
|
||||
{
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
|
||||
auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename.
|
||||
|
||||
LOG_INFO("Saving %s", filename.c_str());
|
||||
|
||||
// Calculate a hash of the data
|
||||
uint32_t hash = getHash(data);
|
||||
|
||||
f.write((uint8_t *)data, sizeof(T)); // Write the actual data
|
||||
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
|
||||
|
||||
// f.flush();
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics
|
||||
|
||||
#endif
|
||||
129
src/graphics/niche/Fonts/FreeSans6pt7b.h
Normal file
129
src/graphics/niche/Fonts/FreeSans6pt7b.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
const uint8_t FreeSans6pt7bBitmaps[] PROGMEM = {
|
||||
0xAA, 0xA8, 0xC0, 0xF6, 0xA0, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00,
|
||||
0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, 0x70, 0x91, 0x23, 0x86, 0x12, 0xA2, 0x4E, 0xF4, 0xE0,
|
||||
0x5A, 0xAA, 0x94, 0x89, 0x12, 0x49, 0x29, 0x00, 0x27, 0x50, 0x21, 0x3E, 0x42, 0x00, 0xE0, 0xC0, 0x80, 0x24, 0xA4, 0xA4, 0x80,
|
||||
0x74, 0xE3, 0x18, 0xC6, 0x33, 0x70, 0x27, 0x92, 0x49, 0x20, 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, 0x79, 0x30, 0x43, 0x18,
|
||||
0x10, 0x71, 0x78, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0x7D, 0x04, 0x1E, 0x44, 0x10, 0x51, 0x78, 0x74, 0x61, 0xE8, 0xC6,
|
||||
0x31, 0x70, 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53,
|
||||
0x78, 0x82, 0x87, 0x01, 0xF1, 0x83, 0x04, 0xF8, 0x3E, 0x07, 0x06, 0x36, 0x40, 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, 0x0F, 0x86,
|
||||
0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42,
|
||||
0x42, 0xC3, 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, 0xF9, 0x0A, 0x1C,
|
||||
0x18, 0x30, 0x61, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x61,
|
||||
0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x87,
|
||||
0x29, 0x70, 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
|
||||
0xA5, 0x99, 0x99, 0x99, 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6,
|
||||
0x1E, 0x00, 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1F, 0x00, 0x00,
|
||||
0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04,
|
||||
0x08, 0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, 0xC2, 0x42, 0x42, 0x64, 0x24, 0x24, 0x38, 0x18, 0x18, 0xC4, 0x28,
|
||||
0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, 0x42, 0x66, 0x24, 0x18, 0x18, 0x18, 0x24, 0x46, 0x42, 0xC3,
|
||||
0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24,
|
||||
0x89, 0x20, 0xE9, 0x24, 0x92, 0x49, 0x70, 0x46, 0xA9, 0x10, 0xFE, 0x40, 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, 0x84, 0x3D, 0x18,
|
||||
0xC6, 0x31, 0xF0, 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, 0x39, 0x38, 0x7F, 0x81, 0x13,
|
||||
0x80, 0x6B, 0xA4, 0x92, 0x40, 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, 0xBF, 0x80,
|
||||
0x45, 0x55, 0x57, 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, 0xFF, 0x80, 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, 0xF4, 0x63, 0x18,
|
||||
0xC6, 0x20, 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04,
|
||||
0xF2, 0x49, 0x20, 0x79, 0x24, 0x1C, 0x0B, 0x27, 0x80, 0x5D, 0x24, 0x93, 0x8C, 0x63, 0x18, 0xCF, 0xA0, 0x85, 0x24, 0x92, 0x30,
|
||||
0xC3, 0x00, 0x89, 0x2C, 0x96, 0x4A, 0xA5, 0x61, 0x30, 0x98, 0x49, 0x23, 0x08, 0x31, 0x2C, 0x80, 0x89, 0x24, 0x94, 0x50, 0xC2,
|
||||
0x08, 0x21, 0x00, 0x78, 0x44, 0x46, 0x23, 0xE0, 0x6A, 0xAA, 0xA9, 0xFF, 0xE0, 0x95, 0x55, 0x56, 0x66, 0x60};
|
||||
|
||||
const GFXglyph FreeSans6pt7bGlyphs[] PROGMEM = {{0, 0, 0, 3, 0, 1}, // 0x20 ' '
|
||||
{0, 2, 9, 4, 1, -8}, // 0x21 '!'
|
||||
{3, 4, 3, 4, 0, -8}, // 0x22 '"'
|
||||
{5, 7, 8, 7, 0, -7}, // 0x23 '#'
|
||||
{12, 6, 11, 7, 0, -9}, // 0x24 '$'
|
||||
{21, 10, 9, 11, 0, -8}, // 0x25 '%'
|
||||
{33, 7, 9, 8, 1, -8}, // 0x26 '&'
|
||||
{41, 1, 3, 2, 1, -8}, // 0x27 '''
|
||||
{42, 2, 11, 4, 1, -8}, // 0x28 '('
|
||||
{45, 3, 11, 4, 0, -8}, // 0x29 ')'
|
||||
{50, 4, 3, 5, 0, -8}, // 0x2A '*'
|
||||
{52, 5, 5, 7, 1, -4}, // 0x2B '+'
|
||||
{56, 1, 3, 3, 1, 0}, // 0x2C ','
|
||||
{57, 2, 1, 4, 1, -3}, // 0x2D '-'
|
||||
{58, 1, 1, 3, 1, 0}, // 0x2E '.'
|
||||
{59, 3, 9, 3, 0, -8}, // 0x2F '/'
|
||||
{63, 5, 9, 7, 1, -8}, // 0x30 '0'
|
||||
{69, 3, 9, 7, 1, -8}, // 0x31 '1'
|
||||
{73, 6, 9, 7, 0, -8}, // 0x32 '2'
|
||||
{80, 6, 9, 7, 0, -8}, // 0x33 '3'
|
||||
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
|
||||
{94, 6, 9, 7, 0, -8}, // 0x35 '5'
|
||||
{101, 5, 9, 7, 1, -8}, // 0x36 '6'
|
||||
{107, 5, 9, 7, 1, -8}, // 0x37 '7'
|
||||
{113, 6, 9, 7, 0, -8}, // 0x38 '8'
|
||||
{120, 6, 9, 7, 0, -8}, // 0x39 '9'
|
||||
{127, 1, 7, 3, 1, -6}, // 0x3A ':'
|
||||
{128, 1, 8, 3, 1, -5}, // 0x3B ';'
|
||||
{129, 5, 6, 7, 1, -5}, // 0x3C '<'
|
||||
{133, 5, 3, 7, 1, -3}, // 0x3D '='
|
||||
{135, 5, 6, 7, 1, -5}, // 0x3E '>'
|
||||
{139, 5, 9, 7, 1, -8}, // 0x3F '?'
|
||||
{145, 11, 11, 12, 0, -8}, // 0x40 '@'
|
||||
{161, 8, 9, 8, 0, -8}, // 0x41 'A'
|
||||
{170, 6, 9, 8, 1, -8}, // 0x42 'B'
|
||||
{177, 8, 9, 9, 0, -8}, // 0x43 'C'
|
||||
{186, 7, 9, 8, 1, -8}, // 0x44 'D'
|
||||
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
|
||||
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
|
||||
{208, 8, 9, 9, 0, -8}, // 0x47 'G'
|
||||
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
|
||||
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
|
||||
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
|
||||
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
|
||||
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
|
||||
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
|
||||
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
|
||||
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
|
||||
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
|
||||
{282, 9, 10, 9, 0, -8}, // 0x51 'Q'
|
||||
{294, 7, 9, 9, 1, -8}, // 0x52 'R'
|
||||
{302, 6, 9, 8, 1, -8}, // 0x53 'S'
|
||||
{309, 7, 9, 8, 0, -8}, // 0x54 'T'
|
||||
{317, 7, 9, 9, 1, -8}, // 0x55 'U'
|
||||
{325, 8, 9, 8, 0, -8}, // 0x56 'V'
|
||||
{334, 11, 9, 11, 0, -8}, // 0x57 'W'
|
||||
{347, 8, 9, 8, 0, -8}, // 0x58 'X'
|
||||
{356, 8, 9, 8, 0, -8}, // 0x59 'Y'
|
||||
{365, 7, 9, 7, 0, -8}, // 0x5A 'Z'
|
||||
{373, 2, 12, 3, 1, -8}, // 0x5B '['
|
||||
{376, 3, 9, 3, 0, -8}, // 0x5C '\'
|
||||
{380, 3, 12, 3, 0, -8}, // 0x5D ']'
|
||||
{385, 4, 5, 6, 1, -8}, // 0x5E '^'
|
||||
{388, 7, 1, 7, 0, 2}, // 0x5F '_'
|
||||
{389, 3, 1, 3, 0, -8}, // 0x60 '`'
|
||||
{390, 6, 7, 7, 0, -6}, // 0x61 'a'
|
||||
{396, 5, 9, 7, 1, -8}, // 0x62 'b'
|
||||
{402, 6, 7, 6, 0, -6}, // 0x63 'c'
|
||||
{408, 6, 9, 7, 0, -8}, // 0x64 'd'
|
||||
{415, 6, 7, 6, 0, -6}, // 0x65 'e'
|
||||
{421, 3, 9, 3, 0, -8}, // 0x66 'f'
|
||||
{425, 6, 10, 7, 0, -6}, // 0x67 'g'
|
||||
{433, 5, 9, 6, 1, -8}, // 0x68 'h'
|
||||
{439, 1, 9, 3, 1, -8}, // 0x69 'i'
|
||||
{441, 2, 12, 3, 0, -8}, // 0x6A 'j'
|
||||
{444, 5, 9, 6, 1, -8}, // 0x6B 'k'
|
||||
{450, 1, 9, 3, 1, -8}, // 0x6C 'l'
|
||||
{452, 8, 7, 10, 1, -6}, // 0x6D 'm'
|
||||
{459, 5, 7, 6, 1, -6}, // 0x6E 'n'
|
||||
{464, 6, 7, 6, 0, -6}, // 0x6F 'o'
|
||||
{470, 5, 9, 7, 1, -6}, // 0x70 'p'
|
||||
{476, 6, 9, 7, 0, -6}, // 0x71 'q'
|
||||
{483, 3, 7, 4, 1, -6}, // 0x72 'r'
|
||||
{486, 6, 7, 6, 0, -6}, // 0x73 's'
|
||||
{492, 3, 8, 3, 0, -7}, // 0x74 't'
|
||||
{495, 5, 7, 6, 1, -6}, // 0x75 'u'
|
||||
{500, 6, 7, 6, 0, -6}, // 0x76 'v'
|
||||
{506, 9, 7, 9, 0, -6}, // 0x77 'w'
|
||||
{514, 6, 7, 6, 0, -6}, // 0x78 'x'
|
||||
{520, 6, 10, 6, 0, -6}, // 0x79 'y'
|
||||
{528, 5, 7, 6, 0, -6}, // 0x7A 'z'
|
||||
{533, 2, 12, 4, 1, -8}, // 0x7B '{'
|
||||
{536, 1, 11, 3, 1, -8}, // 0x7C '|'
|
||||
{538, 2, 12, 4, 1, -8}, // 0x7D '}'
|
||||
{541, 6, 2, 6, 0, -4}}; // 0x7E '~'
|
||||
|
||||
const GFXfont FreeSans6pt7b PROGMEM = {(uint8_t *)FreeSans6pt7bBitmaps, (GFXglyph *)FreeSans6pt7bGlyphs, 0x20, 0x7E, 14};
|
||||
|
||||
// Approx. 1215 bytes
|
||||
302
src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h
Normal file
302
src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
|
||||
Uses Windows-1251 encoding to map translingual Cyrillic characters to range between (uint8_t)127 and (uint8_t)255
|
||||
https://en.wikipedia.org/wiki/Windows-1251
|
||||
|
||||
Cyrillic characters present to the firmware as UTF8.
|
||||
A NicheGraphics implementation needs to identify these, and substitute the appropriate Windows-1251 char value.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
const uint8_t FreeSans6pt8bCyrillicBitmaps[] PROGMEM = {
|
||||
0xFF, 0xA0, 0xC0, 0xFF, 0xA0, 0xC0, 0xB6, 0x80, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x31, 0x75, 0x54, 0x78, 0x79, 0x75,
|
||||
0x7C, 0x41, 0x00, 0x01, 0x1C, 0x49, 0x22, 0x50, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0xC0, 0x21, 0x44, 0x94, 0x62, 0x59, 0xE2,
|
||||
0xF4, 0xE0, 0x6A, 0xAA, 0x90, 0x48, 0x92, 0x49, 0x4A, 0x00, 0x5D, 0x40, 0x21, 0x09, 0xF2, 0x10, 0xE0, 0xC0, 0x80, 0x25, 0x25,
|
||||
0x24, 0x26, 0xA3, 0x18, 0xC6, 0x31, 0xF0, 0x27, 0x92, 0x49, 0x20, 0x11, 0xB4, 0x41, 0x0C, 0xC6, 0x10, 0xFC, 0x26, 0xA2, 0x13,
|
||||
0x04, 0x31, 0xF0, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0xFF, 0xE1, 0x4D, 0x84, 0x31, 0xF0, 0x26, 0xE3, 0x0F, 0x46, 0x31,
|
||||
0xF0, 0xFF, 0xC4, 0x22, 0x11, 0x08, 0x40, 0x11, 0xA4, 0x51, 0x39, 0x1C, 0x51, 0x78, 0x11, 0xA4, 0x71, 0x45, 0xF0, 0x51, 0x78,
|
||||
0xC0, 0x30, 0xC0, 0x36, 0x1F, 0x20, 0xE0, 0x80, 0xF8, 0x3E, 0xC1, 0xC2, 0xE8, 0x00, 0x74, 0x62, 0x11, 0x10, 0x80, 0x20, 0x0F,
|
||||
0x06, 0x18, 0x81, 0xA7, 0xD4, 0x93, 0x22, 0x64, 0x4A, 0x7E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x1C, 0x24, 0x24, 0x7E,
|
||||
0x42, 0x42, 0xC3, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xF9, 0x1A, 0x1C,
|
||||
0x18, 0x30, 0x60, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x3C, 0x46,
|
||||
0x82, 0x80, 0x8F, 0x81, 0x83, 0xC3, 0x7D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x86,
|
||||
0x31, 0x78, 0x87, 0x1A, 0x65, 0x8F, 0x1A, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
|
||||
0xA5, 0x99, 0x99, 0x99, 0x83, 0x87, 0x8D, 0x19, 0x32, 0x62, 0xC3, 0x86, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC2,
|
||||
0x3E, 0x00, 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x3F, 0x00, 0xFD,
|
||||
0x0E, 0x0C, 0x1F, 0xD0, 0xA0, 0xC1, 0x82, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08,
|
||||
0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0x7C, 0xC3, 0x42, 0x42, 0x26, 0x24, 0x24, 0x14, 0x18, 0x18, 0xC4, 0x28, 0xC5,
|
||||
0x39, 0xA5, 0x24, 0xA4, 0x52, 0x8C, 0x71, 0x8C, 0x30, 0x80, 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, 0xC3, 0x42, 0x26, 0x24,
|
||||
0x18, 0x18, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x10, 0x41, 0x06, 0x08, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24, 0x89, 0x20, 0xED,
|
||||
0xB6, 0xDB, 0x6D, 0xF0, 0x46, 0xAA, 0x90, 0xFC, 0x90, 0xFC, 0x4F, 0x98, 0xFC, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0xF0, 0x79, 0x18,
|
||||
0x20, 0x45, 0xE0, 0x04, 0x10, 0x5F, 0xC6, 0x18, 0x51, 0x7C, 0xFC, 0x7F, 0x08, 0xF8, 0x29, 0x74, 0x92, 0x40, 0x7D, 0x18, 0x61,
|
||||
0x45, 0xF0, 0x52, 0x30, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0x88, 0xDF, 0x80, 0x51, 0x55, 0x56, 0x84, 0x21, 0x2A, 0x72, 0x92, 0x98,
|
||||
0xFF, 0x80, 0xFF, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFC, 0x63, 0x18, 0xC4, 0x79, 0x18, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xFA,
|
||||
0x10, 0x80, 0x7D, 0x18, 0x61, 0x45, 0xF0, 0x41, 0x04, 0xF2, 0x49, 0x00, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0x4B, 0xA4, 0x93, 0x8C,
|
||||
0x63, 0x18, 0xFC, 0xCD, 0x24, 0x94, 0x30, 0xC0, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, 0x96, 0x66, 0x99, 0xCA, 0x52, 0x63, 0x18,
|
||||
0x84, 0x40, 0x78, 0xC4, 0x44, 0x7C, 0x6A, 0xAA, 0xA9, 0xFF, 0xF0, 0xC9, 0x24, 0x4A, 0x49, 0x40, 0xE8, 0xC0, 0xFE, 0x18, 0x61,
|
||||
0x86, 0x18, 0x61, 0xFC, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, 0x10, 0x8F, 0xE0, 0x82,
|
||||
0x08, 0x20, 0x82, 0x08, 0x00, 0x64, 0x0F, 0x88, 0x88, 0x80, 0x3D, 0x0C, 0x2E, 0xF9, 0x04, 0x0F, 0x7C, 0x08, 0x81, 0x10, 0x22,
|
||||
0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0,
|
||||
0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0x83,
|
||||
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, 0x78, 0x24, 0x13,
|
||||
0xC9, 0x14, 0x8E, 0x7C, 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, 0x60, 0x9A, 0xCC, 0xA9, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51,
|
||||
0x44, 0x8C, 0x63, 0x18, 0xFC, 0x80, 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, 0x70, 0x22, 0x95, 0xA8, 0xC4,
|
||||
0x23, 0x10, 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, 0x28, 0x0F, 0xE0, 0x82, 0x0F,
|
||||
0xE0, 0x82, 0x0F, 0xC0, 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, 0x51, 0x55, 0x56, 0xA1, 0x24, 0x92, 0x49, 0x00, 0xFF,
|
||||
0x80, 0xDF, 0x80, 0x27, 0xC9, 0x24, 0x8A, 0x28, 0xA2, 0x8B, 0xF8, 0x20, 0x80, 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, 0x88,
|
||||
0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x89, 0x80, 0x79, 0x1F, 0x30, 0x45, 0xE0, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61,
|
||||
0x7C, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0xB4, 0x24, 0x92, 0x40, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, 0xFE, 0x08,
|
||||
0x20, 0xFE, 0x18, 0x61, 0xFC, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, 0x1F, 0x08,
|
||||
0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0x88, 0xA4, 0x9A,
|
||||
0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68,
|
||||
0xE1, 0xC2, 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, 0x3E, 0x44,
|
||||
0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60,
|
||||
0xC1, 0x82, 0x3C, 0x46, 0x83, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, 0xFA, 0x18,
|
||||
0x61, 0xFE, 0x08, 0x20, 0x80, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10,
|
||||
0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, 0x08, 0x1E, 0x32, 0xD1, 0x38, 0x8C, 0x4F, 0x2C, 0xFC, 0x08, 0x00, 0x87, 0x34,
|
||||
0x8C, 0x30, 0xC4, 0xB3, 0x84, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, 0x8E, 0x38, 0xE3, 0x8D, 0xF0,
|
||||
0xC3, 0x0C, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF,
|
||||
0x80, 0x40, 0x20, 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC,
|
||||
0x2E, 0x17, 0x0B, 0xF9, 0x80, 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, 0x87,
|
||||
0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, 0x79, 0x11, 0xD9,
|
||||
0xCD, 0xD0, 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, 0xF4, 0xBD, 0x29, 0xF8, 0xF8, 0x88, 0x88, 0x3C, 0x48, 0x91, 0x22, 0x5F,
|
||||
0xE0, 0x80, 0x79, 0x1F, 0xF0, 0x45, 0xE0, 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, 0x78, 0x23, 0x82, 0xCD, 0xE0, 0x9C, 0xEB, 0x5C,
|
||||
0xC4, 0x70, 0x27, 0x3A, 0xD7, 0x31, 0x9A, 0xCC, 0xA9, 0x7A, 0x52, 0x94, 0xE4, 0x8F, 0x3D, 0x6D, 0xA6, 0x90, 0x8C, 0x7F, 0x18,
|
||||
0xC4, 0x79, 0x1C, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xC4, 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, 0x79, 0x1C, 0x30, 0x45, 0xE0,
|
||||
0xF9, 0x08, 0x42, 0x10, 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04,
|
||||
0x01, 0x00, 0x40, 0x4B, 0x8C, 0x65, 0xE4, 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, 0x99, 0x97, 0x11, 0x96, 0x59, 0x65, 0x97, 0xF0,
|
||||
0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, 0x86, 0x1F, 0x63, 0x8F, 0xD0, 0x84, 0x3D, 0x18,
|
||||
0xF8, 0xF4, 0xDE, 0x19, 0xF8, 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, 0xFC, 0x7E, 0xD4, 0xC4,
|
||||
};
|
||||
|
||||
const GFXglyph FreeSans6pt8bCyrillicGlyphs[] PROGMEM = {
|
||||
{0, 0, 0, 3, 0, 0}, // 0x20 ' '
|
||||
{3, 2, 9, 3, 1, -8}, // 0x21 '!'
|
||||
{6, 3, 3, 4, 1, -8}, // 0x22 '"'
|
||||
{8, 7, 8, 7, 0, -7}, // 0x23 '#'
|
||||
{15, 6, 11, 7, 0, -8}, // 0x24 '$'
|
||||
{24, 10, 9, 11, 0, -8}, // 0x25 '%'
|
||||
{36, 6, 9, 8, 1, -8}, // 0x26 '&'
|
||||
{43, 1, 3, 2, 1, -8}, // 0x27 '''
|
||||
{44, 2, 10, 4, 1, -7}, // 0x28 '('
|
||||
{47, 3, 11, 4, 0, -7}, // 0x29 ')'
|
||||
{52, 3, 4, 5, 1, -8}, // 0x2A '*'
|
||||
{54, 5, 6, 7, 1, -5}, // 0x2B '+'
|
||||
{58, 1, 3, 3, 1, 0}, // 0x2C ','
|
||||
{59, 2, 1, 4, 1, -3}, // 0x2D '-'
|
||||
{60, 1, 1, 3, 1, 0}, // 0x2E '.'
|
||||
{61, 3, 8, 3, 0, -7}, // 0x2F '/'
|
||||
{64, 5, 9, 7, 1, -8}, // 0x30 '0'
|
||||
{70, 3, 9, 7, 1, -8}, // 0x31 '1'
|
||||
{74, 6, 9, 7, 0, -8}, // 0x32 '2'
|
||||
{81, 5, 9, 7, 1, -8}, // 0x33 '3'
|
||||
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
|
||||
{94, 5, 9, 7, 1, -8}, // 0x35 '5'
|
||||
{100, 5, 9, 7, 1, -8}, // 0x36 '6'
|
||||
{106, 5, 9, 7, 1, -8}, // 0x37 '7'
|
||||
{112, 6, 9, 7, 0, -8}, // 0x38 '8'
|
||||
{119, 6, 9, 7, 0, -8}, // 0x39 '9'
|
||||
{126, 2, 6, 3, 1, -5}, // 0x3A ':'
|
||||
{128, 2, 8, 3, 1, -5}, // 0x3B ';'
|
||||
{130, 5, 5, 7, 1, -4}, // 0x3C '<'
|
||||
{134, 5, 3, 7, 1, -3}, // 0x3D '='
|
||||
{136, 5, 5, 7, 1, -4}, // 0x3E '>'
|
||||
{140, 5, 9, 7, 1, -8}, // 0x3F '?'
|
||||
{146, 11, 11, 12, 0, -8}, // 0x40 '@'
|
||||
{162, 8, 9, 8, 0, -8}, // 0x41 'A'
|
||||
{171, 6, 9, 8, 1, -8}, // 0x42 'B'
|
||||
{178, 7, 9, 9, 1, -8}, // 0x43 'C'
|
||||
{186, 7, 9, 9, 1, -8}, // 0x44 'D'
|
||||
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
|
||||
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
|
||||
{208, 8, 9, 9, 1, -8}, // 0x47 'G'
|
||||
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
|
||||
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
|
||||
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
|
||||
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
|
||||
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
|
||||
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
|
||||
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
|
||||
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
|
||||
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
|
||||
{282, 9, 9, 9, 0, -8}, // 0x51 'Q'
|
||||
{293, 7, 9, 9, 1, -8}, // 0x52 'R'
|
||||
{301, 6, 9, 8, 1, -8}, // 0x53 'S'
|
||||
{308, 7, 9, 7, 0, -8}, // 0x54 'T'
|
||||
{316, 7, 9, 9, 1, -8}, // 0x55 'U'
|
||||
{324, 8, 9, 8, 0, -8}, // 0x56 'V'
|
||||
{333, 11, 9, 11, 0, -8}, // 0x57 'W'
|
||||
{346, 6, 9, 8, 1, -8}, // 0x58 'X'
|
||||
{353, 8, 9, 8, 0, -8}, // 0x59 'Y'
|
||||
{362, 7, 9, 7, 0, -8}, // 0x5A 'Z'
|
||||
{370, 2, 12, 3, 1, -8}, // 0x5B '['
|
||||
{373, 3, 9, 3, 0, -8}, // 0x5C '\'
|
||||
{377, 3, 12, 3, 0, -8}, // 0x5D ']'
|
||||
{382, 4, 5, 6, 1, -8}, // 0x5E '^'
|
||||
{385, 6, 1, 7, 0, 2}, // 0x5F '_'
|
||||
{386, 2, 2, 4, 1, -8}, // 0x60 '`'
|
||||
{387, 5, 6, 7, 1, -5}, // 0x61 'a'
|
||||
{391, 5, 9, 7, 1, -8}, // 0x62 'b'
|
||||
{397, 6, 6, 6, 0, -5}, // 0x63 'c'
|
||||
{402, 6, 9, 7, 0, -8}, // 0x64 'd'
|
||||
{409, 5, 6, 7, 1, -5}, // 0x65 'e'
|
||||
{413, 3, 9, 3, 0, -8}, // 0x66 'f'
|
||||
{417, 6, 9, 7, 0, -5}, // 0x67 'g'
|
||||
{424, 5, 9, 7, 1, -8}, // 0x68 'h'
|
||||
{430, 1, 9, 3, 1, -8}, // 0x69 'i'
|
||||
{432, 2, 12, 3, 0, -8}, // 0x6A 'j'
|
||||
{435, 5, 9, 6, 1, -8}, // 0x6B 'k'
|
||||
{441, 1, 9, 3, 1, -8}, // 0x6C 'l'
|
||||
{443, 8, 6, 10, 1, -5}, // 0x6D 'm'
|
||||
{449, 5, 6, 7, 1, -5}, // 0x6E 'n'
|
||||
{453, 6, 6, 7, 0, -5}, // 0x6F 'o'
|
||||
{458, 5, 9, 7, 1, -5}, // 0x70 'p'
|
||||
{464, 6, 9, 7, 0, -5}, // 0x71 'q'
|
||||
{471, 3, 6, 4, 1, -5}, // 0x72 'r'
|
||||
{474, 6, 6, 6, 0, -5}, // 0x73 's'
|
||||
{479, 3, 8, 3, 0, -7}, // 0x74 't'
|
||||
{482, 5, 6, 7, 1, -5}, // 0x75 'u'
|
||||
{486, 6, 6, 6, 0, -5}, // 0x76 'v'
|
||||
{491, 8, 6, 9, 0, -5}, // 0x77 'w'
|
||||
{497, 4, 6, 6, 1, -5}, // 0x78 'x'
|
||||
{500, 5, 9, 6, 0, -5}, // 0x79 'y'
|
||||
{506, 5, 6, 6, 0, -5}, // 0x7A 'z'
|
||||
{510, 2, 12, 4, 1, -8}, // 0x7B '{'
|
||||
{513, 1, 12, 3, 1, -8}, // 0x7C '|'
|
||||
{515, 3, 12, 4, 0, -8}, // 0x7D '}'
|
||||
{520, 5, 2, 7, 1, -4}, // 0x7E '~'
|
||||
{522, 6, 9, 8, 1, -8}, //
|
||||
{529, 9, 11, 9, 0, -8}, //
|
||||
{542, 6, 11, 7, 1, -10}, //
|
||||
{551, 0, 0, 8, 0, 0}, //
|
||||
{551, 4, 9, 5, 1, -8}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 6, 8, 8, 1, -7}, //
|
||||
{562, 0, 0, 8, 0, 0}, //
|
||||
{562, 11, 9, 13, 1, -8}, //
|
||||
{575, 0, 0, 8, 0, 0}, //
|
||||
{575, 11, 9, 12, 1, -8}, //
|
||||
{588, 6, 11, 8, 1, -10}, //
|
||||
{597, 9, 9, 9, 0, -8}, //
|
||||
{608, 7, 11, 9, 1, -8}, //
|
||||
{618, 6, 11, 7, 0, -8}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 9, 6, 10, 0, -5}, //
|
||||
{634, 0, 0, 8, 0, 0}, //
|
||||
{634, 9, 6, 10, 1, -5}, //
|
||||
{641, 4, 8, 6, 1, -7}, //
|
||||
{645, 6, 9, 7, 0, -8}, //
|
||||
{652, 5, 7, 7, 1, -5}, //
|
||||
{657, 0, 0, 8, 0, 0}, //
|
||||
{657, 7, 11, 7, 0, -10}, //
|
||||
{667, 5, 11, 6, 0, -7}, //
|
||||
{674, 5, 9, 6, 0, -8}, //
|
||||
{680, 0, 0, 8, 0, 0}, //
|
||||
{680, 6, 10, 7, 1, -9}, //
|
||||
{688, 0, 0, 8, 0, 0}, //
|
||||
{688, 0, 0, 8, 0, 0}, //
|
||||
{688, 6, 11, 8, 1, -10}, //
|
||||
{697, 7, 9, 9, 1, -8}, //
|
||||
{705, 0, 0, 8, 0, 0}, //
|
||||
{705, 0, 0, 8, 0, 0}, //
|
||||
{705, 2, 12, 3, 0, -8}, //
|
||||
{708, 0, 0, 8, 0, 0}, //
|
||||
{708, 0, 0, 8, 0, 0}, //
|
||||
{708, 3, 11, 3, 0, -10}, //
|
||||
{713, 0, 0, 8, 0, 0}, //
|
||||
{713, 0, 0, 8, 0, 0}, //
|
||||
{713, 1, 9, 3, 1, -8}, //
|
||||
{715, 1, 9, 3, 1, -8}, //
|
||||
{717, 3, 8, 5, 1, -7}, //
|
||||
{720, 6, 9, 7, 1, -5}, //
|
||||
{727, 0, 0, 8, 0, 0}, //
|
||||
{727, 0, 0, 8, 0, 0}, //
|
||||
{727, 6, 9, 7, 0, -8}, //
|
||||
{734, 9, 9, 11, 1, -8}, //
|
||||
{745, 6, 6, 6, 0, -5}, //
|
||||
{750, 0, 0, 8, 0, 0}, //
|
||||
{750, 0, 0, 8, 0, 0}, //
|
||||
{750, 6, 9, 8, 1, -8}, //
|
||||
{757, 6, 6, 6, 0, -5}, //
|
||||
{762, 3, 9, 3, 0, -8}, //
|
||||
{766, 8, 9, 8, 0, -8}, //
|
||||
{775, 6, 9, 8, 1, -8}, //
|
||||
{782, 6, 9, 8, 1, -8}, //
|
||||
{789, 6, 9, 7, 1, -8}, //
|
||||
{796, 9, 11, 10, 0, -8}, //
|
||||
{809, 6, 9, 8, 1, -8}, //
|
||||
{816, 9, 9, 11, 1, -8}, //
|
||||
{827, 6, 9, 8, 1, -8}, //
|
||||
{834, 7, 9, 9, 1, -8}, //
|
||||
{842, 7, 11, 9, 1, -10}, //
|
||||
{852, 6, 9, 8, 1, -8}, //
|
||||
{859, 7, 9, 8, 0, -8}, //
|
||||
{867, 8, 9, 10, 1, -8}, //
|
||||
{876, 7, 9, 9, 1, -8}, //
|
||||
{884, 8, 9, 10, 1, -8}, //
|
||||
{893, 7, 9, 9, 1, -8}, //
|
||||
{901, 6, 9, 8, 1, -8}, //
|
||||
{908, 7, 9, 9, 1, -8}, //
|
||||
{916, 7, 9, 7, 0, -8}, //
|
||||
{924, 7, 9, 7, 0, -8}, //
|
||||
{932, 9, 9, 10, 1, -8}, //
|
||||
{943, 6, 9, 8, 1, -8}, //
|
||||
{950, 8, 11, 9, 1, -8}, //
|
||||
{961, 6, 9, 8, 1, -8}, //
|
||||
{968, 8, 9, 10, 1, -8}, //
|
||||
{977, 9, 11, 10, 1, -8}, //
|
||||
{990, 10, 9, 10, 0, -8}, //
|
||||
{1002, 9, 9, 10, 1, -8}, //
|
||||
{1013, 6, 9, 8, 1, -8}, //
|
||||
{1020, 7, 9, 9, 1, -8}, //
|
||||
{1028, 10, 9, 12, 1, -8}, //
|
||||
{1040, 6, 9, 8, 1, -8}, //
|
||||
{1047, 6, 6, 7, 0, -5}, //
|
||||
{1052, 6, 9, 7, 0, -8}, //
|
||||
{1059, 5, 6, 6, 1, -5}, //
|
||||
{1063, 4, 6, 5, 1, -5}, //
|
||||
{1066, 7, 7, 7, 0, -5}, //
|
||||
{1073, 6, 6, 7, 0, -5}, //
|
||||
{1078, 8, 6, 9, 1, -5}, //
|
||||
{1084, 6, 6, 6, 0, -5}, //
|
||||
{1089, 5, 6, 7, 1, -5}, //
|
||||
{1093, 5, 8, 7, 1, -7}, //
|
||||
{1098, 4, 6, 6, 1, -5}, //
|
||||
{1101, 5, 6, 6, 0, -5}, //
|
||||
{1105, 6, 6, 7, 1, -5}, //
|
||||
{1110, 5, 6, 7, 1, -5}, //
|
||||
{1114, 6, 6, 7, 0, -5}, //
|
||||
{1119, 5, 6, 7, 1, -5}, //
|
||||
{1123, 5, 9, 7, 1, -5}, //
|
||||
{1129, 6, 6, 6, 0, -5}, //
|
||||
{1134, 5, 6, 5, 0, -5}, //
|
||||
{1138, 5, 9, 6, 0, -5}, //
|
||||
{1144, 10, 11, 10, 0, -7}, //
|
||||
{1158, 5, 6, 6, 0, -5}, //
|
||||
{1162, 6, 7, 7, 1, -5}, //
|
||||
{1168, 4, 6, 6, 1, -5}, //
|
||||
{1171, 6, 6, 8, 1, -5}, //
|
||||
{1176, 7, 7, 9, 1, -5}, //
|
||||
{1183, 7, 6, 8, 0, -5}, //
|
||||
{1189, 6, 6, 8, 1, -5}, //
|
||||
{1194, 5, 6, 6, 1, -5}, //
|
||||
{1198, 5, 6, 6, 1, -5}, //
|
||||
{1202, 8, 6, 9, 1, -5}, //
|
||||
{1208, 5, 6, 7, 1, -5} //
|
||||
};
|
||||
|
||||
const GFXfont FreeSans6pt8bCyrillic PROGMEM = {(uint8_t *)FreeSans6pt8bCyrillicBitmaps, (GFXglyph *)FreeSans6pt8bCyrillicGlyphs,
|
||||
0x20, 0xFF, 16};
|
||||
4
src/graphics/niche/Fonts/README.md
Normal file
4
src/graphics/niche/Fonts/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# NicheGraphics - Fonts
|
||||
|
||||
A common area to store fonts which might be reused by different Niche Graphics UIs
|
||||
In future, we may want to separate these by library (AdafruitGFX, u8g2, etc)
|
||||
948
src/graphics/niche/InkHUD/Applet.cpp
Normal file
948
src/graphics/niche/InkHUD/Applet.cpp
Normal file
@@ -0,0 +1,948 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
|
||||
#include "main.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
|
||||
|
||||
inkhud = InkHUD::getInstance();
|
||||
settings = &inkhud->persistence->settings;
|
||||
latestMessage = &inkhud->persistence->latestMessage;
|
||||
}
|
||||
|
||||
// Draw a single pixel
|
||||
// The raw pixel output generated by AdafruitGFX drawing all passes through here
|
||||
// Hand off to the applet's tile, which will in-turn pass to the renderer
|
||||
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);
|
||||
}
|
||||
|
||||
// Link our applet to a tile
|
||||
// This can only be called by Tile::assignApplet
|
||||
// The tile determines the applets dimensions
|
||||
// Pixel output is passed to tile during render()
|
||||
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;
|
||||
}
|
||||
|
||||
// The tile to which our applet is assigned
|
||||
InkHUD::Tile *InkHUD::Applet::getTile()
|
||||
{
|
||||
return assignedTile;
|
||||
}
|
||||
|
||||
// Draw the applet
|
||||
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
|
||||
|
||||
// WindowManager::update has now consumed the info about our update request
|
||||
// Clear everything for future requests
|
||||
wantRender = false; // Flag set by requestUpdate
|
||||
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
|
||||
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
|
||||
|
||||
updateDimensions();
|
||||
resetDrawingSpace();
|
||||
onRender(); // Derived applet's drawing takes place here
|
||||
|
||||
// Handle "Tile Highlighting"
|
||||
// Some devices may use an auxiliary button to switch between tiles
|
||||
// When this happens, we temporarily highlight the newly focused tile with a border
|
||||
|
||||
// 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
|
||||
// Used by WindowManager::update
|
||||
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
|
||||
// Used by WindowManager::update
|
||||
bool InkHUD::Applet::wantsToAutoshow()
|
||||
{
|
||||
return wantAutoshow;
|
||||
}
|
||||
|
||||
// Which technique would this applet prefer that the display use to change the image?
|
||||
// Used by WindowManager::update
|
||||
Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
||||
{
|
||||
return wantUpdateType;
|
||||
}
|
||||
|
||||
// Get size of the applet's drawing space from its tile
|
||||
// Performed immediately before derived applet's drawing code runs
|
||||
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(fontSmall);
|
||||
}
|
||||
|
||||
// Tell InkHUD::Renderer 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 renderer 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 (foreground)
|
||||
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
|
||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
|
||||
{
|
||||
wantRender = true;
|
||||
wantUpdateType = type;
|
||||
inkhud->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 unexpectedly renderer at any time by other InkHUD components
|
||||
// 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 stops 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 Renderer 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 (exception: some system applets)
|
||||
void InkHUD::Applet::sendToBackground()
|
||||
{
|
||||
if (foreground) {
|
||||
foreground = false;
|
||||
onBackground(); // Run derived applet class' handler
|
||||
}
|
||||
}
|
||||
|
||||
// Is the applet currently displayed on a tile
|
||||
// Note: in some uncommon situations, an applet may be "foreground", and still not visible.
|
||||
// This can occur when a system applet is covering the screen (e.g. during BLE pairing)
|
||||
// This is not our applets responsibility to handle,
|
||||
// as in those situations, the system applet will have "locked" rendering
|
||||
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 glyph 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, rather than sizing to text (getTextBounds)
|
||||
|
||||
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 AdafruitGFX 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 AdafruitGFX font, with some precalculated dimension data
|
||||
InkHUD::AppletFont InkHUD::Applet::getFont()
|
||||
{
|
||||
return currentFont;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Wrapper 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::Applet::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);
|
||||
}
|
||||
|
||||
// Print text, with word wrapping
|
||||
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
|
||||
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 glyph 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 = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 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)
|
||||
return "";
|
||||
|
||||
// Times are invalid: argument time is significantly ahead of RTC
|
||||
// Don't give any human readable string
|
||||
if (daysAgo < -2)
|
||||
return "";
|
||||
|
||||
// Times are probably invalid: more than 6 months ago
|
||||
if (daysAgo > 6 * 30)
|
||||
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;
|
||||
|
||||
// Imperial
|
||||
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;
|
||||
}
|
||||
|
||||
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
|
||||
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(NicheGraphics::InkHUD::Notification &n)
|
||||
{
|
||||
// By default, no objection
|
||||
return true;
|
||||
}
|
||||
|
||||
// Draw the standard header, used by most Applets
|
||||
/*
|
||||
┌───────────────────────────────┐
|
||||
│ Applet::name here │
|
||||
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└───────────────────────────────┘
|
||||
*/
|
||||
void InkHUD::Applet::drawHeader(std::string text)
|
||||
{
|
||||
// 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)
|
||||
/*
|
||||
+-----------------------------+
|
||||
--| a2 b2/c1 |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
--| a1 b1 c2 |
|
||||
+-----------------------------+
|
||||
| | | |
|
||||
*/
|
||||
|
||||
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 angle of the path(s)
|
||||
// Used to thicken the single pixel paths
|
||||
/*
|
||||
+-------------------------------+
|
||||
| a2 |
|
||||
| -| |
|
||||
| -/ | |
|
||||
| -/ | |
|
||||
| -/# | |
|
||||
| -/ # | |
|
||||
| / # | |
|
||||
| a1---------- |
|
||||
+-------------------------------+
|
||||
*/
|
||||
|
||||
Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)};
|
||||
float angle = tanh((float)deltaA.y / deltaA.x);
|
||||
|
||||
// Distance (at right angle to 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
|
||||
/*
|
||||
| a2
|
||||
| .
|
||||
| ..
|
||||
| aq1 ..
|
||||
| # ..
|
||||
| | # ..
|
||||
|fromPath.y | # ..
|
||||
| +----a1
|
||||
|
|
||||
| fromPath.x
|
||||
+--------------------------------
|
||||
*/
|
||||
|
||||
Distance fromPath;
|
||||
fromPath.x = cos(radians(90) - angle) * logoTh * 0.5;
|
||||
fromPath.y = sin(radians(90) - angle) * logoTh * 0.5;
|
||||
|
||||
// Make the paths thick
|
||||
// Corner points for the rectangles (quads):
|
||||
/*
|
||||
|
||||
aq2
|
||||
a2
|
||||
/ aq3
|
||||
/
|
||||
/
|
||||
aq1 /
|
||||
a1
|
||||
aq3
|
||||
*/
|
||||
|
||||
// Filled as two triangles per quad:
|
||||
/*
|
||||
aq2 #
|
||||
# ###
|
||||
## # aq3
|
||||
## ### -
|
||||
## #### -/
|
||||
## ### -/
|
||||
## #### -/
|
||||
aq1 ## -/
|
||||
--- -/
|
||||
\---aq4
|
||||
*/
|
||||
|
||||
// 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 thick: 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
|
||||
/*
|
||||
b2 / c1
|
||||
####
|
||||
## ##
|
||||
/ \
|
||||
/ \/ \
|
||||
/ /\ \
|
||||
/ / \ \
|
||||
|
||||
*/
|
||||
|
||||
// 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 re-deriving it
|
||||
int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2));
|
||||
fillCircle(b2.x, b2.y, capRad, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
172
src/graphics/niche/InkHUD/Applet.h
Normal file
172
src/graphics/niche/InkHUD/Applet.h
Normal file
@@ -0,0 +1,172 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Base class for InkHUD applets
|
||||
Must be overriden
|
||||
|
||||
An applet is one "program" which may show info on the display.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <GFX.h> // GFXRoot drawing lib
|
||||
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
#include "./AppletFont.h"
|
||||
#include "./Applets/System/Notification/Notification.h" // The notification object, not the applet
|
||||
#include "./InkHUD.h"
|
||||
#include "./Persistence.h"
|
||||
#include "./Tile.h"
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
using NicheGraphics::Drivers::EInk;
|
||||
using std::to_string;
|
||||
|
||||
class Applet : public GFX
|
||||
{
|
||||
public:
|
||||
// Which edge Applet::printAt will place on the Y parameter
|
||||
enum VerticalAlignment : uint8_t {
|
||||
TOP,
|
||||
MIDDLE,
|
||||
BOTTOM,
|
||||
};
|
||||
|
||||
// Which edge Applet::printAt will place on the X parameter
|
||||
enum HorizontalAlignment : uint8_t {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
CENTER,
|
||||
};
|
||||
|
||||
// An easy-to-understand interpretation of SNR and RSSI
|
||||
// Calculate with Applet::getSignalStrength
|
||||
enum SignalStrength : int8_t {
|
||||
SIGNAL_UNKNOWN = -1,
|
||||
SIGNAL_NONE,
|
||||
SIGNAL_BAD,
|
||||
SIGNAL_FAIR,
|
||||
SIGNAL_GOOD,
|
||||
};
|
||||
|
||||
Applet();
|
||||
|
||||
void setTile(Tile *t); // Should only be called via Tile::setApplet
|
||||
Tile *getTile(); // Tile with which this applet is linked
|
||||
|
||||
// Rendering
|
||||
|
||||
void render(); // Draw the applet
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||
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
|
||||
|
||||
// State of the applet
|
||||
|
||||
void activate(); // Begin running
|
||||
void deactivate(); // Stop running
|
||||
void bringToForeground(); // Show
|
||||
void sendToBackground(); // Hide
|
||||
bool isActive();
|
||||
bool isForeground();
|
||||
|
||||
// Event handlers
|
||||
|
||||
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() {} // (System Applets only)
|
||||
virtual void onButtonLongPress() {} // (System Applets only)
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
static uint16_t getHeaderHeight(); // How tall the "standard" applet header is
|
||||
|
||||
static AppletFont fontSmall, fontLarge; // The general purpose fonts, used by all applets
|
||||
|
||||
const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet
|
||||
|
||||
protected:
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
||||
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
|
||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
|
||||
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()
|
||||
|
||||
// Text
|
||||
|
||||
void setFont(AppletFont f);
|
||||
AppletFont getFont();
|
||||
uint16_t getTextWidth(std::string text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
|
||||
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); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
|
||||
|
||||
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
|
||||
|
||||
// Meshtastic Logo
|
||||
|
||||
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); // Style as !0123abdc
|
||||
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
|
||||
|
||||
// Convenient references
|
||||
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
Persistence::LatestMessage *latestMessage = nullptr;
|
||||
|
||||
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 = 0;
|
||||
int16_t cropTop = 0;
|
||||
uint16_t cropWidth = 0;
|
||||
uint16_t cropHeight = 0;
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
221
src/graphics/niche/InkHUD/AppletFont.cpp
Normal file
221
src/graphics/niche/InkHUD/AppletFont.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#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 are 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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
▲ ##### # ▲
|
||||
│ # # │
|
||||
lineHeight │ ### # │
|
||||
│ # # # # │ heightAboveCursor
|
||||
│ # # # # │
|
||||
│ # # #### │
|
||||
│ -----------------#----
|
||||
│ # │ heightBelowCursor
|
||||
▼ ### ▼
|
||||
*/
|
||||
|
||||
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 substitutions 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 Substitution::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, suitable for several languages which use the Cyrillic script
|
||||
void InkHUD::AppletFont::addSubstitutionsWin1251()
|
||||
{
|
||||
addSubstitution("Ђ", "\x80");
|
||||
addSubstitution("Ѓ", "\x81");
|
||||
addSubstitution("ѓ", "\x83");
|
||||
addSubstitution("€", "\x88");
|
||||
addSubstitution("Љ", "\x8A");
|
||||
addSubstitution("Њ", "\x8C");
|
||||
addSubstitution("Ќ", "\x8D");
|
||||
addSubstitution("Ћ", "\x8E");
|
||||
addSubstitution("Џ", "\x8F");
|
||||
|
||||
addSubstitution("ђ", "\x90");
|
||||
addSubstitution("љ", "\x9A");
|
||||
addSubstitution("њ", "\x9C");
|
||||
addSubstitution("ќ", "\x9D");
|
||||
addSubstitution("ћ", "\x9E");
|
||||
addSubstitution("џ", "\x9F");
|
||||
|
||||
addSubstitution("Ў", "\xA1");
|
||||
addSubstitution("ў", "\xA2");
|
||||
addSubstitution("Ј", "\xA3");
|
||||
addSubstitution("Ґ", "\xA5");
|
||||
addSubstitution("Ё", "\xA8");
|
||||
addSubstitution("Є", "\xAA");
|
||||
addSubstitution("Ї", "\xAF");
|
||||
|
||||
addSubstitution("І", "\xB2");
|
||||
addSubstitution("і", "\xB3");
|
||||
addSubstitution("ґ", "\xB4");
|
||||
addSubstitution("ё", "\xB8");
|
||||
addSubstitution("№", "\xB9");
|
||||
addSubstitution("є", "\xBA");
|
||||
addSubstitution("ј", "\xBC");
|
||||
addSubstitution("Ѕ", "\xBD");
|
||||
addSubstitution("ѕ", "\xBE");
|
||||
addSubstitution("ї", "\xBF");
|
||||
|
||||
addSubstitution("А", "\xC0");
|
||||
addSubstitution("Б", "\xC1");
|
||||
addSubstitution("В", "\xC2");
|
||||
addSubstitution("Г", "\xC3");
|
||||
addSubstitution("Д", "\xC4");
|
||||
addSubstitution("Е", "\xC5");
|
||||
addSubstitution("Ж", "\xC6");
|
||||
addSubstitution("З", "\xC7");
|
||||
addSubstitution("И", "\xC8");
|
||||
addSubstitution("Й", "\xC9");
|
||||
addSubstitution("К", "\xCA");
|
||||
addSubstitution("Л", "\xCB");
|
||||
addSubstitution("М", "\xCC");
|
||||
addSubstitution("Н", "\xCD");
|
||||
addSubstitution("О", "\xCE");
|
||||
addSubstitution("П", "\xCF");
|
||||
|
||||
addSubstitution("Р", "\xD0");
|
||||
addSubstitution("С", "\xD1");
|
||||
addSubstitution("Т", "\xD2");
|
||||
addSubstitution("У", "\xD3");
|
||||
addSubstitution("Ф", "\xD4");
|
||||
addSubstitution("Х", "\xD5");
|
||||
addSubstitution("Ц", "\xD6");
|
||||
addSubstitution("Ч", "\xD7");
|
||||
addSubstitution("Ш", "\xD8");
|
||||
addSubstitution("Щ", "\xD9");
|
||||
addSubstitution("Ъ", "\xDA");
|
||||
addSubstitution("Ы", "\xDB");
|
||||
addSubstitution("Ь", "\xDC");
|
||||
addSubstitution("Э", "\xDD");
|
||||
addSubstitution("Ю", "\xDE");
|
||||
addSubstitution("Я", "\xDF");
|
||||
|
||||
addSubstitution("а", "\xE0");
|
||||
addSubstitution("б", "\xE1");
|
||||
addSubstitution("в", "\xE2");
|
||||
addSubstitution("г", "\xE3");
|
||||
addSubstitution("д", "\xE4");
|
||||
addSubstitution("е", "\xE5");
|
||||
addSubstitution("ж", "\xE6");
|
||||
addSubstitution("з", "\xE7");
|
||||
addSubstitution("и", "\xE8");
|
||||
addSubstitution("й", "\xE9");
|
||||
addSubstitution("к", "\xEA");
|
||||
addSubstitution("л", "\xEB");
|
||||
addSubstitution("м", "\xEC");
|
||||
addSubstitution("н", "\xED");
|
||||
addSubstitution("о", "\xEE");
|
||||
addSubstitution("п", "\xEF");
|
||||
|
||||
addSubstitution("р", "\xF0");
|
||||
addSubstitution("с", "\xF1");
|
||||
addSubstitution("т", "\xF2");
|
||||
addSubstitution("у", "\xF3");
|
||||
addSubstitution("ф", "\xF4");
|
||||
addSubstitution("х", "\xF5");
|
||||
addSubstitution("ц", "\xF6");
|
||||
addSubstitution("ч", "\xF7");
|
||||
addSubstitution("ш", "\xF8");
|
||||
addSubstitution("щ", "\xF9");
|
||||
addSubstitution("ъ", "\xFA");
|
||||
addSubstitution("ы", "\xFB");
|
||||
addSubstitution("ь", "\xFC");
|
||||
addSubstitution("э", "\xFD");
|
||||
addSubstitution("ю", "\xFE");
|
||||
addSubstitution("я", "\xFF");
|
||||
}
|
||||
|
||||
#endif
|
||||
59
src/graphics/niche/InkHUD/AppletFont.h
Normal file
59
src/graphics/niche/InkHUD/AppletFont.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Wrapper class for an AdafruitGFX font
|
||||
Pre-calculates some font dimension info which InkHUD uses repeatedly
|
||||
|
||||
Also contains an optional set of "substitutions".
|
||||
These can be used to detect special UTF8 chars, and replace occurrences with a remapped char val to suit a custom font
|
||||
These can also be used to swap UTF8 chars for a suitable ASCII substitution (e.g. German ö -> oe, etc)
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <GFX.h> // GFXRoot drawing lib
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
// An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD
|
||||
class AppletFont
|
||||
{
|
||||
public:
|
||||
AppletFont();
|
||||
explicit AppletFont(const GFXfont &adafruitGFXFont);
|
||||
|
||||
uint8_t lineHeight();
|
||||
uint8_t heightAboveCursor();
|
||||
uint8_t heightBelowCursor();
|
||||
uint8_t widthBetweenWords(); // Width of the space character
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
std::vector<Substitution> substitutions; // List of all character substitutions to run, prior to printing a string
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
428
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
Normal file
428
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./MapApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::MapApplet::onRender()
|
||||
{
|
||||
// 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 *sets* passed parameters (C-style)
|
||||
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 stretches 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 coords
|
||||
// (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 traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees traveled 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, converted 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
|
||||
// Hopefully 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, converted 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
|
||||
65
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
Normal file
65
src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#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 and size of a marker to be drawn
|
||||
struct Marker {
|
||||
float eastMeters = 0; // Meters east of map center. Negative if west.
|
||||
float northMeters = 0; // Meters north of map center. Negative if south.
|
||||
bool hasHopsAway = false;
|
||||
uint8_t hopsAway = 0; // Determines marker size
|
||||
};
|
||||
|
||||
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);
|
||||
void calculateAllMarkers();
|
||||
void calculateMapScale(); // Conversion factor for meters to pixels
|
||||
void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers
|
||||
|
||||
float metersToPx = 0; // Conversion factor for meters to pixels
|
||||
float latCenter = 0; // Map center: latitude
|
||||
float lngCenter = 0; // Map center: longitude
|
||||
|
||||
std::list<Marker> markers;
|
||||
uint32_t widthMeters = 0; // Map width: meters
|
||||
uint32_t heightMeters = 0; // Map height: meters
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,279 @@
|
||||
#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 act as if 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
|
||||
|
||||
// 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* promiscuous, 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
|
||||
}
|
||||
|
||||
// Calculate maximum number of cards we may ever need to render, in our tallest layout config
|
||||
// Number might be slightly in excess of the true value: applet header text 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 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
|
||||
// ========================
|
||||
|
||||
// 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 gutterW = 0.1; // Between bars
|
||||
|
||||
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest
|
||||
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) * gutterW) + 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++) {
|
||||
// Coords for this specific bar
|
||||
float barH = barHMax * barHRel[i];
|
||||
float barX = paddingW + (i * (gutterW + barW));
|
||||
float barY = paddingH + (barHMax - barH);
|
||||
|
||||
// Rasterize to px coords at the last moment
|
||||
int16_t rX = (x + (w * barX)) + 0.5;
|
||||
int16_t rY = (y + (h * barY)) + 0.5;
|
||||
uint16_t rW = (w * barW) + 0.5;
|
||||
uint16_t rH = (h * barH) + 0.5;
|
||||
|
||||
// Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
|
||||
if (i <= strength)
|
||||
drawRect(rX, rY, rW, rH, BLACK);
|
||||
else {
|
||||
// Just draw a placeholder line
|
||||
float lineY = barY + barH;
|
||||
uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
|
||||
drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,74 @@
|
||||
#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"
|
||||
|
||||
#include "main.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class NodeListApplet : public Applet, public MeshModule
|
||||
{
|
||||
protected:
|
||||
// Info needed to draw a node card to the list
|
||||
// - generated each time we hear a node
|
||||
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;
|
||||
};
|
||||
|
||||
public:
|
||||
NodeListApplet(const char *name);
|
||||
|
||||
void onRender() override;
|
||||
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
protected:
|
||||
virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node
|
||||
virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be
|
||||
|
||||
uint8_t maxCards(); // Max number of cards which could ever fit on screen
|
||||
|
||||
std::deque<CardInfo> cards; // Cards to be rendered. Derived applet fills this.
|
||||
|
||||
private:
|
||||
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h,
|
||||
SignalStrength signal); // Draw a "mobile phone" style signal indicator
|
||||
|
||||
// Card Dimensions
|
||||
// - for rendering and for maxCards calc
|
||||
const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||
const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./BasicExampleApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// All drawing happens here
|
||||
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||
void InkHUD::BasicExampleApplet::onRender()
|
||||
{
|
||||
print("Hello, World!");
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
A bare-minimum example of an InkHUD applet.
|
||||
Only prints Hello World.
|
||||
|
||||
In variants/<your device>/nicheGraphics.h:
|
||||
|
||||
- include this .h file
|
||||
- add the following line of code:
|
||||
windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class BasicExampleApplet : public Applet
|
||||
{
|
||||
public:
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#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()
|
||||
{
|
||||
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||
|
||||
int16_t centerX = X(0.5); // Same as width() / 2
|
||||
int16_t centerY = Y(0.5); // Same as height() / 2
|
||||
|
||||
if (haveMessage) {
|
||||
printAt(centerX, centerY, "New Message", CENTER, BOTTOM);
|
||||
printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP);
|
||||
} else {
|
||||
printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
An example of an InkHUD applet.
|
||||
Tells us when a new text message arrives.
|
||||
|
||||
This applet makes use of the MeshModule API to detect new messages,
|
||||
which is a general part of the Meshtastic firmware, and not part of InkHUD.
|
||||
|
||||
In variants/<your device>/nicheGraphics.h:
|
||||
|
||||
- include this .h file
|
||||
- add the following line of code:
|
||||
windowManager->addApplet("New Msg", new InkHUD::NewMsgExampleApplet);
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include "mesh/SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class NewMsgExampleApplet : public Applet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
// The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages.
|
||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||
|
||||
// All drawing happens here
|
||||
void onRender() override;
|
||||
|
||||
// Your applet might also want to use some of these
|
||||
// Useful for setting up or tidying up
|
||||
|
||||
/*
|
||||
void onActivate(); // When started
|
||||
void onDeactivate(); // When stopped
|
||||
void onForeground(); // When shown by short-press
|
||||
void onBackground(); // When hidden by short-press
|
||||
*/
|
||||
|
||||
private:
|
||||
// Called when we receive new text messages
|
||||
// Part of the MeshModule API
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
// Store info from handleReceived
|
||||
bool haveMessage = false;
|
||||
NodeNum fromWho = 0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./BatteryIconApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
// We handle power status' even when the feature is disabled,
|
||||
// so that we have up to date data ready if the feature is enabled later.
|
||||
// Otherwise could be 30s before new status update, with weird battery value displayed
|
||||
int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status)
|
||||
{
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
|
||||
// This method should only receive power statuses
|
||||
// If we get a different type of status, something has gone weird elsewhere
|
||||
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||
|
||||
// Get the new state of charge %, and round to the nearest 10%
|
||||
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
|
||||
// If rounded value has changed, trigger a display update
|
||||
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||
// Don't trigger an update if the feature is disabled
|
||||
if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon)
|
||||
requestUpdate();
|
||||
|
||||
// Store the new value
|
||||
this->socRounded = newSocRounded;
|
||||
|
||||
return 0; // Tell Observable to continue informing other observers
|
||||
}
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender()
|
||||
{
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
int16_t l = 0;
|
||||
int16_t t = 0;
|
||||
uint16_t w = width();
|
||||
int16_t h = height();
|
||||
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
|
||||
// Positive terminal "bump"
|
||||
const int16_t &bumpL = l;
|
||||
const uint16_t bumpH = h / 2;
|
||||
const int16_t bumpT = m - (bumpH / 2);
|
||||
constexpr uint16_t bumpW = 2;
|
||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE);
|
||||
|
||||
// ===================
|
||||
// Draw battery level
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#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/SystemApplet.h"
|
||||
|
||||
#include "PowerStatus.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class BatteryIconApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
BatteryIconApplet();
|
||||
|
||||
void onRender() override;
|
||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||
|
||||
private:
|
||||
// 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
|
||||
92
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
Normal file
92
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./LogoApplet.h"
|
||||
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
|
||||
{
|
||||
OSThread::setIntervalFromNow(8 * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = xstr(APP_VERSION_SHORT);
|
||||
fontTitle = fontSmall;
|
||||
|
||||
bringToForeground();
|
||||
// This is then drawn with a FULL refresh by Renderer::begin
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it.
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onBackground()
|
||||
{
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
|
||||
// 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
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// Begin displaying the screen which is shown at shutdown
|
||||
void InkHUD::LogoApplet::onShutdown()
|
||||
{
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = owner.short_name;
|
||||
fontTitle = fontLarge;
|
||||
|
||||
bringToForeground();
|
||||
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update
|
||||
}
|
||||
|
||||
int32_t InkHUD::LogoApplet::runOnce()
|
||||
{
|
||||
sendToBackground();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
#endif
|
||||
40
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
Normal file
40
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#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/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class LogoApplet : public SystemApplet, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
LogoApplet();
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onShutdown() override;
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
std::string textLeft;
|
||||
std::string textRight;
|
||||
std::string textTitle;
|
||||
AppletFont fontTitle;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
38
src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
Normal file
38
src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Set of end-point actions for the Menu Applet
|
||||
|
||||
Added as menu entries in MenuApplet::showPage
|
||||
Behaviors assigned in MenuApplet::execute
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_NODEINFO,
|
||||
SEND_POSITION,
|
||||
SHUTDOWN,
|
||||
NEXT_TILE,
|
||||
TOGGLE_APPLET,
|
||||
ACTIVATE_APPLETS, // Todo: remove? Possible redundant, handled by TOGGLE_APPLET?
|
||||
TOGGLE_AUTOSHOW_APPLET,
|
||||
SET_RECENTS,
|
||||
ROTATE,
|
||||
LAYOUT,
|
||||
TOGGLE_BATTERY_ICON,
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_BACKLIGHT,
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
599
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
Normal file
599
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
Normal file
@@ -0,0 +1,599 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./MenuApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
|
||||
#include "airtime.h"
|
||||
#include "power.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();
|
||||
|
||||
// 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::onActivate() {}
|
||||
|
||||
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 requesting update while menu is open
|
||||
// Handle button input with this applet
|
||||
SystemApplet::lockRequests = true;
|
||||
SystemApplet::handleInput = true;
|
||||
|
||||
// Begin the auto-close timeout
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
||||
inkhud->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
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
|
||||
// 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 upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu
|
||||
inkhud->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:
|
||||
inkhud->nextTile();
|
||||
break;
|
||||
|
||||
case ROTATE:
|
||||
inkhud->rotate();
|
||||
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;
|
||||
|
||||
inkhud->updateLayout();
|
||||
break;
|
||||
|
||||
case TOGGLE_APPLET:
|
||||
settings->userApplets.active[cursor] = !settings->userApplets.active[cursor];
|
||||
inkhud->updateAppletSelection();
|
||||
// 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?
|
||||
inkhud->updateAppletSelection();
|
||||
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:
|
||||
inkhud->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 & Shut Down", 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");
|
||||
|
||||
// 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(); // Checkbox: 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 < inkhud->userApplets.size(); i++) {
|
||||
const char *name = inkhud->userApplets.at(i)->name;
|
||||
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 < inkhud->userApplets.size(); i++) {
|
||||
// Only add a menu item if applet is active
|
||||
if (settings->userApplets.active[i]) {
|
||||
const char *name = inkhud->userApplets.at(i)->name;
|
||||
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 situation, where slotCount needs to know panel height, and panel height needs to know slotCount
|
||||
uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
|
||||
{
|
||||
// Render *far* off screen
|
||||
uint16_t height = 0;
|
||||
drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
#endif
|
||||
60
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
Normal file
60
src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/Persistence.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
#include "./MenuItem.h"
|
||||
#include "./MenuPage.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
|
||||
class MenuApplet : public SystemApplet, 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:
|
||||
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
||||
|
||||
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 = MenuPage::ROOT;
|
||||
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
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
47
src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h
Normal file
47
src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
One item of a MenuPage, in InkHUD::MenuApplet
|
||||
|
||||
Added to MenuPages in InkHUD::showPage
|
||||
|
||||
- May open a submenu or exit
|
||||
- May perform an action
|
||||
- May toggle a bool value, shown by a checkbox
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./MenuAction.h"
|
||||
#include "./MenuPage.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
// One item of a MenuPage
|
||||
class MenuItem
|
||||
{
|
||||
public:
|
||||
std::string label;
|
||||
MenuAction action = NO_ACTION;
|
||||
MenuPage nextPage = EXIT;
|
||||
bool *checkState = nullptr;
|
||||
|
||||
// Various constructors, depending on the intended function of the item
|
||||
|
||||
MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action) : label(label), action(action) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState)
|
||||
: label(label), action(action), nextPage(nextPage), checkState(checkState)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user