mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 23:32:34 +00:00
Compare commits
211 Commits
no-arduino
...
split-noti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
550c0796eb | ||
|
|
93d101d11a | ||
|
|
fc2fd5ebff | ||
|
|
7265b5e6c6 | ||
|
|
bf5c9f6263 | ||
|
|
1736db6b56 | ||
|
|
3a5dc870e0 | ||
|
|
11d307c609 | ||
|
|
c090a7f6d5 | ||
|
|
c8bfb61c8d | ||
|
|
c144bd03dc | ||
|
|
dbc67973c6 | ||
|
|
3dd77ace85 | ||
|
|
e1b1e35a27 | ||
|
|
18098fb1cb | ||
|
|
b1e3353ceb | ||
|
|
8fb1e0f874 | ||
|
|
667ff17fdb | ||
|
|
42c1967e7b | ||
|
|
7512673b09 | ||
|
|
94258cfd1c | ||
|
|
7c297eff8f | ||
|
|
c178396e20 | ||
|
|
caf4c3919c | ||
|
|
7d09bd981a | ||
|
|
f046c1a68a | ||
|
|
3870d81bf6 | ||
|
|
a7dcf580ad | ||
|
|
61f81ac758 | ||
|
|
9446f07c4d | ||
|
|
94904cb6a7 | ||
|
|
646b370411 | ||
|
|
b6bcee18b5 | ||
|
|
20988aa4fa | ||
|
|
cab6707ca0 | ||
|
|
46ac9841d6 | ||
|
|
88ab198e0f | ||
|
|
0c948a3fc0 | ||
|
|
17456d0618 | ||
|
|
a395448170 | ||
|
|
e6ba326876 | ||
|
|
ecfaf3a095 | ||
|
|
a6cc4ab3fe | ||
|
|
d411fd99f0 | ||
|
|
819f5a2fde | ||
|
|
ca34fe9a90 | ||
|
|
137e7183c7 | ||
|
|
54fa39b2e9 | ||
|
|
eca240373a | ||
|
|
0b1703a51a | ||
|
|
653f6c2a85 | ||
|
|
7a285cf221 | ||
|
|
cea5cd171a | ||
|
|
c5e3bc841e | ||
|
|
ca7d2d7482 | ||
|
|
7af31a88c0 | ||
|
|
9f53df4f2e | ||
|
|
485fc7639e | ||
|
|
34f3800e2b | ||
|
|
a3ed75c5c9 | ||
|
|
088143dbf3 | ||
|
|
fecf80c39b | ||
|
|
7ef8067b87 | ||
|
|
91bcf072a0 | ||
|
|
9de5d170bf | ||
|
|
4802cef3ca | ||
|
|
3d28086f68 | ||
|
|
232d601b14 | ||
|
|
36ee2cfda0 | ||
|
|
56c1ba037a | ||
|
|
ae9c062dc9 | ||
|
|
6c5b947ad5 | ||
|
|
f9bf7a1010 | ||
|
|
c35610b04d | ||
|
|
0df1d49220 | ||
|
|
0ba3170dfe | ||
|
|
94b9684981 | ||
|
|
e0918ea448 | ||
|
|
4c0517c6f2 | ||
|
|
07cd16d2df | ||
|
|
a33672db4f | ||
|
|
38896198f2 | ||
|
|
6088ab49eb | ||
|
|
7f8acf5658 | ||
|
|
99176a8388 | ||
|
|
30e0972de5 | ||
|
|
6bd600a878 | ||
|
|
2f31ee5b6e | ||
|
|
6a91741209 | ||
|
|
b55e763b29 | ||
|
|
60acba877e | ||
|
|
221988c665 | ||
|
|
850d957931 | ||
|
|
012f88e56f | ||
|
|
83248ce0d0 | ||
|
|
bdc1df9f5c | ||
|
|
2de08bebdc | ||
|
|
d3e56ea084 | ||
|
|
2f37204df2 | ||
|
|
0808f5215f | ||
|
|
247e05bb10 | ||
|
|
791377b76b | ||
|
|
53d28f3a3a | ||
|
|
4308bbc156 | ||
|
|
574cbe55c0 | ||
|
|
ce1480df98 | ||
|
|
0108ad7992 | ||
|
|
f11b49863d | ||
|
|
5ca5ee2846 | ||
|
|
e1df4e19e5 | ||
|
|
766189212c | ||
|
|
8ba98ae873 | ||
|
|
7a38368494 | ||
|
|
195b7cc30a | ||
|
|
4feaec651f | ||
|
|
82b7cb5dd0 | ||
|
|
30bbb449db | ||
|
|
14421c3609 | ||
|
|
2cf7e51061 | ||
|
|
7fd12782a1 | ||
|
|
c914a62d93 | ||
|
|
12680ad9cd | ||
|
|
0561f2ca4b | ||
|
|
58743021c8 | ||
|
|
2fb46ce5d5 | ||
|
|
8be76a56c7 | ||
|
|
2c206febab | ||
|
|
db1eac12af | ||
|
|
56e67cb434 | ||
|
|
e9d5e36738 | ||
|
|
f71fdef3fd | ||
|
|
5e92145324 | ||
|
|
89a4589b68 | ||
|
|
20991d8b53 | ||
|
|
3ab9005b2f | ||
|
|
aabc5b7cf2 | ||
|
|
afcd97c154 | ||
|
|
cbdd7eae70 | ||
|
|
6374ffea35 | ||
|
|
1a6bb97f16 | ||
|
|
4f0b95e910 | ||
|
|
a81b41cbfb | ||
|
|
465fe18a89 | ||
|
|
bd0e25f3f5 | ||
|
|
9861e82f0a | ||
|
|
fcefd592e2 | ||
|
|
8a8a7cdefc | ||
|
|
8f9e569825 | ||
|
|
b0c5327585 | ||
|
|
f1dd623ce9 | ||
|
|
ac52edd11a | ||
|
|
66d5dde956 | ||
|
|
7dfbcc8f1d | ||
|
|
28244148a2 | ||
|
|
e623c70bd0 | ||
|
|
425f384b1f | ||
|
|
1557219bad | ||
|
|
691917b956 | ||
|
|
cc0fbfbd21 | ||
|
|
5d0bf03b01 | ||
|
|
8ff99437cb | ||
|
|
ba93097bb7 | ||
|
|
de098cca4c | ||
|
|
8faa04afdb | ||
|
|
fede1b8597 | ||
|
|
8557bd031d | ||
|
|
4e6418b635 | ||
|
|
a1a5503fe9 | ||
|
|
3b94981e56 | ||
|
|
f299447216 | ||
|
|
5f0c8863fd | ||
|
|
f9d17cdee0 | ||
|
|
68a28a177f | ||
|
|
60ec05e536 | ||
|
|
730cd388d6 | ||
|
|
6549b0477c | ||
|
|
8304cae010 | ||
|
|
0ad9758cfd | ||
|
|
e5f6804421 | ||
|
|
720add72b2 | ||
|
|
693b11db1d | ||
|
|
4bf2dd04ae | ||
|
|
c6c2a4d4dd | ||
|
|
79b8e7b1cf | ||
|
|
cf4f088337 | ||
|
|
22cb20d294 | ||
|
|
1eacdd0629 | ||
|
|
67e3d57412 | ||
|
|
7924ef87b5 | ||
|
|
3dec521f75 | ||
|
|
57a33790ed | ||
|
|
484af8eb9f | ||
|
|
b8970d66a1 | ||
|
|
e78033bb85 | ||
|
|
8bd7adca47 | ||
|
|
f67aec40e8 | ||
|
|
46c7d74760 | ||
|
|
15d2ae17f8 | ||
|
|
91579c4650 | ||
|
|
79b710a108 | ||
|
|
ba296db701 | ||
|
|
c0e1616382 | ||
|
|
070deb290f | ||
|
|
76f7207463 | ||
|
|
55b2bbf937 | ||
|
|
a5716cf25c | ||
|
|
4d81280ac2 | ||
|
|
9ce44556ce | ||
|
|
be0c7d73a3 | ||
|
|
d833a9ea61 | ||
|
|
5cd74f4b53 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
open_collective: meshtastic
|
||||||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@@ -1,6 +1,7 @@
|
|||||||
## 🙏 Thank you for sending in a pull request, here's some tips to get started!
|
## 🙏 Thank you for sending in a pull request, here's some tips to get started!
|
||||||
|
|
||||||
### ❌ (Please delete all these tips and replace them with your text) ❌
|
### ❌ (Please delete all these tips and replace them with your text) ❌
|
||||||
|
|
||||||
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first
|
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first
|
||||||
to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback
|
to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback
|
||||||
is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc...
|
is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc...
|
||||||
@@ -15,8 +16,8 @@
|
|||||||
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes.
|
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes.
|
||||||
- If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord
|
- If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord
|
||||||
|
|
||||||
|
|
||||||
## 🤝 Attestations
|
## 🤝 Attestations
|
||||||
|
|
||||||
- [ ] I have tested that my proposed changes behave as described.
|
- [ ] I have tested that my proposed changes behave as described.
|
||||||
- [ ] I have tested that my proposed changes do not cause any obvious regressions on the following devices:
|
- [ ] I have tested that my proposed changes do not cause any obvious regressions on the following devices:
|
||||||
- [ ] Heltec (Lora32) V3
|
- [ ] Heltec (Lora32) V3
|
||||||
|
|||||||
2
.github/workflows/daily_packaging.yml
vendored
2
.github/workflows/daily_packaging.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: Daily Packaging
|
name: Daily Packaging
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: 0 9 * * *
|
- cron: 0 2 * * *
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
1
.github/workflows/release_channels.yml
vendored
1
.github/workflows/release_channels.yml
vendored
@@ -98,6 +98,7 @@ jobs:
|
|||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
base: ${{ github.event.repository.default_branch }}
|
base: ${{ github.event.repository.default_branch }}
|
||||||
|
branch: create-pull-request/bump-version
|
||||||
title: Bump release version
|
title: Bump release version
|
||||||
commit-message: automated bumps
|
commit-message: automated bumps
|
||||||
add-paths: |
|
add-paths: |
|
||||||
|
|||||||
1
.github/workflows/update_protobufs.yml
vendored
1
.github/workflows/update_protobufs.yml
vendored
@@ -33,6 +33,7 @@ jobs:
|
|||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
|
branch: create-pull-request/update-protobufs
|
||||||
title: Update protobufs and classes
|
title: Update protobufs and classes
|
||||||
add-paths: |
|
add-paths: |
|
||||||
protobufs
|
protobufs
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ plugins:
|
|||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.435
|
- checkov@3.2.442
|
||||||
- renovate@40.34.4
|
- renovate@40.60.3
|
||||||
- prettier@3.5.3
|
- prettier@3.5.3
|
||||||
- trufflehog@3.88.34
|
- trufflehog@3.89.2
|
||||||
- yamllint@1.37.1
|
- yamllint@1.37.1
|
||||||
- bandit@1.8.3
|
- bandit@1.8.5
|
||||||
- trivy@0.62.1
|
- trivy@0.63.0
|
||||||
- taplo@0.9.3
|
- taplo@0.9.3
|
||||||
- ruff@0.11.11
|
- ruff@0.12.0
|
||||||
- isort@6.0.1
|
- isort@6.0.1
|
||||||
- markdownlint@0.45.0
|
- markdownlint@0.45.0
|
||||||
- oxipng@9.1.5
|
- oxipng@9.1.5
|
||||||
@@ -28,7 +28,7 @@ lint:
|
|||||||
- shellcheck@0.10.0
|
- shellcheck@0.10.0
|
||||||
- black@25.1.0
|
- black@25.1.0
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gitleaks@8.26.0
|
- gitleaks@8.27.2
|
||||||
- clang-format@16.0.3
|
- clang-format@16.0.3
|
||||||
ignore:
|
ignore:
|
||||||
- linters: [ALL]
|
- linters: [ALL]
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ Join our community and help improve Meshtastic! 🚀
|
|||||||
## Stats
|
## Stats
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM python:3.13-alpine3.21 AS builder
|
FROM python:3.13-alpine3.22 AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \
|
|||||||
|
|
||||||
# ##### PRODUCTION BUILD #############
|
# ##### PRODUCTION BUILD #############
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.22
|
||||||
LABEL org.opencontainers.image.title="Meshtastic" \
|
LABEL org.opencontainers.image.title="Meshtastic" \
|
||||||
org.opencontainers.image.description="Alpine Meshtastic daemon" \
|
org.opencontainers.image.description="Alpine Meshtastic daemon" \
|
||||||
org.opencontainers.image.url="https://meshtastic.org" \
|
org.opencontainers.image.url="https://meshtastic.org" \
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ extends = arduino_base
|
|||||||
custom_esp32_kind = esp32
|
custom_esp32_kind = esp32
|
||||||
platform =
|
platform =
|
||||||
# renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32
|
# renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32
|
||||||
platformio/espressif32@6.10.0
|
platformio/espressif32@6.11.0
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
[portduino_base]
|
[portduino_base]
|
||||||
platform =
|
platform =
|
||||||
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
||||||
https://github.com/meshtastic/platform-native/archive/622341c6de8a239704318b10c3dbb00c21a3eab3.zip
|
https://github.com/meshtastic/platform-native/archive/681ee029207e9fd040afa223df6e54074cbbe084.zip
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
extends = arduino_base
|
extends = arduino_base
|
||||||
platform =
|
platform =
|
||||||
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
|
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
|
||||||
platformio/ststm32@19.1.0
|
platformio/ststm32@19.2.0
|
||||||
platform_packages =
|
platform_packages =
|
||||||
# TODO renovate
|
# TODO renovate
|
||||||
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
|
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ for BOARD in $BOARDS; do
|
|||||||
CHECK="${CHECK} -e ${BOARD}"
|
CHECK="${CHECK} -e ${BOARD}"
|
||||||
done
|
done
|
||||||
|
|
||||||
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high
|
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ Lora:
|
|||||||
### Some devices, like the pinedio, may require spidev0.1 as a workaround.
|
### Some devices, like the pinedio, may require spidev0.1 as a workaround.
|
||||||
# spidev: spidev0.0
|
# spidev: spidev0.0
|
||||||
|
|
||||||
### Define GPIO buttons here:
|
### Deprecated location for User Button:
|
||||||
|
|
||||||
GPIO:
|
#GPIO:
|
||||||
# User: 6
|
# User: 6
|
||||||
|
|
||||||
### Define GPS
|
### Define GPS
|
||||||
@@ -115,17 +115,6 @@ I2C:
|
|||||||
|
|
||||||
Display:
|
Display:
|
||||||
|
|
||||||
### Waveshare 1.44inch LCD HAT
|
|
||||||
# Panel: ST7735S
|
|
||||||
# CS: 8 #Chip Select
|
|
||||||
# DC: 25 # Data/Command pin
|
|
||||||
# Backlight: 24
|
|
||||||
# Width: 128
|
|
||||||
# Height: 128
|
|
||||||
# Reset: 27
|
|
||||||
# OffsetX: 0
|
|
||||||
# OffsetY: 0
|
|
||||||
|
|
||||||
### Adafruit PiTFT 2.8 TFT+Touchscreen
|
### Adafruit PiTFT 2.8 TFT+Touchscreen
|
||||||
# Panel: ILI9341
|
# Panel: ILI9341
|
||||||
# CS: 8
|
# CS: 8
|
||||||
@@ -180,6 +169,16 @@ Input:
|
|||||||
|
|
||||||
# KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd
|
# KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd
|
||||||
|
|
||||||
|
### Standard User Button Config
|
||||||
|
# UserButton: 6
|
||||||
|
|
||||||
|
### Trackball/Joystick input
|
||||||
|
# TrackballUp: 6
|
||||||
|
# TrackballDown: 19
|
||||||
|
# TrackballLeft: 5
|
||||||
|
# TrackballRight: 26
|
||||||
|
# TrackballPress: 13
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
Logging:
|
Logging:
|
||||||
|
|||||||
26
bin/config.d/display-waveshare-1-44.yaml
Normal file
26
bin/config.d/display-waveshare-1-44.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
### Waveshare 1.44inch LCD HAT
|
||||||
|
Display:
|
||||||
|
Panel: ST7735S
|
||||||
|
spidev: spidev0.0 # Specify either the spidev here, or the CS below
|
||||||
|
# CS: 8 #Chip Select # Optional, as this is the default pin for spidev0.0
|
||||||
|
DC: 25 # Data/Command pin
|
||||||
|
Backlight: 24
|
||||||
|
Width: 128
|
||||||
|
Height: 128
|
||||||
|
Reset: 27
|
||||||
|
OffsetX: 2
|
||||||
|
OffsetY: 1
|
||||||
|
|
||||||
|
|
||||||
|
# OffsetY: 31 # These two options are used to properly flip the screen 180 degrees
|
||||||
|
# OffsetRotate: 3
|
||||||
|
|
||||||
|
|
||||||
|
Input:
|
||||||
|
TrackballUp: 6
|
||||||
|
TrackballDown: 19
|
||||||
|
TrackballLeft: 5
|
||||||
|
TrackballRight: 26
|
||||||
|
TrackballPress: 13
|
||||||
|
|
||||||
|
# User: 21
|
||||||
@@ -6,6 +6,6 @@ Lora:
|
|||||||
IRQ: 16
|
IRQ: 16
|
||||||
Busy: 20
|
Busy: 20
|
||||||
Reset: 24
|
Reset: 24
|
||||||
TXen: 13
|
RXen: 12
|
||||||
DIO2_AS_RF_SWITCH: true
|
DIO2_AS_RF_SWITCH: true
|
||||||
DIO3_TCXO_VOLTAGE: true
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
|||||||
21
bin/config.d/lora-RAK6421.yaml
Normal file
21
bin/config.d/lora-RAK6421.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Lora:
|
||||||
|
|
||||||
|
### RAK13300in Slot 1
|
||||||
|
Module: sx1262
|
||||||
|
IRQ: 22 #IO6
|
||||||
|
Reset: 16 # IO4
|
||||||
|
Busy: 24 # IO5
|
||||||
|
# Ant_sw: 13 # IO3
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
spidev: spidev0.0
|
||||||
|
# CS: 8
|
||||||
|
|
||||||
|
|
||||||
|
### RAK13300in Slot 2 pins
|
||||||
|
# IRQ: 18 #IO6
|
||||||
|
# Reset: 24 # IO4
|
||||||
|
# Busy: 19 # IO5
|
||||||
|
# # Ant_sw: 23 # IO3
|
||||||
|
# spidev: spidev0.1
|
||||||
|
# # CS: 7
|
||||||
18
bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml
Normal file
18
bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
gpiochip: 0
|
||||||
|
MOSI: 12
|
||||||
|
MISO: 13
|
||||||
|
IRQ: 1
|
||||||
|
Busy: 23
|
||||||
|
Reset: 22
|
||||||
|
RXen: 0
|
||||||
|
gpiochip: 1
|
||||||
|
CS: 9
|
||||||
|
SCK: 11
|
||||||
|
# TXen: bridge to DIO2 on E22 module
|
||||||
|
SX126X_MAX_POWER: 22
|
||||||
|
spidev: spidev1.0
|
||||||
|
spiSpeed: 2000000
|
||||||
@@ -12,6 +12,7 @@ SET "BIGDB16=0"
|
|||||||
SET "ESPTOOL_BAUD=115200"
|
SET "ESPTOOL_BAUD=115200"
|
||||||
SET "ESPTOOL_CMD="
|
SET "ESPTOOL_CMD="
|
||||||
SET "LOGCOUNTER=0"
|
SET "LOGCOUNTER=0"
|
||||||
|
SET "BPS_RESET=0"
|
||||||
|
|
||||||
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
|
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
|
||||||
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
|
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
|
||||||
@@ -24,7 +25,7 @@ GOTO getopts
|
|||||||
:help
|
:help
|
||||||
ECHO Flash image file to device, but first erasing and writing system information.
|
ECHO Flash image file to device, but first erasing and writing system information.
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web)
|
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web) [--1200bps-reset]
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Options:
|
ECHO Options:
|
||||||
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
|
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
|
||||||
@@ -35,13 +36,16 @@ ECHO -P python Specify alternate python interpreter to use to invoke
|
|||||||
ECHO If supplied the script will use python.
|
ECHO If supplied the script will use python.
|
||||||
ECHO If not supplied the script will try to find esptool in Path.
|
ECHO If not supplied the script will try to find esptool in Path.
|
||||||
ECHO --web Enable WebUI. (default: false)
|
ECHO --web Enable WebUI. (default: false)
|
||||||
|
ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset)
|
||||||
|
ECHO Some hardware requires this twice.
|
||||||
ECHO.
|
ECHO.
|
||||||
|
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
|
||||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
|
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
|
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web
|
||||||
GOTO eof
|
GOTO eof
|
||||||
|
|
||||||
:version
|
:version
|
||||||
ECHO %SCRIPT_NAME% [Version 2.6.1]
|
ECHO %SCRIPT_NAME% [Version 2.6.2]
|
||||||
ECHO Meshtastic
|
ECHO Meshtastic
|
||||||
GOTO eof
|
GOTO eof
|
||||||
|
|
||||||
@@ -58,10 +62,13 @@ IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
|||||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||||
IF /I "%~1"=="--web" SET "WEB_APP=1"
|
IF /I "%~1"=="--web" SET "WEB_APP=1"
|
||||||
|
IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1"
|
||||||
SHIFT
|
SHIFT
|
||||||
GOTO getopts
|
GOTO getopts
|
||||||
:endopts
|
:endopts
|
||||||
|
|
||||||
|
IF %BPS_RESET% EQU 1 GOTO skip-filename
|
||||||
|
|
||||||
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
||||||
IF "__!FILENAME!__"=="____" (
|
IF "__!FILENAME!__"=="____" (
|
||||||
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
||||||
@@ -95,6 +102,9 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" (
|
|||||||
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
:skip-filename
|
||||||
|
SET "ESPTOOL_BAUD=1200"
|
||||||
|
|
||||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||||
IF NOT "__%PYTHON%__"=="____" (
|
IF NOT "__%PYTHON%__"=="____" (
|
||||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||||
@@ -133,6 +143,12 @@ IF "__!ESPTOOL_PORT!__" == "____" (
|
|||||||
)
|
)
|
||||||
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||||
|
|
||||||
|
IF %BPS_RESET% EQU 1 (
|
||||||
|
@REM Attempt to change mode via 1200bps Reset.
|
||||||
|
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
|
||||||
|
GOTO eof
|
||||||
|
)
|
||||||
|
|
||||||
@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
|
@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
|
||||||
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
|
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
|
||||||
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
|
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
|
||||||
@@ -254,6 +270,7 @@ EXIT /B %ERRORLEVEL%
|
|||||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||||
CALL :RESET_ERROR
|
CALL :RESET_ERROR
|
||||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||||
|
IF %BPS_RESET% EQU 1 GOTO :eof
|
||||||
IF %ERRORLEVEL% NEQ 0 (
|
IF %ERRORLEVEL% NEQ 0 (
|
||||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||||
EXIT /B %ERRORLEVEL%
|
EXIT /B %ERRORLEVEL%
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
||||||
WEB_APP=false
|
WEB_APP=false
|
||||||
|
BPS_RESET=false
|
||||||
TFT_BUILD=false
|
TFT_BUILD=false
|
||||||
MCU=""
|
MCU=""
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ BIGDB_16MB=(
|
|||||||
"station-g2"
|
"station-g2"
|
||||||
"t-eth-elite"
|
"t-eth-elite"
|
||||||
"t-watch-s3"
|
"t-watch-s3"
|
||||||
|
"elecrow-adv-35-tft"
|
||||||
|
"elecrow-adv-24-28-tft"
|
||||||
|
"elecrow-adv1-43-50-70-tft"
|
||||||
)
|
)
|
||||||
S3_VARIANTS=(
|
S3_VARIANTS=(
|
||||||
"s3"
|
"s3"
|
||||||
@@ -72,7 +76,7 @@ set -e
|
|||||||
# Usage info
|
# Usage info
|
||||||
show_help() {
|
show_help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--web]
|
Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--web] [--1200bps-reset]
|
||||||
Flash image file to device, but first erasing and writing system information.
|
Flash image file to device, but first erasing and writing system information.
|
||||||
|
|
||||||
-h Display this help and exit.
|
-h Display this help and exit.
|
||||||
@@ -80,6 +84,7 @@ Flash image file to device, but first erasing and writing system information.
|
|||||||
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
||||||
-f FILENAME The firmware .bin file to flash. Custom to your device type and region.
|
-f FILENAME The firmware .bin file to flash. Custom to your device type and region.
|
||||||
--web Enable WebUI. (Default: false)
|
--web Enable WebUI. (Default: false)
|
||||||
|
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -105,6 +110,9 @@ while [ $# -gt 0 ]; do
|
|||||||
--web)
|
--web)
|
||||||
WEB_APP=true
|
WEB_APP=true
|
||||||
;;
|
;;
|
||||||
|
--1200bps-reset)
|
||||||
|
BPS_RESET=true
|
||||||
|
;;
|
||||||
--) # Stop parsing options
|
--) # Stop parsing options
|
||||||
shift
|
shift
|
||||||
break
|
break
|
||||||
@@ -117,18 +125,23 @@ while [ $# -gt 0 ]; do
|
|||||||
shift # Move to the next argument
|
shift # Move to the next argument
|
||||||
done
|
done
|
||||||
|
|
||||||
[ -z "$FILENAME" -a -n "$1" ] && {
|
if [[ $BPS_RESET == true ]]; then
|
||||||
FILENAME=$1
|
$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||||
|
FILENAME="$1"
|
||||||
shift
|
shift
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ $FILENAME != firmware-* ]]; then
|
if [[ "$FILENAME" != firmware-* ]]; then
|
||||||
echo "Filename must be a firmware-* file."
|
echo "Filename must be a firmware-* file."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if FILENAME contains "-tft-" and prevent web/mui comingling.
|
# Check if FILENAME contains "-tft-" and prevent web/mui comingling.
|
||||||
if [[ ${FILENAME//-tft-/} != "$FILENAME" ]]; then
|
if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then
|
||||||
TFT_BUILD=true
|
TFT_BUILD=true
|
||||||
if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then
|
if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then
|
||||||
echo "Cannot enable WebUI (--web) and MUI."
|
echo "Cannot enable WebUI (--web) and MUI."
|
||||||
@@ -187,15 +200,15 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
|||||||
SPIFFSFILE=littlefs-${BASENAME}
|
SPIFFSFILE=littlefs-${BASENAME}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $FILENAME ]]; then
|
if [[ ! -f "$FILENAME" ]]; then
|
||||||
echo "Error: file ${FILENAME} wasn't found. Terminating."
|
echo "Error: file ${FILENAME} wasn't found. Terminating."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ ! -f $OTAFILE ]]; then
|
if [[ ! -f "$OTAFILE" ]]; then
|
||||||
echo "Error: file ${OTAFILE} wasn't found. Terminating."
|
echo "Error: file ${OTAFILE} wasn't found. Terminating."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ ! -f $SPIFFSFILE ]]; then
|
if [[ ! -f "$SPIFFSFILE" ]]; then
|
||||||
echo "Error: file ${SPIFFSFILE} wasn't found. Terminating."
|
echo "Error: file ${SPIFFSFILE} wasn't found. Terminating."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ SET "PYTHON="
|
|||||||
SET "ESPTOOL_BAUD=115200"
|
SET "ESPTOOL_BAUD=115200"
|
||||||
SET "ESPTOOL_CMD="
|
SET "ESPTOOL_CMD="
|
||||||
SET "LOGCOUNTER=0"
|
SET "LOGCOUNTER=0"
|
||||||
|
SET "CHANGE_MODE=0"
|
||||||
|
|
||||||
GOTO getopts
|
GOTO getopts
|
||||||
:help
|
:help
|
||||||
ECHO Flash image file to device, but leave existing system intact.
|
ECHO Flash image file to device, but leave existing system intact.
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python]
|
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--change-mode]
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Options:
|
ECHO Options:
|
||||||
ECHO -f filename The update .bin file to flash. Custom to your device type and region. (required)
|
ECHO -f filename The update .bin file to flash. Custom to your device type and region. (required)
|
||||||
@@ -23,12 +24,15 @@ ECHO If not set, ESPTOOL iterates all ports (Dangerous).
|
|||||||
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
||||||
ECHO If supplied the script will use python.
|
ECHO If supplied the script will use python.
|
||||||
ECHO If not supplied the script will try to find esptool in Path.
|
ECHO If not supplied the script will try to find esptool in Path.
|
||||||
|
ECHO --change-mode Attempt to place the device in correct mode. (1200bps Reset)
|
||||||
|
ECHO Some hardware requires this twice.
|
||||||
ECHO.
|
ECHO.
|
||||||
|
ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode
|
||||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11
|
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11
|
||||||
GOTO eof
|
GOTO eof
|
||||||
|
|
||||||
:version
|
:version
|
||||||
ECHO %SCRIPT_NAME% [Version 2.6.1]
|
ECHO %SCRIPT_NAME% [Version 2.6.2]
|
||||||
ECHO Meshtastic
|
ECHO Meshtastic
|
||||||
GOTO eof
|
GOTO eof
|
||||||
|
|
||||||
@@ -44,10 +48,13 @@ IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
|
|||||||
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||||
|
IF /I "%~1"=="--change-mode" SET "CHANGE_MODE=1"
|
||||||
SHIFT
|
SHIFT
|
||||||
GOTO getopts
|
GOTO getopts
|
||||||
:endopts
|
:endopts
|
||||||
|
|
||||||
|
IF %CHANGE_MODE% EQU 1 GOTO skip-filename
|
||||||
|
|
||||||
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
|
||||||
IF "__!FILENAME!__"=="____" (
|
IF "__!FILENAME!__"=="____" (
|
||||||
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
|
||||||
@@ -77,6 +84,9 @@ IF "!FILENAME:update=!"=="!FILENAME!" (
|
|||||||
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
:skip-filename
|
||||||
|
SET "ESPTOOL_BAUD=1200"
|
||||||
|
|
||||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||||
IF NOT "__%PYTHON%__"=="____" (
|
IF NOT "__%PYTHON%__"=="____" (
|
||||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||||
@@ -115,6 +125,12 @@ IF "__!ESPTOOL_PORT!__" == "____" (
|
|||||||
)
|
)
|
||||||
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||||
|
|
||||||
|
IF %CHANGE_MODE% EQU 1 (
|
||||||
|
@REM Attempt to change mode via 1200bps Reset.
|
||||||
|
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
|
||||||
|
GOTO eof
|
||||||
|
)
|
||||||
|
|
||||||
@REM Flashing operations.
|
@REM Flashing operations.
|
||||||
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
|
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
|
||||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
|
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
|
||||||
@@ -135,6 +151,7 @@ EXIT /B %ERRORLEVEL%
|
|||||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||||
CALL :RESET_ERROR
|
CALL :RESET_ERROR
|
||||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||||
|
IF %CHANGE_MODE% EQU 1 GOTO :eof
|
||||||
IF %ERRORLEVEL% NEQ 0 (
|
IF %ERRORLEVEL% NEQ 0 (
|
||||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||||
EXIT /B %ERRORLEVEL%
|
EXIT /B %ERRORLEVEL%
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
||||||
|
CHANGE_MODE=false
|
||||||
|
|
||||||
# Determine the correct esptool command to use
|
# Determine the correct esptool command to use
|
||||||
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
|
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
|
||||||
@@ -17,13 +18,14 @@ fi
|
|||||||
# Usage info
|
# Usage info
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME]
|
Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--change-mode]
|
||||||
Flash image file to device, leave existing system intact."
|
Flash image file to device, leave existing system intact."
|
||||||
|
|
||||||
-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 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")
|
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
||||||
-f FILENAME The *update.bin file to flash. Custom to your device type.
|
-f FILENAME The *update.bin file to flash. Custom to your device type.
|
||||||
|
--change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -41,6 +43,9 @@ while getopts ":hp:P:f:" opt; do
|
|||||||
;;
|
;;
|
||||||
f) FILENAME=${OPTARG}
|
f) FILENAME=${OPTARG}
|
||||||
;;
|
;;
|
||||||
|
--change-mode)
|
||||||
|
CHANGE_MODE=true
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Invalid flag."
|
echo "Invalid flag."
|
||||||
show_help >&2
|
show_help >&2
|
||||||
@@ -50,14 +55,19 @@ while getopts ":hp:P:f:" opt; do
|
|||||||
done
|
done
|
||||||
shift "$((OPTIND-1))"
|
shift "$((OPTIND-1))"
|
||||||
|
|
||||||
[ -z "$FILENAME" -a -n "$1" ] && {
|
if [[ $CHANGE_MODE == true ]]; then
|
||||||
FILENAME=$1
|
$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||||
|
FILENAME="$1"
|
||||||
shift
|
shift
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
|
if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
|
||||||
printf "Trying to flash update ${FILENAME}"
|
echo "Trying to flash update ${FILENAME}"
|
||||||
$ESPTOOL_CMD --baud 115200 write_flash 0x10000 ${FILENAME}
|
$ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}"
|
||||||
else
|
else
|
||||||
show_help
|
show_help
|
||||||
echo "Invalid file: ${FILENAME}"
|
echo "Invalid file: ${FILENAME}"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ for subdir, dirs, files in os.walk(rootdir):
|
|||||||
if c.startswith("env:"):
|
if c.startswith("env:"):
|
||||||
section = config[c].name[4:]
|
section = config[c].name[4:]
|
||||||
if "extends" in config[config[c].name]:
|
if "extends" in config[config[c].name]:
|
||||||
if config[config[c].name]["extends"] == options[0] + "_base":
|
if options[0] + "_base" in config[config[c].name]["extends"]:
|
||||||
if "board_level" in config[config[c].name]:
|
if "board_level" in config[config[c].name]:
|
||||||
if (
|
if (
|
||||||
config[config[c].name]["board_level"] == "extra"
|
config[config[c].name]["board_level"] == "extra"
|
||||||
|
|||||||
@@ -87,6 +87,21 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.7.1" date="2025-06-21">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.0" date="2025-06-20">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.6.13" date="2025-06-16">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.13</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.6.12" date="2025-06-15">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.12</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.6.11" date="2025-06-02">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.11</url>
|
||||||
|
</release>
|
||||||
<release version="2.6.10" date="2025-05-25">
|
<release version="2.6.10" date="2025-05-25">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.10</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.10</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
@@ -48,6 +48,6 @@
|
|||||||
"require_upload_port": true,
|
"require_upload_port": true,
|
||||||
"wait_for_upload_port": true
|
"wait_for_upload_port": true
|
||||||
},
|
},
|
||||||
"url": "FIXME",
|
"url": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html",
|
||||||
"vendor": "ELECROW"
|
"vendor": "ELECROW"
|
||||||
}
|
}
|
||||||
|
|||||||
52
boards/gat562_mesh_trial_tracker.json
Normal file
52
boards/gat562_mesh_trial_tracker.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "nrf52840_s140_v6.ld"
|
||||||
|
},
|
||||||
|
"core": "nRF5",
|
||||||
|
"cpu": "cortex-m4",
|
||||||
|
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
|
||||||
|
"f_cpu": "64000000L",
|
||||||
|
"hwids": [
|
||||||
|
["0x239A", "0x8029"],
|
||||||
|
["0x239A", "0x0029"],
|
||||||
|
["0x239A", "0x002A"],
|
||||||
|
["0x239A", "0x802A"]
|
||||||
|
],
|
||||||
|
"usb_product": "GAT562 Mesh Trial Tracker",
|
||||||
|
"mcu": "nrf52840",
|
||||||
|
"variant": "gat562_mesh_trial_tracker",
|
||||||
|
"bsp": {
|
||||||
|
"name": "adafruit"
|
||||||
|
},
|
||||||
|
"softdevice": {
|
||||||
|
"sd_flags": "-DS140",
|
||||||
|
"sd_name": "s140",
|
||||||
|
"sd_version": "6.1.1",
|
||||||
|
"sd_fwid": "0x00B6"
|
||||||
|
},
|
||||||
|
"bootloader": {
|
||||||
|
"settings_addr": "0xFF000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectivity": ["bluetooth"],
|
||||||
|
"debug": {
|
||||||
|
"jlink_device": "nRF52840_xxAA",
|
||||||
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino", "freertos"],
|
||||||
|
"name": "GAT562 Mesh Trial Tracker",
|
||||||
|
"upload": {
|
||||||
|
"maximum_ram_size": 248832,
|
||||||
|
"maximum_size": 815104,
|
||||||
|
"speed": 115200,
|
||||||
|
"protocol": "nrfutil",
|
||||||
|
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"wait_for_upload_port": true
|
||||||
|
},
|
||||||
|
"url": "http://www.gat-iot.com/",
|
||||||
|
"vendor": "GAT-IOT"
|
||||||
|
}
|
||||||
@@ -48,6 +48,6 @@
|
|||||||
"require_upload_port": true,
|
"require_upload_port": true,
|
||||||
"wait_for_upload_port": true
|
"wait_for_upload_port": true
|
||||||
},
|
},
|
||||||
"url": "FIXME",
|
"url": "https://heltec.org/project/mesh-node-t114/",
|
||||||
"vendor": "Heltec"
|
"vendor": "Heltec"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
"cpu": "cortex-m4",
|
"cpu": "cortex-m4",
|
||||||
"extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA",
|
"extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA",
|
||||||
"f_cpu": "64000000L",
|
"f_cpu": "64000000L",
|
||||||
"hwids": [
|
"hwids": [["0x2886", "0x0166"]],
|
||||||
["0x2886", "0x0166"]
|
|
||||||
],
|
|
||||||
"usb_product": "XIAO-BOOT",
|
"usb_product": "XIAO-BOOT",
|
||||||
"mcu": "nrf52840",
|
"mcu": "nrf52840",
|
||||||
"variant": "seeed_xiao_nrf52840_kit",
|
"variant": "seeed_xiao_nrf52840_kit",
|
||||||
|
|||||||
13
debian/changelog
vendored
13
debian/changelog
vendored
@@ -1,4 +1,4 @@
|
|||||||
meshtasticd (2.6.10.0) UNRELEASED; urgency=medium
|
meshtasticd (2.7.1.0) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
[ Austin Lane ]
|
[ Austin Lane ]
|
||||||
* Initial packaging
|
* Initial packaging
|
||||||
@@ -16,4 +16,13 @@ meshtasticd (2.6.10.0) UNRELEASED; urgency=medium
|
|||||||
[ ]
|
[ ]
|
||||||
* GitHub Actions Automatic version bump
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
-- <github-actions[bot]@users.noreply.github.com> Sun, 25 May 2025 20:46:49 +0000
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
-- <github-actions[bot]@users.noreply.github.com> Sat, 21 Jun 2025 15:51:49 +0000
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||||
|
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||||
#-DBUILD_EPOCH=$UNIX_TIME
|
#-DBUILD_EPOCH=$UNIX_TIME
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
|
|
||||||
@@ -103,12 +104,12 @@ lib_deps =
|
|||||||
[radiolib_base]
|
[radiolib_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||||
jgromes/RadioLib@7.1.2
|
jgromes/RadioLib@7.2.0
|
||||||
|
|
||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/37e2fb84a8d1b7d8cc1e2ed00d34cfb1f284bd59.zip
|
https://github.com/meshtastic/device-ui/archive/cdc6e5bdeedb8293d10e4a02be6ca64e95a7c515.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
@@ -165,6 +166,8 @@ lib_deps =
|
|||||||
adafruit/Adafruit LTR390 Library@1.1.2
|
adafruit/Adafruit LTR390 Library@1.1.2
|
||||||
# renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075
|
# renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075
|
||||||
adafruit/Adafruit PCT2075@1.0.5
|
adafruit/Adafruit PCT2075@1.0.5
|
||||||
|
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
|
||||||
|
dfrobot/DFRobot_BMM150@1.0.0
|
||||||
|
|
||||||
; (not included in native / portduino)
|
; (not included in native / portduino)
|
||||||
[environmental_extra]
|
[environmental_extra]
|
||||||
|
|||||||
Submodule protobufs updated: 24c7a3d287...386fa53c15
@@ -59,12 +59,12 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG_DEBUG("AmbientLighting init");
|
LOG_DEBUG("AmbientLighting init");
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#ifdef HAS_NCP5623
|
||||||
if (_type == ScanI2C::NCP5623) {
|
if (_type == ScanI2C::NCP5623) {
|
||||||
rgb.begin();
|
rgb.begin();
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_LP5562
|
#ifdef HAS_LP5562
|
||||||
} else if (_type == ScanI2C::LP5562) {
|
if (_type == ScanI2C::LP5562) {
|
||||||
rgbw.begin();
|
rgbw.begin();
|
||||||
#endif
|
#endif
|
||||||
#ifdef RGBLED_RED
|
#ifdef RGBLED_RED
|
||||||
@@ -155,8 +155,9 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
rgb.setRed(moduleConfig.ambient_lighting.red);
|
rgb.setRed(moduleConfig.ambient_lighting.red);
|
||||||
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
||||||
rgb.setBlue(moduleConfig.ambient_lighting.blue);
|
rgb.setBlue(moduleConfig.ambient_lighting.blue);
|
||||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
|
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
|
||||||
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_LP5562
|
#ifdef HAS_LP5562
|
||||||
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
||||||
@@ -183,8 +184,8 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
#endif
|
#endif
|
||||||
pixels.show();
|
pixels.show();
|
||||||
LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
||||||
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||||
moduleConfig.ambient_lighting.blue);
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef RGBLED_CA
|
#ifdef RGBLED_CA
|
||||||
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
||||||
@@ -200,7 +201,8 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef UNPHONE
|
#ifdef UNPHONE
|
||||||
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||||
|
moduleConfig.ambient_lighting.blue);
|
||||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ class AudioThread : public concurrency::OSThread
|
|||||||
setCPUFast(false);
|
setCPUFast(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void readAloud(const char *text)
|
||||||
|
{
|
||||||
|
if (i2sRtttl != nullptr) {
|
||||||
|
i2sRtttl->stop();
|
||||||
|
delete i2sRtttl;
|
||||||
|
i2sRtttl = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP8266SAM *sam = new ESP8266SAM;
|
||||||
|
sam->Say(audioOut, text);
|
||||||
|
delete sam;
|
||||||
|
setCPUFast(false);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int32_t runOnce() override
|
int32_t runOnce() override
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,10 +88,16 @@ class BluetoothStatus : public Status
|
|||||||
break;
|
break;
|
||||||
case ConnectionState::CONNECTED:
|
case ConnectionState::CONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||||
|
#ifdef BLE_LED
|
||||||
|
digitalWrite(BLE_LED, HIGH);
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ConnectionState::DISCONNECTED:
|
case ConnectionState::DISCONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||||
|
#ifdef BLE_LED
|
||||||
|
digitalWrite(BLE_LED, LOW);
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,467 +0,0 @@
|
|||||||
#include "ButtonThread.h"
|
|
||||||
|
|
||||||
#include "configuration.h"
|
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
|
||||||
#include "GPS.h"
|
|
||||||
#endif
|
|
||||||
#include "MeshService.h"
|
|
||||||
#include "PowerFSM.h"
|
|
||||||
#include "RadioLibInterface.h"
|
|
||||||
#include "buzz.h"
|
|
||||||
#include "main.h"
|
|
||||||
#include "modules/ExternalNotificationModule.h"
|
|
||||||
#include "power.h"
|
|
||||||
#include "sleep.h"
|
|
||||||
#ifdef ARCH_PORTDUINO
|
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define DEBUG_BUTTONS 0
|
|
||||||
#if DEBUG_BUTTONS
|
|
||||||
#define LOG_BUTTON(...) LOG_DEBUG(__VA_ARGS__)
|
|
||||||
#else
|
|
||||||
#define LOG_BUTTON(...)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace concurrency;
|
|
||||||
|
|
||||||
ButtonThread *buttonThread; // Declared extern in header
|
|
||||||
volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE;
|
|
||||||
|
|
||||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
|
||||||
OneButton ButtonThread::userButton; // Get reference to static member
|
|
||||||
#endif
|
|
||||||
ButtonThread::ButtonThread() : OSThread("Button")
|
|
||||||
{
|
|
||||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
|
||||||
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) {
|
|
||||||
this->userButton = OneButton(settingsMap[user], true, true);
|
|
||||||
LOG_DEBUG("Use GPIO%02d for button", settingsMap[user]);
|
|
||||||
}
|
|
||||||
#elif defined(BUTTON_PIN)
|
|
||||||
#if !defined(USERPREFS_BUTTON_PIN)
|
|
||||||
int pin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; // Resolved button pin
|
|
||||||
#endif
|
|
||||||
#ifdef USERPREFS_BUTTON_PIN
|
|
||||||
int pin = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; // Resolved button pin
|
|
||||||
#endif
|
|
||||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
|
||||||
this->userButton = OneButton(pin, false, false);
|
|
||||||
#elif defined(BUTTON_ACTIVE_LOW)
|
|
||||||
this->userButton = OneButton(pin, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP);
|
|
||||||
#else
|
|
||||||
this->userButton = OneButton(pin, true, true);
|
|
||||||
#endif
|
|
||||||
LOG_DEBUG("Use GPIO%02d for button", pin);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef INPUT_PULLUP_SENSE
|
|
||||||
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
|
|
||||||
#ifdef BUTTON_SENSE_TYPE
|
|
||||||
pinMode(pin, BUTTON_SENSE_TYPE);
|
|
||||||
#else
|
|
||||||
pinMode(pin, INPUT_PULLUP_SENSE);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
|
||||||
userButton.attachClick(userButtonPressed);
|
|
||||||
userButton.setClickMs(BUTTON_CLICK_MS);
|
|
||||||
userButton.setPressMs(BUTTON_LONGPRESS_MS);
|
|
||||||
userButton.setDebounceMs(1);
|
|
||||||
userButton.attachDoubleClick(userButtonDoublePressed);
|
|
||||||
userButton.attachMultiClick(userButtonMultiPressed, this); // Reference to instance: get click count from non-static OneButton
|
|
||||||
#if !defined(T_DECK) && \
|
|
||||||
!defined( \
|
|
||||||
ELECROW_ThinkNode_M2) // T-Deck immediately wakes up after shutdown, Thinknode M2 has this on the smaller ALT button
|
|
||||||
userButton.attachLongPressStart(userButtonPressedLongStart);
|
|
||||||
userButton.attachLongPressStop(userButtonPressedLongStop);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_ALT
|
|
||||||
#if defined(ELECROW_ThinkNode_M2)
|
|
||||||
this->userButtonAlt = OneButton(BUTTON_PIN_ALT, false, false);
|
|
||||||
#else
|
|
||||||
this->userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true);
|
|
||||||
#endif
|
|
||||||
#ifdef INPUT_PULLUP_SENSE
|
|
||||||
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
|
|
||||||
pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE);
|
|
||||||
#endif
|
|
||||||
userButtonAlt.attachClick(userButtonPressedScreen);
|
|
||||||
userButtonAlt.setClickMs(BUTTON_CLICK_MS);
|
|
||||||
userButtonAlt.setPressMs(BUTTON_LONGPRESS_MS);
|
|
||||||
userButtonAlt.setDebounceMs(1);
|
|
||||||
userButtonAlt.attachLongPressStart(userButtonPressedLongStart);
|
|
||||||
userButtonAlt.attachLongPressStop(userButtonPressedLongStop);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
userButtonTouch = OneButton(BUTTON_PIN_TOUCH, true, true);
|
|
||||||
userButtonTouch.setPressMs(BUTTON_TOUCH_MS);
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
void ButtonThread::switchPage()
|
|
||||||
{
|
|
||||||
#ifdef BUTTON_PIN
|
|
||||||
#if !defined(USERPREFS_BUTTON_PIN)
|
|
||||||
if (((config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN) !=
|
|
||||||
moduleConfig.canned_message.inputbroker_pin_press) ||
|
|
||||||
!(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) ||
|
|
||||||
!moduleConfig.canned_message.enabled) {
|
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#if defined(USERPREFS_BUTTON_PIN)
|
|
||||||
if (((config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN) !=
|
|
||||||
moduleConfig.canned_message.inputbroker_pin_press) ||
|
|
||||||
!(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) ||
|
|
||||||
!moduleConfig.canned_message.enabled) {
|
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
if ((settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) &&
|
|
||||||
(settingsMap[user] != moduleConfig.canned_message.inputbroker_pin_press) ||
|
|
||||||
!moduleConfig.canned_message.enabled) {
|
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void ButtonThread::sendAdHocPosition()
|
|
||||||
{
|
|
||||||
service->refreshLocalMeshNode();
|
|
||||||
auto sentPosition = service->trySendPosition(NODENUM_BROADCAST, true);
|
|
||||||
if (screen) {
|
|
||||||
if (sentPosition)
|
|
||||||
screen->print("Sent ad-hoc position\n");
|
|
||||||
else
|
|
||||||
screen->print("Sent ad-hoc nodeinfo\n");
|
|
||||||
screen->forceDisplay(true); // Force a new UI frame, then force an EInk update
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t ButtonThread::runOnce()
|
|
||||||
{
|
|
||||||
// If the button is pressed we suppress CPU sleep until release
|
|
||||||
canSleep = true; // Assume we should not keep the board awake
|
|
||||||
|
|
||||||
#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN)
|
|
||||||
userButton.tick();
|
|
||||||
canSleep &= userButton.isIdle();
|
|
||||||
#elif defined(ARCH_PORTDUINO)
|
|
||||||
if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) {
|
|
||||||
userButton.tick();
|
|
||||||
canSleep &= userButton.isIdle();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#ifdef BUTTON_PIN_ALT
|
|
||||||
userButtonAlt.tick();
|
|
||||||
canSleep &= userButtonAlt.isIdle();
|
|
||||||
#endif
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
userButtonTouch.tick();
|
|
||||||
canSleep &= userButtonTouch.isIdle();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (btnEvent != BUTTON_EVENT_NONE) {
|
|
||||||
switch (btnEvent) {
|
|
||||||
case BUTTON_EVENT_PRESSED: {
|
|
||||||
LOG_BUTTON("press!");
|
|
||||||
// If a nag notification is running, stop it and prevent other actions
|
|
||||||
if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) {
|
|
||||||
externalNotificationModule->stopNow();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
|
||||||
sendAdHocPosition();
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
switchPage();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case BUTTON_EVENT_PRESSED_SCREEN: {
|
|
||||||
LOG_BUTTON("AltPress!");
|
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
|
||||||
// If a nag notification is running, stop it and prevent other actions
|
|
||||||
if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) {
|
|
||||||
externalNotificationModule->stopNow();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switchPage();
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
// turn screen on or off
|
|
||||||
screen_flag = !screen_flag;
|
|
||||||
if (screen)
|
|
||||||
screen->setOn(screen_flag);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case BUTTON_EVENT_DOUBLE_PRESSED: {
|
|
||||||
LOG_BUTTON("Double press!");
|
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
|
||||||
digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW);
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
sendAdHocPosition();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case BUTTON_EVENT_MULTI_PRESSED: {
|
|
||||||
LOG_BUTTON("Mulitipress! %hux", multipressClickCount);
|
|
||||||
switch (multipressClickCount) {
|
|
||||||
#if HAS_GPS && !defined(ELECROW_ThinkNode_M1)
|
|
||||||
// 3 clicks: toggle GPS
|
|
||||||
case 3:
|
|
||||||
if (!config.device.disable_triple_click && (gps != nullptr)) {
|
|
||||||
gps->toggleGpsMode();
|
|
||||||
if (screen)
|
|
||||||
screen->forceDisplay(true); // Force a new UI frame, then force an EInk update
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
#elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
|
|
||||||
case 3:
|
|
||||||
LOG_INFO("3 clicks: toggle buzzer");
|
|
||||||
buzzer_flag = !buzzer_flag;
|
|
||||||
if (!buzzer_flag)
|
|
||||||
noTone(PIN_BUZZER);
|
|
||||||
break;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_EINK) && defined(PIN_EINK_EN) && !defined(ELECROW_ThinkNode_M1) // i.e. T-Echo
|
|
||||||
// 4 clicks: toggle backlight
|
|
||||||
case 4:
|
|
||||||
digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW);
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_SCREEN && HAS_SCREEN
|
|
||||||
// 5 clicks: start accelerometer/magenetometer calibration for 30 seconds
|
|
||||||
case 5:
|
|
||||||
if (accelerometerThread) {
|
|
||||||
accelerometerThread->calibrate(30);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// 6 clicks: start accelerometer/magenetometer calibration for 60 seconds
|
|
||||||
case 6:
|
|
||||||
if (accelerometerThread) {
|
|
||||||
accelerometerThread->calibrate(60);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
// No valid multipress action
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
} // end switch: click count
|
|
||||||
|
|
||||||
break;
|
|
||||||
} // end multipress event
|
|
||||||
|
|
||||||
case BUTTON_EVENT_LONG_PRESSED: {
|
|
||||||
LOG_BUTTON("Long press!");
|
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
|
||||||
if (screen) {
|
|
||||||
screen->startAlert("Shutting down...");
|
|
||||||
}
|
|
||||||
playBeep();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do actual shutdown when button released, otherwise the button release
|
|
||||||
// may wake the board immediatedly.
|
|
||||||
case BUTTON_EVENT_LONG_RELEASED: {
|
|
||||||
LOG_INFO("Shutdown from long press");
|
|
||||||
playShutdownMelody();
|
|
||||||
delay(3000);
|
|
||||||
power->shutdown();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
case BUTTON_EVENT_TOUCH_LONG_PRESSED: {
|
|
||||||
LOG_BUTTON("Touch press!");
|
|
||||||
// Ignore if: no screen
|
|
||||||
if (!screen)
|
|
||||||
break;
|
|
||||||
|
|
||||||
#ifdef TTGO_T_ECHO
|
|
||||||
// Ignore if: TX in progress
|
|
||||||
// Uncommon T-Echo hardware bug, LoRa TX triggers touch button
|
|
||||||
if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending())
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Wake if asleep
|
|
||||||
if (powerFSM.getState() == &stateDARK)
|
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
|
||||||
|
|
||||||
// Update display (legacy behaviour)
|
|
||||||
screen->forceDisplay();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif // BUTTON_PIN_TOUCH
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
btnEvent = BUTTON_EVENT_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Attach (or re-attach) hardware interrupts for buttons
|
|
||||||
* Public method. Used outside class when waking from MCU sleep
|
|
||||||
*/
|
|
||||||
void ButtonThread::attachButtonInterrupts()
|
|
||||||
{
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC)
|
|
||||||
wakeOnIrq(settingsMap[user], FALLING);
|
|
||||||
#elif defined(BUTTON_PIN)
|
|
||||||
// Interrupt for user button, during normal use. Improves responsiveness.
|
|
||||||
attachInterrupt(
|
|
||||||
#if !defined(USERPREFS_BUTTON_PIN)
|
|
||||||
config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN,
|
|
||||||
#endif
|
|
||||||
#if defined(USERPREFS_BUTTON_PIN)
|
|
||||||
config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN,
|
|
||||||
#endif
|
|
||||||
[]() {
|
|
||||||
ButtonThread::userButton.tick();
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
},
|
|
||||||
CHANGE);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_ALT
|
|
||||||
#ifdef ELECROW_ThinkNode_M2
|
|
||||||
wakeOnIrq(BUTTON_PIN_ALT, RISING);
|
|
||||||
#else
|
|
||||||
wakeOnIrq(BUTTON_PIN_ALT, FALLING);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
wakeOnIrq(BUTTON_PIN_TOUCH, FALLING);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Detach the "normal" button interrupts.
|
|
||||||
* Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep
|
|
||||||
*/
|
|
||||||
void ButtonThread::detachButtonInterrupts()
|
|
||||||
{
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC)
|
|
||||||
detachInterrupt(settingsMap[user]);
|
|
||||||
#elif defined(BUTTON_PIN)
|
|
||||||
#if !defined(USERPREFS_BUTTON_PIN)
|
|
||||||
detachInterrupt(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN);
|
|
||||||
#endif
|
|
||||||
#if defined(USERPREFS_BUTTON_PIN)
|
|
||||||
detachInterrupt(config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_ALT
|
|
||||||
detachInterrupt(BUTTON_PIN_ALT);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
detachInterrupt(BUTTON_PIN_TOUCH);
|
|
||||||
#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
|
|
||||||
*/
|
|
||||||
void ButtonThread::wakeOnIrq(int irq, int mode)
|
|
||||||
{
|
|
||||||
attachInterrupt(
|
|
||||||
irq,
|
|
||||||
[] {
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
runASAP = true;
|
|
||||||
},
|
|
||||||
FALLING);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static callback
|
|
||||||
void ButtonThread::userButtonMultiPressed(void *callerThread)
|
|
||||||
{
|
|
||||||
// Grab click count from non-static button, while the info is still valid
|
|
||||||
ButtonThread *thread = (ButtonThread *)callerThread;
|
|
||||||
thread->storeClickCount();
|
|
||||||
|
|
||||||
// Then handle later, in the usual way
|
|
||||||
btnEvent = BUTTON_EVENT_MULTI_PRESSED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-static method, runs during callback. Grabs info while still valid
|
|
||||||
void ButtonThread::storeClickCount()
|
|
||||||
{
|
|
||||||
#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN)
|
|
||||||
multipressClickCount = userButton.getNumberClicks();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void ButtonThread::userButtonPressedLongStart()
|
|
||||||
{
|
|
||||||
if (millis() > c_holdOffTime) {
|
|
||||||
btnEvent = BUTTON_EVENT_LONG_PRESSED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ButtonThread::userButtonPressedLongStop()
|
|
||||||
{
|
|
||||||
if (millis() > c_holdOffTime) {
|
|
||||||
btnEvent = BUTTON_EVENT_LONG_RELEASED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OneButton.h"
|
|
||||||
#include "concurrency/OSThread.h"
|
|
||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#ifndef BUTTON_CLICK_MS
|
|
||||||
#define BUTTON_CLICK_MS 250
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef BUTTON_LONGPRESS_MS
|
|
||||||
#define BUTTON_LONGPRESS_MS 5000
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef BUTTON_TOUCH_MS
|
|
||||||
#define BUTTON_TOUCH_MS 400
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class ButtonThread : public concurrency::OSThread
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot
|
|
||||||
|
|
||||||
enum ButtonEventType {
|
|
||||||
BUTTON_EVENT_NONE,
|
|
||||||
BUTTON_EVENT_PRESSED,
|
|
||||||
BUTTON_EVENT_PRESSED_SCREEN,
|
|
||||||
BUTTON_EVENT_DOUBLE_PRESSED,
|
|
||||||
BUTTON_EVENT_MULTI_PRESSED,
|
|
||||||
BUTTON_EVENT_LONG_PRESSED,
|
|
||||||
BUTTON_EVENT_LONG_RELEASED,
|
|
||||||
BUTTON_EVENT_TOUCH_LONG_PRESSED,
|
|
||||||
};
|
|
||||||
|
|
||||||
ButtonThread();
|
|
||||||
int32_t runOnce() override;
|
|
||||||
void attachButtonInterrupts();
|
|
||||||
void detachButtonInterrupts();
|
|
||||||
void storeClickCount();
|
|
||||||
bool isBuzzing() { return buzzer_flag; }
|
|
||||||
void setScreenFlag(bool flag) { screen_flag = flag; }
|
|
||||||
bool getScreenFlag() { return screen_flag; }
|
|
||||||
|
|
||||||
// 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
|
|
||||||
#endif
|
|
||||||
#ifdef BUTTON_PIN_ALT
|
|
||||||
OneButton userButtonAlt;
|
|
||||||
#endif
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
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;
|
|
||||||
bool buzzer_flag = false;
|
|
||||||
bool screen_flag = true;
|
|
||||||
|
|
||||||
// Store click count during callback, for later use
|
|
||||||
volatile int multipressClickCount = 0;
|
|
||||||
|
|
||||||
static void wakeOnIrq(int irq, int mode);
|
|
||||||
|
|
||||||
static void sendAdHocPosition();
|
|
||||||
static void switchPage();
|
|
||||||
|
|
||||||
// IRQ callbacks
|
|
||||||
static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; }
|
|
||||||
static void userButtonPressedScreen() { btnEvent = BUTTON_EVENT_PRESSED_SCREEN; }
|
|
||||||
static void userButtonDoublePressed() { btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; }
|
|
||||||
static void userButtonMultiPressed(void *callerThread); // Retrieve click count from non-static Onebutton while still valid
|
|
||||||
static void userButtonPressedLongStart();
|
|
||||||
static void userButtonPressedLongStop();
|
|
||||||
static void touchPressedLongStart() { btnEvent = BUTTON_EVENT_TOUCH_LONG_PRESSED; }
|
|
||||||
};
|
|
||||||
|
|
||||||
extern ButtonThread *buttonThread;
|
|
||||||
@@ -661,12 +661,14 @@ bool Power::analogInit()
|
|||||||
*/
|
*/
|
||||||
bool Power::setup()
|
bool Power::setup()
|
||||||
{
|
{
|
||||||
// initialise one power sensor (only)
|
bool found = false;
|
||||||
bool found = axpChipInit();
|
if (axpChipInit()) {
|
||||||
if (!found)
|
found = true;
|
||||||
found = lipoInit();
|
} else if (lipoInit()) {
|
||||||
if (!found)
|
found = true;
|
||||||
found = analogInit();
|
} else if (analogInit()) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef NRF_APM
|
#ifdef NRF_APM
|
||||||
found = true;
|
found = true;
|
||||||
@@ -853,6 +855,7 @@ int32_t Power::runOnce()
|
|||||||
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
|
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
|
||||||
if (PMU->isPekeyLongPressIrq()) {
|
if (PMU->isPekeyLongPressIrq()) {
|
||||||
LOG_DEBUG("PEK long button press");
|
LOG_DEBUG("PEK long button press");
|
||||||
|
if (screen)
|
||||||
screen->setOn(false);
|
screen->setOn(false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#ifndef SLEEP_TIME
|
#ifndef SLEEP_TIME
|
||||||
#define SLEEP_TIME 30
|
#define SLEEP_TIME 30
|
||||||
#endif
|
#endif
|
||||||
#if EXCLUDE_POWER_FSM
|
#if MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
FakeFsm powerFSM;
|
FakeFsm powerFSM;
|
||||||
void PowerFSM_setup(){};
|
void PowerFSM_setup(){};
|
||||||
#else
|
#else
|
||||||
@@ -82,6 +82,7 @@ static uint32_t secsSlept;
|
|||||||
static void lsEnter()
|
static void lsEnter()
|
||||||
{
|
{
|
||||||
LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs);
|
LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs);
|
||||||
|
if (screen)
|
||||||
screen->setOn(false);
|
screen->setOn(false);
|
||||||
secsSlept = 0; // How long have we been sleeping this time
|
secsSlept = 0; // How long have we been sleeping this time
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ static void lsExit()
|
|||||||
static void nbEnter()
|
static void nbEnter()
|
||||||
{
|
{
|
||||||
LOG_DEBUG("State: NB");
|
LOG_DEBUG("State: NB");
|
||||||
|
if (screen)
|
||||||
screen->setOn(false);
|
screen->setOn(false);
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
// Only ESP32 should turn off bluetooth
|
// Only ESP32 should turn off bluetooth
|
||||||
@@ -172,6 +174,7 @@ static void nbEnter()
|
|||||||
static void darkEnter()
|
static void darkEnter()
|
||||||
{
|
{
|
||||||
setBluetoothEnable(true);
|
setBluetoothEnable(true);
|
||||||
|
if (screen)
|
||||||
screen->setOn(false);
|
screen->setOn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,15 +182,15 @@ static void serialEnter()
|
|||||||
{
|
{
|
||||||
LOG_DEBUG("State: SERIAL");
|
LOG_DEBUG("State: SERIAL");
|
||||||
setBluetoothEnable(false);
|
setBluetoothEnable(false);
|
||||||
|
if (screen) {
|
||||||
screen->setOn(true);
|
screen->setOn(true);
|
||||||
screen->print("Serial connected\n");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void serialExit()
|
static void serialExit()
|
||||||
{
|
{
|
||||||
// Turn bluetooth back on when we leave serial stream API
|
// Turn bluetooth back on when we leave serial stream API
|
||||||
setBluetoothEnable(true);
|
setBluetoothEnable(true);
|
||||||
screen->print("Serial disconnected\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void powerEnter()
|
static void powerEnter()
|
||||||
@@ -198,15 +201,10 @@ static void powerEnter()
|
|||||||
LOG_INFO("Loss of power in Powered");
|
LOG_INFO("Loss of power in Powered");
|
||||||
powerFSM.trigger(EVENT_POWER_DISCONNECTED);
|
powerFSM.trigger(EVENT_POWER_DISCONNECTED);
|
||||||
} else {
|
} else {
|
||||||
|
if (screen)
|
||||||
screen->setOn(true);
|
screen->setOn(true);
|
||||||
setBluetoothEnable(true);
|
setBluetoothEnable(true);
|
||||||
// within enter() the function getState() returns the state we came from
|
// within enter() the function getState() returns the state we came from
|
||||||
|
|
||||||
// Mothballed: print change of power-state to device screen
|
|
||||||
/* if (strcmp(powerFSM.getState()->name, "BOOT") != 0 && strcmp(powerFSM.getState()->name, "POWER") != 0 &&
|
|
||||||
strcmp(powerFSM.getState()->name, "DARK") != 0) {
|
|
||||||
screen->print("Powered...\n");
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,17 +219,15 @@ static void powerIdle()
|
|||||||
|
|
||||||
static void powerExit()
|
static void powerExit()
|
||||||
{
|
{
|
||||||
|
if (screen)
|
||||||
screen->setOn(true);
|
screen->setOn(true);
|
||||||
setBluetoothEnable(true);
|
setBluetoothEnable(true);
|
||||||
|
|
||||||
// Mothballed: print change of power-state to device screen
|
|
||||||
/*if (!isPowered())
|
|
||||||
screen->print("Unpowered...\n");*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onEnter()
|
static void onEnter()
|
||||||
{
|
{
|
||||||
LOG_DEBUG("State: ON");
|
LOG_DEBUG("State: ON");
|
||||||
|
if (screen)
|
||||||
screen->setOn(true);
|
screen->setOn(true);
|
||||||
setBluetoothEnable(true);
|
setBluetoothEnable(true);
|
||||||
}
|
}
|
||||||
@@ -244,11 +240,6 @@ static void onIdle()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void screenPress()
|
|
||||||
{
|
|
||||||
screen->onPress();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void bootEnter()
|
static void bootEnter()
|
||||||
{
|
{
|
||||||
LOG_DEBUG("State: BOOT");
|
LOG_DEBUG("State: BOOT");
|
||||||
@@ -292,9 +283,9 @@ void PowerFSM_setup()
|
|||||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press");
|
powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press");
|
||||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press");
|
powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press");
|
||||||
powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press");
|
powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press");
|
||||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, screenPress, "Press");
|
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press");
|
||||||
powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress, "Press"); // reenter On to restart our timers
|
powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers
|
||||||
powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, screenPress,
|
powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL,
|
||||||
"Press"); // Allow button to work while in serial API
|
"Press"); // Allow button to work while in serial API
|
||||||
|
|
||||||
// Handle critically low power battery by forcing deep sleep
|
// Handle critically low power battery by forcing deep sleep
|
||||||
@@ -328,10 +319,10 @@ void PowerFSM_setup()
|
|||||||
// if any packet destined for phone arrives, turn on bluetooth at least
|
// if any packet destined for phone arrives, turn on bluetooth at least
|
||||||
powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone");
|
powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone");
|
||||||
|
|
||||||
// show the latest node when we get a new node db update
|
// Removed 2.7: we don't show the nodes individually for every node on the screen anymore
|
||||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
|
// powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
|
||||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
|
// powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
|
||||||
powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
|
// powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
|
||||||
|
|
||||||
// Show the received text message
|
// Show the received text message
|
||||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text");
|
powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text");
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#define EVENT_RECEIVED_MSG 5
|
#define EVENT_RECEIVED_MSG 5
|
||||||
// #define EVENT_BOOT 6 // now done with a timed transition
|
// #define EVENT_BOOT 6 // now done with a timed transition
|
||||||
#define EVENT_BLUETOOTH_PAIR 7
|
#define EVENT_BLUETOOTH_PAIR 7
|
||||||
#define EVENT_NODEDB_UPDATED 8 // NodeDB has a big enough change that we think you should turn on the screen
|
// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen
|
||||||
#define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth
|
#define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth
|
||||||
#define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep
|
#define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep
|
||||||
#define EVENT_SERIAL_CONNECTED 11
|
#define EVENT_SERIAL_CONNECTED 11
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
#define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep)
|
#define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep)
|
||||||
#define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen
|
#define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen
|
||||||
|
|
||||||
#if EXCLUDE_POWER_FSM
|
#if MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
class FakeFsm
|
class FakeFsm
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PowerFSMThread : public OSThread
|
|||||||
protected:
|
protected:
|
||||||
int32_t runOnce() override
|
int32_t runOnce() override
|
||||||
{
|
{
|
||||||
#if !EXCLUDE_POWER_FSM
|
#if !MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
powerFSM.run_machine();
|
powerFSM.run_machine();
|
||||||
|
|
||||||
/// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake
|
/// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16
|
|||||||
if (i % 128 == 0)
|
if (i % 128 == 0)
|
||||||
log(logLevel, " +------------------------------------------------+ +----------------+");
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
||||||
char s[] = " | | | |\n";
|
char s[] = " | | | |\n";
|
||||||
uint8_t ix = 1, iy = 52;
|
uint8_t ix = 5, iy = 56;
|
||||||
for (uint8_t j = 0; j < 16; j++) {
|
for (uint8_t j = 0; j < 16; j++) {
|
||||||
if (i + j < len) {
|
if (i + j < len) {
|
||||||
uint8_t c = buf[i + j];
|
uint8_t c = buf[i + j];
|
||||||
@@ -367,10 +367,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
uint8_t index = i / 16;
|
uint8_t index = i / 16;
|
||||||
if (i < 256)
|
sprintf(s, "%03x", index);
|
||||||
log(logLevel, " ");
|
s[3] = '.';
|
||||||
log(logLevel, "%02x", index);
|
|
||||||
log(logLevel, ".");
|
|
||||||
log(logLevel, s);
|
log(logLevel, s);
|
||||||
}
|
}
|
||||||
log(logLevel, " +------------------------------------------------+ +----------------+");
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
||||||
|
|||||||
79
src/buzz/BuzzerFeedbackThread.cpp
Normal file
79
src/buzz/BuzzerFeedbackThread.cpp
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#include "BuzzerFeedbackThread.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "buzz.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
BuzzerFeedbackThread *buzzerFeedbackThread;
|
||||||
|
|
||||||
|
BuzzerFeedbackThread::BuzzerFeedbackThread() : OSThread("BuzzerFeedback")
|
||||||
|
{
|
||||||
|
if (inputBroker)
|
||||||
|
inputObserver.observe(inputBroker);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
||||||
|
{
|
||||||
|
// Only provide feedback if buzzer is enabled for notifications
|
||||||
|
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
|
||||||
|
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) {
|
||||||
|
return 0; // Let other handlers process the event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track last event time for potential future use
|
||||||
|
lastEventTime = millis();
|
||||||
|
needsUpdate = true;
|
||||||
|
|
||||||
|
// Handle different input events with appropriate buzzer feedback
|
||||||
|
switch (event->inputEvent) {
|
||||||
|
case INPUT_BROKER_USER_PRESS:
|
||||||
|
case INPUT_BROKER_ALT_PRESS:
|
||||||
|
case INPUT_BROKER_SELECT:
|
||||||
|
playBeep(); // Confirmation feedback
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BROKER_UP:
|
||||||
|
case INPUT_BROKER_DOWN:
|
||||||
|
case INPUT_BROKER_LEFT:
|
||||||
|
case INPUT_BROKER_RIGHT:
|
||||||
|
playChirp(); // Navigation feedback
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BROKER_CANCEL:
|
||||||
|
case INPUT_BROKER_BACK:
|
||||||
|
playBoop(); // Cancel/back feedback
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BROKER_SEND_PING:
|
||||||
|
playComboTune(); // Ping sent feedback
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BROKER_SHUTDOWN:
|
||||||
|
playShutdownMelody(); // Shutdown feedback
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For other events, check if it's a printable character
|
||||||
|
if (event->kbchar >= 32 && event->kbchar <= 126) {
|
||||||
|
// Typing feedback - very short boop
|
||||||
|
// Removing this for now, too chatty
|
||||||
|
// playChirp();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Allow other handlers to process the event
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t BuzzerFeedbackThread::runOnce()
|
||||||
|
{
|
||||||
|
// This thread is primarily event-driven, but we can use runOnce
|
||||||
|
// for any periodic tasks if needed in the future
|
||||||
|
|
||||||
|
if (needsUpdate) {
|
||||||
|
needsUpdate = false;
|
||||||
|
// Could add any periodic processing here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run every 100ms when active, less frequently when idle
|
||||||
|
return needsUpdate ? 100 : 1000;
|
||||||
|
}
|
||||||
24
src/buzz/BuzzerFeedbackThread.h
Normal file
24
src/buzz/BuzzerFeedbackThread.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Observer.h"
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
#include "input/InputBroker.h"
|
||||||
|
|
||||||
|
class BuzzerFeedbackThread : public concurrency::OSThread
|
||||||
|
{
|
||||||
|
CallbackObserver<BuzzerFeedbackThread, const InputEvent *> inputObserver =
|
||||||
|
CallbackObserver<BuzzerFeedbackThread, const InputEvent *>(this, &BuzzerFeedbackThread::handleInputEvent);
|
||||||
|
|
||||||
|
public:
|
||||||
|
BuzzerFeedbackThread();
|
||||||
|
int handleInputEvent(const InputEvent *event);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t lastEventTime = 0;
|
||||||
|
bool needsUpdate = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern BuzzerFeedbackThread *buzzerFeedbackThread;
|
||||||
@@ -38,6 +38,11 @@ const int DURATION_1_1 = 1000; // 1/1 note
|
|||||||
|
|
||||||
void playTones(const ToneDuration *tone_durations, int size)
|
void playTones(const ToneDuration *tone_durations, int size)
|
||||||
{
|
{
|
||||||
|
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
|
||||||
|
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) {
|
||||||
|
// Buzzer is disabled or not set to system tones
|
||||||
|
return;
|
||||||
|
}
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
if (!config.device.buzzer_gpio)
|
if (!config.device.buzzer_gpio)
|
||||||
config.device.buzzer_gpio = PIN_BUZZER;
|
config.device.buzzer_gpio = PIN_BUZZER;
|
||||||
@@ -54,7 +59,7 @@ void playTones(const ToneDuration *tone_durations, int size)
|
|||||||
|
|
||||||
void playBeep()
|
void playBeep()
|
||||||
{
|
{
|
||||||
ToneDuration melody[] = {{NOTE_B3, DURATION_1_4}};
|
ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}};
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,3 +92,72 @@ void playShutdownMelody()
|
|||||||
ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}};
|
ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}};
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void playChirp()
|
||||||
|
{
|
||||||
|
// A short, friendly "chirp" sound for key presses
|
||||||
|
ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note
|
||||||
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
void playBoop()
|
||||||
|
{
|
||||||
|
// A short, friendly "boop" sound for button presses
|
||||||
|
ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note
|
||||||
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
void playLongPressLeadUp()
|
||||||
|
{
|
||||||
|
// An ascending lead-up sequence for long press - builds anticipation
|
||||||
|
ToneDuration melody[] = {
|
||||||
|
{NOTE_C3, 100}, // Start low
|
||||||
|
{NOTE_E3, 100}, // Step up
|
||||||
|
{NOTE_G3, 100}, // Keep climbing
|
||||||
|
{NOTE_B3, 150} // Peak with longer note for emphasis
|
||||||
|
};
|
||||||
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static state for progressive lead-up notes
|
||||||
|
static int leadUpNoteIndex = 0;
|
||||||
|
static const ToneDuration leadUpNotes[] = {
|
||||||
|
{NOTE_C3, 100}, // Start low
|
||||||
|
{NOTE_E3, 100}, // Step up
|
||||||
|
{NOTE_G3, 100}, // Keep climbing
|
||||||
|
{NOTE_B3, 150} // Peak with longer note for emphasis
|
||||||
|
};
|
||||||
|
static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration);
|
||||||
|
|
||||||
|
bool playNextLeadUpNote()
|
||||||
|
{
|
||||||
|
if (leadUpNoteIndex >= leadUpNotesCount) {
|
||||||
|
return false; // All notes have been played
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use playTones to handle buzzer logic consistently
|
||||||
|
const auto ¬e = leadUpNotes[leadUpNoteIndex];
|
||||||
|
playTones(¬e, 1); // Play single note using existing playTones function
|
||||||
|
|
||||||
|
leadUpNoteIndex++;
|
||||||
|
return true; // Note was played (playTones handles buzzer availability internally)
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetLeadUpSequence()
|
||||||
|
{
|
||||||
|
leadUpNoteIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void playComboTune()
|
||||||
|
{
|
||||||
|
// Quick high-pitched notes with trills
|
||||||
|
ToneDuration melody[] = {
|
||||||
|
{NOTE_G3, 80}, // Quick chirp
|
||||||
|
{NOTE_B3, 60}, // Higher chirp
|
||||||
|
{NOTE_CS4, 80}, // Even higher
|
||||||
|
{NOTE_G3, 60}, // Quick trill down
|
||||||
|
{NOTE_CS4, 60}, // Quick trill up
|
||||||
|
{NOTE_B3, 120} // Ending chirp
|
||||||
|
};
|
||||||
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,3 +6,9 @@ void playStartMelody();
|
|||||||
void playShutdownMelody();
|
void playShutdownMelody();
|
||||||
void playGPSEnableBeep();
|
void playGPSEnableBeep();
|
||||||
void playGPSDisableBeep();
|
void playGPSDisableBeep();
|
||||||
|
void playComboTune();
|
||||||
|
void playBoop();
|
||||||
|
void playChirp();
|
||||||
|
void playLongPressLeadUp();
|
||||||
|
bool playNextLeadUpNote(); // Play the next note in the lead-up sequence
|
||||||
|
void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning
|
||||||
@@ -12,7 +12,6 @@ enum class Cmd {
|
|||||||
STOP_ALERT_FRAME,
|
STOP_ALERT_FRAME,
|
||||||
START_FIRMWARE_UPDATE_SCREEN,
|
START_FIRMWARE_UPDATE_SCREEN,
|
||||||
STOP_BOOT_SCREEN,
|
STOP_BOOT_SCREEN,
|
||||||
PRINT,
|
|
||||||
SHOW_PREV_FRAME,
|
SHOW_PREV_FRAME,
|
||||||
SHOW_NEXT_FRAME
|
SHOW_NEXT_FRAME
|
||||||
};
|
};
|
||||||
@@ -9,17 +9,23 @@ namespace concurrency
|
|||||||
Lock::Lock() : handle(xSemaphoreCreateBinary())
|
Lock::Lock() : handle(xSemaphoreCreateBinary())
|
||||||
{
|
{
|
||||||
assert(handle);
|
assert(handle);
|
||||||
assert(xSemaphoreGive(handle));
|
if (xSemaphoreGive(handle) == false) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Lock::lock()
|
void Lock::lock()
|
||||||
{
|
{
|
||||||
assert(xSemaphoreTake(handle, portMAX_DELAY));
|
if (xSemaphoreTake(handle, portMAX_DELAY) == false) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Lock::unlock()
|
void Lock::unlock()
|
||||||
{
|
{
|
||||||
assert(xSemaphoreGive(handle));
|
if (xSemaphoreGive(handle) == false) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Lock::Lock() {}
|
Lock::Lock() {}
|
||||||
|
|||||||
@@ -81,7 +81,43 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_SG_923
|
// #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_SG_923
|
||||||
|
|
||||||
// Total system gain in dBm to subtract from Tx power to remain within regulatory and Tx PA limits
|
// Total system gain in dBm to subtract from Tx power to remain within regulatory and Tx PA limits
|
||||||
// This value should be set in variant.h and is PA gain + antenna gain (if variant has a non-removable antenna)
|
// The value consists of PA gain + antenna gain (if variant has a non-removable antenna)
|
||||||
|
// TX_GAIN_LORA should be set with definitions below for common modules, or in variant.h.
|
||||||
|
|
||||||
|
// Gain for common modules with transmit PAs
|
||||||
|
#ifdef EBYTE_E22_900M30S
|
||||||
|
// 10dB PA gain and 30dB rated output; based on measurements from
|
||||||
|
// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/E22-900M30S%20power%20output%20testing.txt
|
||||||
|
#define TX_GAIN_LORA 7
|
||||||
|
#define SX126X_MAX_POWER 22
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef EBYTE_E22_900M33S
|
||||||
|
// 25dB PA gain and 33dB rated output; based on TX Power Curve from E22-900M33S_UserManual_EN_v1.0.pdf
|
||||||
|
#define TX_GAIN_LORA 25
|
||||||
|
#define SX126X_MAX_POWER 8
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef NICERF_MINIF27
|
||||||
|
// Note that datasheet power level of 9 corresponds with SX1262 at 22dBm
|
||||||
|
// Maximum output power of 29dBm with VCC_PA = 5V
|
||||||
|
#define TX_GAIN_LORA 7
|
||||||
|
#define SX126X_MAX_POWER 22
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef NICERF_F30_HF
|
||||||
|
// Maximum output power of 29.6dBm with VCC = 5V and SX1262 at 22dBm
|
||||||
|
#define TX_GAIN_LORA 8
|
||||||
|
#define SX126X_MAX_POWER 22
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef NICERF_F30_LF
|
||||||
|
// Maximum output power of 32.0dBm with VCC = 5V and SX1262 at 22dBm
|
||||||
|
#define TX_GAIN_LORA 10
|
||||||
|
#define SX126X_MAX_POWER 22
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Default system gain to 0 if not defined
|
||||||
#ifndef TX_GAIN_LORA
|
#ifndef TX_GAIN_LORA
|
||||||
#define TX_GAIN_LORA 0
|
#define TX_GAIN_LORA 0
|
||||||
#endif
|
#endif
|
||||||
@@ -171,6 +207,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define BMX160_ADDR 0x69
|
#define BMX160_ADDR 0x69
|
||||||
#define ICM20948_ADDR 0x69
|
#define ICM20948_ADDR 0x69
|
||||||
#define ICM20948_ADDR_ALT 0x68
|
#define ICM20948_ADDR_ALT 0x68
|
||||||
|
#define BMM150_ADDR 0x13
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// LED
|
// LED
|
||||||
@@ -193,6 +230,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
#define FT6336U_ADDR 0x48
|
#define FT6336U_ADDR 0x48
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
|
||||||
|
// - the default i2c address for this sensor is 0x20, and users are instructed to
|
||||||
|
// set 0x21 and 0x22 for the second and third sensor if present.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
#define RAK120351_ADDR 0x20
|
||||||
|
#define RAK120352_ADDR 0x21
|
||||||
|
#define RAK120353_ADDR 0x22
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// BIAS-T Generator
|
// BIAS-T Generator
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -302,11 +348,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#error HW_VENDOR must be defined
|
#error HW_VENDOR must be defined
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef TB_DOWN
|
||||||
|
#define TB_DOWN 255
|
||||||
|
#endif
|
||||||
|
#ifndef TB_UP
|
||||||
|
#define TB_UP 255
|
||||||
|
#endif
|
||||||
|
#ifndef TB_LEFT
|
||||||
|
#define TB_LEFT 255
|
||||||
|
#endif
|
||||||
|
#ifndef TB_RIGHT
|
||||||
|
#define TB_RIGHT 255
|
||||||
|
#endif
|
||||||
|
#ifndef TB_PRESS
|
||||||
|
#define TB_PRESS 255
|
||||||
|
#endif
|
||||||
|
|
||||||
// Support multiple RGB LED configuration
|
// Support multiple RGB LED configuration
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE)
|
||||||
#define HAS_RGB_LED
|
#define HAS_RGB_LED
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// default mapping of pins
|
||||||
|
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
||||||
|
#define ALT_BUTTON_PIN PIN_BUTTON2
|
||||||
|
#endif
|
||||||
|
#if defined ALT_BUTTON_PIN
|
||||||
|
|
||||||
|
#ifndef ALT_BUTTON_ACTIVE_LOW
|
||||||
|
#define ALT_BUTTON_ACTIVE_LOW true
|
||||||
|
#endif
|
||||||
|
#ifndef ALT_BUTTON_ACTIVE_PULLUP
|
||||||
|
#define ALT_BUTTON_ACTIVE_PULLUP true
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Global switches to turn off features for a minimized build
|
// Global switches to turn off features for a minimized build
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
|||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P};
|
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150};
|
||||||
return firstOfOrNONE(8, types);
|
return firstOfOrNONE(9, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
||||||
|
|||||||
@@ -70,8 +70,10 @@ class ScanI2C
|
|||||||
DFROBOT_RAIN,
|
DFROBOT_RAIN,
|
||||||
DPS310,
|
DPS310,
|
||||||
LTR390UV,
|
LTR390UV,
|
||||||
|
RAK12035,
|
||||||
TCA8418KB,
|
TCA8418KB,
|
||||||
PCT2075,
|
PCT2075,
|
||||||
|
BMM150,
|
||||||
} DeviceType;
|
} DeviceType;
|
||||||
|
|
||||||
// typedef uint8_t DeviceAddress;
|
// typedef uint8_t DeviceAddress;
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
|
||||||
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c) {
|
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c || registerValue == 0xc8d) {
|
||||||
type = SHT4X;
|
type = SHT4X;
|
||||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||||
@@ -423,9 +423,21 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
logFoundDevice("BMA423", (uint8_t)addr.address);
|
logFoundDevice("BMA423", (uint8_t)addr.address);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case TCA9535_ADDR:
|
||||||
|
case RAK120352_ADDR:
|
||||||
|
case RAK120353_ADDR:
|
||||||
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1);
|
||||||
|
if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20)
|
||||||
|
type = RAK12035;
|
||||||
|
logFoundDevice("RAK12035", (uint8_t)addr.address);
|
||||||
|
} else {
|
||||||
|
type = TCA9535;
|
||||||
|
logFoundDevice("TCA9535", (uint8_t)addr.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(TCA9535_ADDR, TCA9535, "TCA9535", (uint8_t)addr.address);
|
|
||||||
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
|
||||||
@@ -435,6 +447,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (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);
|
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
||||||
|
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
||||||
#ifdef HAS_TPS65233
|
#ifdef HAS_TPS65233
|
||||||
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,10 @@
|
|||||||
#include "detect/ScanI2C.h"
|
#include "detect/ScanI2C.h"
|
||||||
#include "mesh/generated/meshtastic/config.pb.h"
|
#include "mesh/generated/meshtastic/config.pb.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||||
|
|
||||||
#if !HAS_SCREEN
|
#if !HAS_SCREEN
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -14,11 +18,19 @@ namespace graphics
|
|||||||
class Screen
|
class Screen
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum FrameFocus : uint8_t {
|
||||||
|
FOCUS_DEFAULT, // No specific frame
|
||||||
|
FOCUS_PRESERVE, // Return to the previous frame
|
||||||
|
FOCUS_FAULT,
|
||||||
|
FOCUS_TEXTMESSAGE,
|
||||||
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
|
FOCUS_CLOCK,
|
||||||
|
};
|
||||||
|
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
void onPress() {}
|
void onPress() {}
|
||||||
void setup() {}
|
void setup() {}
|
||||||
void setOn(bool) {}
|
void setOn(bool) {}
|
||||||
void print(const char *) {}
|
|
||||||
void doDeepSleep() {}
|
void doDeepSleep() {}
|
||||||
void forceDisplay(bool forceUiUpdate = false) {}
|
void forceDisplay(bool forceUiUpdate = false) {}
|
||||||
void startFirmwareUpdateScreen() {}
|
void startFirmwareUpdateScreen() {}
|
||||||
@@ -27,6 +39,11 @@ class Screen
|
|||||||
void setFunctionSymbol(std::string) {}
|
void setFunctionSymbol(std::string) {}
|
||||||
void removeFunctionSymbol(std::string) {}
|
void removeFunctionSymbol(std::string) {}
|
||||||
void startAlert(const char *) {}
|
void startAlert(const char *) {}
|
||||||
|
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
|
||||||
|
uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
void setFrames(FrameFocus focus) {}
|
||||||
void endAlert() {}
|
void endAlert() {}
|
||||||
};
|
};
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
@@ -62,8 +79,10 @@ class Screen
|
|||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "input/InputBroker.h"
|
#include "input/InputBroker.h"
|
||||||
#include "mesh/MeshModule.h"
|
#include "mesh/MeshModule.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// 0 to 255, though particular variants might define different defaults
|
// 0 to 255, though particular variants might define different defaults
|
||||||
#ifndef BRIGHTNESS_DEFAULT
|
#ifndef BRIGHTNESS_DEFAULT
|
||||||
@@ -90,7 +109,7 @@ class Screen
|
|||||||
|
|
||||||
/// Convert an integer GPS coords to a floating point
|
/// Convert an integer GPS coords to a floating point
|
||||||
#define DegD(i) (i * 1e-7)
|
#define DegD(i) (i * 1e-7)
|
||||||
|
extern bool hasUnreadMessage;
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
/// A basic 2D point class for drawing
|
/// A basic 2D point class for drawing
|
||||||
@@ -176,14 +195,29 @@ class Screen : public concurrency::OSThread
|
|||||||
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
||||||
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
||||||
CallbackObserver<Screen, const InputEvent *>(this, &Screen::handleInputEvent);
|
CallbackObserver<Screen, const InputEvent *>(this, &Screen::handleInputEvent);
|
||||||
CallbackObserver<Screen, const meshtastic_AdminMessage *> adminMessageObserver =
|
CallbackObserver<Screen, AdminModule_ObserverData *> adminMessageObserver =
|
||||||
CallbackObserver<Screen, const meshtastic_AdminMessage *>(this, &Screen::handleAdminMessage);
|
CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
|
size_t frameCount = 0; // Total number of active frames
|
||||||
~Screen();
|
~Screen();
|
||||||
|
|
||||||
|
// Which frame we want to be displayed, after we regen the frameset by calling setFrames
|
||||||
|
enum FrameFocus : uint8_t {
|
||||||
|
FOCUS_DEFAULT, // No specific frame
|
||||||
|
FOCUS_PRESERVE, // Return to the previous frame
|
||||||
|
FOCUS_FAULT,
|
||||||
|
FOCUS_TEXTMESSAGE,
|
||||||
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
|
FOCUS_CLOCK,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Regenerate the normal set of frames, focusing a specific frame if requested
|
||||||
|
// Call when a frame should be added / removed, or custom frames should be cleared
|
||||||
|
void setFrames(FrameFocus focus = FOCUS_DEFAULT);
|
||||||
|
|
||||||
|
std::vector<const uint8_t *> indicatorIcons; // Per-frame custom icon pointers
|
||||||
Screen(const Screen &) = delete;
|
Screen(const Screen &) = delete;
|
||||||
Screen &operator=(const Screen &) = delete;
|
Screen &operator=(const Screen &) = delete;
|
||||||
|
|
||||||
@@ -191,6 +225,12 @@ class Screen : public concurrency::OSThread
|
|||||||
meshtastic_Config_DisplayConfig_OledType model;
|
meshtastic_Config_DisplayConfig_OledType model;
|
||||||
OLEDDISPLAY_GEOMETRY geometry;
|
OLEDDISPLAY_GEOMETRY geometry;
|
||||||
|
|
||||||
|
bool isOverlayBannerShowing();
|
||||||
|
|
||||||
|
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
|
||||||
|
// FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class
|
||||||
|
char ourId[5];
|
||||||
|
|
||||||
/// Initializes the UI, turns on the display, starts showing boot screen.
|
/// Initializes the UI, turns on the display, starts showing boot screen.
|
||||||
//
|
//
|
||||||
// Not thread safe - must be called before any other methods are called.
|
// Not thread safe - must be called before any other methods are called.
|
||||||
@@ -214,21 +254,9 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
void blink();
|
void blink();
|
||||||
|
|
||||||
void drawFrameText(OLEDDisplay *, OLEDDisplayUiState *, int16_t, int16_t, const char *);
|
|
||||||
|
|
||||||
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength);
|
|
||||||
|
|
||||||
// Draw north
|
// Draw north
|
||||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading);
|
|
||||||
|
|
||||||
static uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
|
||||||
|
|
||||||
float estimatedHeading(double lat, double lon);
|
float estimatedHeading(double lat, double lon);
|
||||||
|
|
||||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian);
|
|
||||||
|
|
||||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
|
||||||
|
|
||||||
/// Handle button press, trackball or swipe action)
|
/// Handle button press, trackball or swipe action)
|
||||||
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
|
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
|
||||||
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
|
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
|
||||||
@@ -260,6 +288,11 @@ class Screen : public concurrency::OSThread
|
|||||||
enqueueCmd(cmd);
|
enqueueCmd(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
|
||||||
|
uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0);
|
||||||
|
|
||||||
|
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback);
|
||||||
|
|
||||||
void startFirmwareUpdateScreen()
|
void startFirmwareUpdateScreen()
|
||||||
{
|
{
|
||||||
ScreenCmd cmd;
|
ScreenCmd cmd;
|
||||||
@@ -292,23 +325,6 @@ class Screen : public concurrency::OSThread
|
|||||||
/// Stops showing the boot screen.
|
/// Stops showing the boot screen.
|
||||||
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
||||||
|
|
||||||
/// Writes a string to the screen.
|
|
||||||
void print(const char *text)
|
|
||||||
{
|
|
||||||
ScreenCmd cmd;
|
|
||||||
cmd.cmd = Cmd::PRINT;
|
|
||||||
// TODO(girts): strdup() here is scary, but we can't use std::string as
|
|
||||||
// FreeRTOS queue is just dumbly copying memory contents. It would be
|
|
||||||
// nice if we had a queue that could copy objects by value.
|
|
||||||
cmd.print_text = strdup(text);
|
|
||||||
if (!enqueueCmd(cmd)) {
|
|
||||||
free(cmd.print_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generates a very brief time delta display
|
|
||||||
std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
|
||||||
|
|
||||||
/// Overrides the default utf8 character conversion, to replace empty space with question marks
|
/// Overrides the default utf8 character conversion, to replace empty space with question marks
|
||||||
static char customFontTableLookup(const uint8_t ch)
|
static char customFontTableLookup(const uint8_t ch)
|
||||||
{
|
{
|
||||||
@@ -533,7 +549,7 @@ class Screen : public concurrency::OSThread
|
|||||||
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
||||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||||
int handleInputEvent(const InputEvent *arg);
|
int handleInputEvent(const InputEvent *arg);
|
||||||
int handleAdminMessage(const meshtastic_AdminMessage *arg);
|
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||||
|
|
||||||
/// Used to force (super slow) eink displays to draw critical frames
|
/// Used to force (super slow) eink displays to draw critical frames
|
||||||
void forceDisplay(bool forceUiUpdate = false);
|
void forceDisplay(bool forceUiUpdate = false);
|
||||||
@@ -541,8 +557,6 @@ class Screen : public concurrency::OSThread
|
|||||||
/// Draws our SSL cert screen during boot (called from WebServer)
|
/// Draws our SSL cert screen during boot (called from WebServer)
|
||||||
void setSSLFrames();
|
void setSSLFrames();
|
||||||
|
|
||||||
void setWelcomeFrames();
|
|
||||||
|
|
||||||
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
||||||
void dismissCurrentFrame();
|
void dismissCurrentFrame();
|
||||||
|
|
||||||
@@ -591,7 +605,6 @@ class Screen : public concurrency::OSThread
|
|||||||
void handleOnPress();
|
void handleOnPress();
|
||||||
void handleShowNextFrame();
|
void handleShowNextFrame();
|
||||||
void handleShowPrevFrame();
|
void handleShowPrevFrame();
|
||||||
void handlePrint(const char *text);
|
|
||||||
void handleStartFirmwareUpdateScreen();
|
void handleStartFirmwareUpdateScreen();
|
||||||
|
|
||||||
// Info collected by setFrames method.
|
// Info collected by setFrames method.
|
||||||
@@ -600,30 +613,37 @@ class Screen : public concurrency::OSThread
|
|||||||
// - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo
|
// - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo
|
||||||
struct FramesetInfo {
|
struct FramesetInfo {
|
||||||
struct FramePositions {
|
struct FramePositions {
|
||||||
uint8_t fault = 0;
|
uint8_t fault = 255;
|
||||||
uint8_t textMessage = 0;
|
uint8_t waypoint = 255;
|
||||||
uint8_t waypoint = 0;
|
uint8_t focusedModule = 255;
|
||||||
uint8_t focusedModule = 0;
|
uint8_t log = 255;
|
||||||
uint8_t log = 0;
|
uint8_t settings = 255;
|
||||||
uint8_t settings = 0;
|
uint8_t wifi = 255;
|
||||||
uint8_t wifi = 0;
|
uint8_t deviceFocused = 255;
|
||||||
|
uint8_t memory = 255;
|
||||||
|
uint8_t gps = 255;
|
||||||
|
uint8_t home = 255;
|
||||||
|
uint8_t textMessage = 255;
|
||||||
|
uint8_t nodelist = 255;
|
||||||
|
uint8_t nodelist_lastheard = 255;
|
||||||
|
uint8_t nodelist_hopsignal = 255;
|
||||||
|
uint8_t nodelist_distance = 255;
|
||||||
|
uint8_t nodelist_bearings = 255;
|
||||||
|
uint8_t clock = 255;
|
||||||
|
uint8_t firstFavorite = 255;
|
||||||
|
uint8_t lastFavorite = 255;
|
||||||
|
uint8_t lora = 255;
|
||||||
} positions;
|
} positions;
|
||||||
|
|
||||||
uint8_t frameCount = 0;
|
uint8_t frameCount = 0;
|
||||||
} framesetInfo;
|
} framesetInfo;
|
||||||
|
|
||||||
// Which frame we want to be displayed, after we regen the frameset by calling setFrames
|
struct DismissedFrames {
|
||||||
enum FrameFocus : uint8_t {
|
bool textMessage = false;
|
||||||
FOCUS_DEFAULT, // No specific frame
|
bool waypoint = false;
|
||||||
FOCUS_PRESERVE, // Return to the previous frame
|
bool wifi = false;
|
||||||
FOCUS_FAULT,
|
bool memory = false;
|
||||||
FOCUS_TEXTMESSAGE,
|
} dismissedFrames;
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
|
||||||
};
|
|
||||||
|
|
||||||
// Regenerate the normal set of frames, focusing a specific frame if requested
|
|
||||||
// Call when a frame should be added / removed, or custom frames should be cleared
|
|
||||||
void setFrames(FrameFocus focus = FOCUS_DEFAULT);
|
|
||||||
|
|
||||||
/// Try to start drawing ASAP
|
/// Try to start drawing ASAP
|
||||||
void setFastFramerate();
|
void setFastFramerate();
|
||||||
@@ -631,34 +651,6 @@ class Screen : public concurrency::OSThread
|
|||||||
// Sets frame up for immediate drawing
|
// Sets frame up for immediate drawing
|
||||||
void setFrameImmediateDraw(FrameCallback *drawFrames);
|
void setFrameImmediateDraw(FrameCallback *drawFrames);
|
||||||
|
|
||||||
/// Called when debug screen is to be drawn, calls through to debugInfo.drawFrame.
|
|
||||||
static void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
|
||||||
|
|
||||||
static void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
|
||||||
|
|
||||||
static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
|
||||||
|
|
||||||
#if defined(DISPLAY_CLOCK_FRAME)
|
|
||||||
static void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
|
||||||
|
|
||||||
static void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
|
||||||
|
|
||||||
static void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1);
|
|
||||||
|
|
||||||
static void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
|
||||||
|
|
||||||
static void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
|
||||||
|
|
||||||
static void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1);
|
|
||||||
|
|
||||||
static void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
|
||||||
|
|
||||||
static void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
|
||||||
|
|
||||||
// Whether we are showing the digital watch face or the analog one
|
|
||||||
bool digitalWatchFace = true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// callback for current alert frame
|
/// callback for current alert frame
|
||||||
FrameCallback alertFrame;
|
FrameCallback alertFrame;
|
||||||
|
|
||||||
@@ -691,4 +683,9 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
|
// Extern declarations for function symbols used in UIRenderer
|
||||||
|
extern std::vector<std::string> functionSymbol;
|
||||||
|
extern std::string functionSymbolString;
|
||||||
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
6
src/graphics/ScreenGlobals.cpp
Normal file
6
src/graphics/ScreenGlobals.cpp
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Global variables for screen function overlay
|
||||||
|
std::vector<std::string> functionSymbol;
|
||||||
|
std::string functionSymbolString;
|
||||||
340
src/graphics/SharedUIDisplay.cpp
Normal file
340
src/graphics/SharedUIDisplay.cpp
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "RTC.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "meshtastic/config.pb.h"
|
||||||
|
#include "power.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <graphics/images.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||||
|
{
|
||||||
|
if (screenwidth > 128) {
|
||||||
|
isHighResolution = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for Heltec Wireless Tracker v1.1
|
||||||
|
if (screenwidth == 160 && screenheight == 80) {
|
||||||
|
isHighResolution = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Shared External State ===
|
||||||
|
bool hasUnreadMessage = false;
|
||||||
|
bool isMuted = false;
|
||||||
|
bool isHighResolution = false;
|
||||||
|
|
||||||
|
// === Internal State ===
|
||||||
|
bool isBoltVisibleShared = true;
|
||||||
|
uint32_t lastBlinkShared = 0;
|
||||||
|
bool isMailIconVisible = true;
|
||||||
|
uint32_t lastMailBlink = 0;
|
||||||
|
|
||||||
|
// *********************************
|
||||||
|
// * Rounded Header when inverted *
|
||||||
|
// *********************************
|
||||||
|
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
||||||
|
{
|
||||||
|
// Draw the center and side rectangles
|
||||||
|
display->fillRect(x + r, y, w - 2 * r, h); // center bar
|
||||||
|
display->fillRect(x, y + r, r, h - 2 * r); // left edge
|
||||||
|
display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
||||||
|
|
||||||
|
// Draw the rounded corners using filled circles
|
||||||
|
display->fillCircle(x + r + 1, y + r, r); // top-left
|
||||||
|
display->fillCircle(x + w - r - 1, y + r, r); // top-right
|
||||||
|
display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
||||||
|
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************
|
||||||
|
// * Common Header Drawing *
|
||||||
|
// *************************
|
||||||
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
|
||||||
|
{
|
||||||
|
constexpr int HEADER_OFFSET_Y = 1;
|
||||||
|
y += HEADER_OFFSET_Y;
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const int xOffset = 4;
|
||||||
|
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||||
|
const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||||
|
const bool isBold = config.display.heading_bold;
|
||||||
|
|
||||||
|
const int screenW = display->getWidth();
|
||||||
|
const int screenH = display->getHeight();
|
||||||
|
|
||||||
|
if (!battery_only) {
|
||||||
|
// === Inverted Header Background ===
|
||||||
|
if (isInverted) {
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
if (isHighResolution) {
|
||||||
|
display->drawLine(0, 20, screenW, 20);
|
||||||
|
} else {
|
||||||
|
display->drawLine(0, 14, screenW, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Screen Title ===
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||||||
|
if (config.display.heading_bold) {
|
||||||
|
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
// === Battery State ===
|
||||||
|
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||||
|
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
|
||||||
|
if (chargePercent == 100) {
|
||||||
|
isCharging = false;
|
||||||
|
}
|
||||||
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
#ifndef USE_EINK
|
||||||
|
if (isCharging && now - lastBlinkShared > 500) {
|
||||||
|
isBoltVisibleShared = !isBoltVisibleShared;
|
||||||
|
lastBlinkShared = now;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
|
||||||
|
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||||
|
|
||||||
|
// === Battery Icons ===
|
||||||
|
if (useHorizontalBattery) {
|
||||||
|
int batteryX = 2;
|
||||||
|
int batteryY = HEADER_OFFSET_Y + 3;
|
||||||
|
display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
|
||||||
|
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
|
||||||
|
if (isCharging && isBoltVisibleShared)
|
||||||
|
display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
|
||||||
|
else {
|
||||||
|
display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
|
||||||
|
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||||
|
int fillWidth = 14 * chargePercent / 100;
|
||||||
|
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int batteryX = 1;
|
||||||
|
int batteryY = HEADER_OFFSET_Y + 1;
|
||||||
|
#ifdef USE_EINK
|
||||||
|
batteryY += 2;
|
||||||
|
#endif
|
||||||
|
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||||||
|
if (isCharging && isBoltVisibleShared)
|
||||||
|
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||||||
|
else {
|
||||||
|
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||||
|
int fillHeight = 8 * chargePercent / 100;
|
||||||
|
int fillY = batteryY - fillHeight;
|
||||||
|
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Battery % Display ===
|
||||||
|
char chargeStr[4];
|
||||||
|
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||||
|
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||||
|
const int batteryOffset = useHorizontalBattery ? 19 : 9;
|
||||||
|
const int percentX = x + batteryOffset;
|
||||||
|
display->drawString(percentX, textY, chargeStr);
|
||||||
|
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
|
||||||
|
if (isBold) {
|
||||||
|
display->drawString(percentX + 1, textY, chargeStr);
|
||||||
|
display->drawString(percentX + chargeNumWidth, textY, "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Time and Right-aligned Icons ===
|
||||||
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||||
|
char timeStr[10] = "--:--"; // Fallback display
|
||||||
|
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||||
|
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||||
|
|
||||||
|
if (rtc_sec > 0 && !battery_only) {
|
||||||
|
// === Build Time String ===
|
||||||
|
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
int hour = hms / SEC_PER_HOUR;
|
||||||
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||||
|
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
bool isPM = hour >= 12;
|
||||||
|
hour %= 12;
|
||||||
|
if (hour == 0)
|
||||||
|
hour = 12;
|
||||||
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStrWidth = display->getStringWidth(timeStr);
|
||||||
|
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||||
|
|
||||||
|
// === Show Mail or Mute Icon to the Left of Time ===
|
||||||
|
int iconRightEdge = timeX - 1;
|
||||||
|
|
||||||
|
bool showMail = false;
|
||||||
|
|
||||||
|
#ifndef USE_EINK
|
||||||
|
if (hasUnreadMessage) {
|
||||||
|
if (now - lastMailBlink > 500) {
|
||||||
|
isMailIconVisible = !isMailIconVisible;
|
||||||
|
lastMailBlink = now;
|
||||||
|
}
|
||||||
|
showMail = isMailIconVisible;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (hasUnreadMessage) {
|
||||||
|
showMail = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (showMail) {
|
||||||
|
if (useHorizontalBattery) {
|
||||||
|
int iconW = 16, iconH = 12;
|
||||||
|
int iconX = iconRightEdge - iconW;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||||
|
if (isInverted) {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||||||
|
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||||
|
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||||
|
} else {
|
||||||
|
int iconX = iconRightEdge - (mail_width - 2);
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
|
if (isInverted) {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
|
}
|
||||||
|
} else if (isMuted) {
|
||||||
|
if (isHighResolution) {
|
||||||
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
|
|
||||||
|
if (isInverted) {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||||
|
} else {
|
||||||
|
int iconX = iconRightEdge - mute_symbol_width;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
|
|
||||||
|
if (isInverted) {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Draw Time ===
|
||||||
|
display->drawString(timeX, textY, timeStr);
|
||||||
|
if (isBold)
|
||||||
|
display->drawString(timeX - 1, textY, timeStr);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||||
|
int iconRightEdge = screenW - xOffset;
|
||||||
|
|
||||||
|
bool showMail = false;
|
||||||
|
|
||||||
|
if (hasUnreadMessage) {
|
||||||
|
if (now - lastMailBlink > 500) {
|
||||||
|
isMailIconVisible = !isMailIconVisible;
|
||||||
|
lastMailBlink = now;
|
||||||
|
}
|
||||||
|
showMail = isMailIconVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMail) {
|
||||||
|
if (useHorizontalBattery) {
|
||||||
|
int iconW = 16, iconH = 12;
|
||||||
|
int iconX = iconRightEdge - iconW;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||||
|
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||||||
|
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||||
|
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||||
|
} else {
|
||||||
|
int iconX = iconRightEdge - mail_width;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
|
}
|
||||||
|
} else if (isMuted) {
|
||||||
|
if (isHighResolution) {
|
||||||
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
|
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||||
|
} else {
|
||||||
|
int iconX = iconRightEdge - mute_symbol_width;
|
||||||
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
|
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display->setColor(WHITE); // Reset for other UI
|
||||||
|
}
|
||||||
|
|
||||||
|
const int *getTextPositions(OLEDDisplay *display)
|
||||||
|
{
|
||||||
|
static int textPositions[7]; // Static array that persists beyond function scope
|
||||||
|
|
||||||
|
if (isHighResolution) {
|
||||||
|
textPositions[0] = textZeroLine;
|
||||||
|
textPositions[1] = textFirstLine_medium;
|
||||||
|
textPositions[2] = textSecondLine_medium;
|
||||||
|
textPositions[3] = textThirdLine_medium;
|
||||||
|
textPositions[4] = textFourthLine_medium;
|
||||||
|
textPositions[5] = textFifthLine_medium;
|
||||||
|
textPositions[6] = textSixthLine_medium;
|
||||||
|
} else {
|
||||||
|
textPositions[0] = textZeroLine;
|
||||||
|
textPositions[1] = textFirstLine;
|
||||||
|
textPositions[2] = textSecondLine;
|
||||||
|
textPositions[3] = textThirdLine;
|
||||||
|
textPositions[4] = textFourthLine;
|
||||||
|
textPositions[5] = textFifthLine;
|
||||||
|
textPositions[6] = textSixthLine;
|
||||||
|
}
|
||||||
|
return textPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
55
src/graphics/SharedUIDisplay.h
Normal file
55
src/graphics/SharedUIDisplay.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Shared UI Helpers
|
||||||
|
// =======================
|
||||||
|
|
||||||
|
#define textZeroLine 0
|
||||||
|
// Consistent Line Spacing - this is standard for all display and the fall-back spacing
|
||||||
|
#define textFirstLine (FONT_HEIGHT_SMALL - 1)
|
||||||
|
#define textSecondLine (textFirstLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
|
#define textThirdLine (textSecondLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
|
#define textFourthLine (textThirdLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
|
#define textFifthLine (textFourthLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
|
#define textSixthLine (textFifthLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
|
|
||||||
|
// Consistent Line Spacing for devices like T114 and TEcho/ThinkNode M1 of devices
|
||||||
|
#define textFirstLine_medium (FONT_HEIGHT_SMALL + 1)
|
||||||
|
#define textSecondLine_medium (textFirstLine_medium + FONT_HEIGHT_SMALL)
|
||||||
|
#define textThirdLine_medium (textSecondLine_medium + FONT_HEIGHT_SMALL)
|
||||||
|
#define textFourthLine_medium (textThirdLine_medium + FONT_HEIGHT_SMALL)
|
||||||
|
#define textFifthLine_medium (textFourthLine_medium + FONT_HEIGHT_SMALL)
|
||||||
|
#define textSixthLine_medium (textFifthLine_medium + FONT_HEIGHT_SMALL)
|
||||||
|
|
||||||
|
// Consistent Line Spacing for devices like VisionMaster T190
|
||||||
|
#define textFirstLine_large (FONT_HEIGHT_SMALL + 1)
|
||||||
|
#define textSecondLine_large (textFirstLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||||
|
#define textThirdLine_large (textSecondLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||||
|
#define textFourthLine_large (textThirdLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||||
|
#define textFifthLine_large (textFourthLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||||
|
#define textSixthLine_large (textFifthLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||||
|
|
||||||
|
// Quick screen access
|
||||||
|
#define SCREEN_WIDTH display->getWidth()
|
||||||
|
#define SCREEN_HEIGHT display->getHeight()
|
||||||
|
|
||||||
|
// Shared state (declare inside namespace)
|
||||||
|
extern bool hasUnreadMessage;
|
||||||
|
extern bool isMuted;
|
||||||
|
extern bool isHighResolution;
|
||||||
|
void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||||
|
|
||||||
|
// Rounded highlight (used for inverted headers)
|
||||||
|
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||||
|
|
||||||
|
// Shared battery/time/mail header
|
||||||
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
|
||||||
|
|
||||||
|
const int *getTextPositions(OLEDDisplay *display);
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
@@ -467,7 +467,15 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
|
|
||||||
// The following setting values are general initial values for each panel, so please comment out any
|
// The following setting values are general initial values for each panel, so please comment out any
|
||||||
// unknown items and try them.
|
// unknown items and try them.
|
||||||
|
#if defined(T_WATCH_S3)
|
||||||
|
cfg.panel_width = 240;
|
||||||
|
cfg.panel_height = 240;
|
||||||
|
cfg.memory_width = 240;
|
||||||
|
cfg.memory_height = 320;
|
||||||
|
cfg.offset_x = 0;
|
||||||
|
cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned
|
||||||
|
cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout
|
||||||
|
#else
|
||||||
cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
||||||
cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
|
cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
|
||||||
cfg.panel_width = TFT_WIDTH; // actual displayable width
|
cfg.panel_width = TFT_WIDTH; // actual displayable width
|
||||||
@@ -475,6 +483,7 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction
|
cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction
|
||||||
cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction
|
cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction
|
||||||
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
||||||
|
#endif
|
||||||
#ifdef TFT_DUMMY_READ_PIXELS
|
#ifdef TFT_DUMMY_READ_PIXELS
|
||||||
cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout
|
cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout
|
||||||
#else
|
#else
|
||||||
@@ -653,7 +662,7 @@ static LGFX *tft = nullptr;
|
|||||||
#include <TFT_eSPI.h> // Graphics and font library for ILI9342 driver chip
|
#include <TFT_eSPI.h> // Graphics and font library for ILI9342 driver chip
|
||||||
|
|
||||||
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
|
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
|
||||||
#elif ARCH_PORTDUINO && HAS_SCREEN != 0 && !HAS_TFT
|
#elif ARCH_PORTDUINO
|
||||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||||
|
|
||||||
class LGFX : public lgfx::LGFX_Device
|
class LGFX : public lgfx::LGFX_Device
|
||||||
@@ -697,11 +706,16 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
|
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
|
||||||
|
|
||||||
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
|
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
|
||||||
LOG_DEBUG("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]);
|
LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]);
|
||||||
cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
|
cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
|
||||||
cfg.pin_rst = settingsMap[displayReset];
|
cfg.pin_rst = settingsMap[displayReset];
|
||||||
|
if (settingsMap[displayRotate]) {
|
||||||
|
cfg.panel_width = settingsMap[displayHeight]; // actual displayable width
|
||||||
|
cfg.panel_height = settingsMap[displayWidth]; // actual displayable height
|
||||||
|
} else {
|
||||||
cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
|
cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
|
||||||
cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
|
cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
|
||||||
|
}
|
||||||
cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
|
cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
|
||||||
cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
|
cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
|
||||||
cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
||||||
@@ -978,9 +992,9 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
|
|||||||
|
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
if (settingsMap[displayRotate]) {
|
if (settingsMap[displayRotate]) {
|
||||||
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
|
|
||||||
} else {
|
|
||||||
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
|
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
|
||||||
|
} else {
|
||||||
|
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(SCREEN_ROTATE)
|
#elif defined(SCREEN_ROTATE)
|
||||||
@@ -1169,6 +1183,8 @@ bool TFTDisplay::connect()
|
|||||||
tft->setRotation(1); // T-Deck has the TFT in landscape
|
tft->setRotation(1); // T-Deck has the TFT in landscape
|
||||||
#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
||||||
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
||||||
|
#elif ARCH_PORTDUINO
|
||||||
|
tft->setRotation(0); // use config.yaml to set rotation
|
||||||
#else
|
#else
|
||||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
103
src/graphics/TimeFormatters.cpp
Normal file
103
src/graphics/TimeFormatters.cpp
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#include "TimeFormatters.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
#include "mesh/NodeDB.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo)
|
||||||
|
{
|
||||||
|
// Cache the result - avoid frequent recalculation
|
||||||
|
static uint8_t hoursCached = 0, minutesCached = 0;
|
||||||
|
static uint32_t daysAgoCached = 0;
|
||||||
|
static uint32_t secondsAgoCached = 0;
|
||||||
|
static bool validCached = false;
|
||||||
|
|
||||||
|
// Abort: if timezone not set
|
||||||
|
if (strlen(config.device.tzdef) == 0) {
|
||||||
|
validCached = false;
|
||||||
|
return validCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort: if invalid pointers passed
|
||||||
|
if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) {
|
||||||
|
validCached = false;
|
||||||
|
return validCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set)
|
||||||
|
if (secondsAgo > SEC_PER_DAY * 30UL * 6) {
|
||||||
|
validCached = false;
|
||||||
|
return validCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If repeated request, don't bother recalculating
|
||||||
|
if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) {
|
||||||
|
if (validCached) {
|
||||||
|
*hours = hoursCached;
|
||||||
|
*minutes = minutesCached;
|
||||||
|
*daysAgo = daysAgoCached;
|
||||||
|
}
|
||||||
|
return validCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get local time
|
||||||
|
uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time
|
||||||
|
|
||||||
|
// Abort: if RTC not set
|
||||||
|
if (!secondsRTC) {
|
||||||
|
validCached = false;
|
||||||
|
return validCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get absolute time when last seen
|
||||||
|
uint32_t secondsSeenAt = secondsRTC - secondsAgo;
|
||||||
|
|
||||||
|
// Calculate daysAgo
|
||||||
|
*daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed
|
||||||
|
|
||||||
|
// Get seconds since midnight
|
||||||
|
uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY;
|
||||||
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
|
// Tear apart hms into hours and minutes
|
||||||
|
*hours = hms / SEC_PER_HOUR;
|
||||||
|
*minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
daysAgoCached = *daysAgo;
|
||||||
|
hoursCached = *hours;
|
||||||
|
minutesCached = *minutes;
|
||||||
|
secondsAgoCached = secondsAgo;
|
||||||
|
|
||||||
|
validCached = true;
|
||||||
|
return validCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength)
|
||||||
|
{
|
||||||
|
// Use an absolute timestamp in some cases.
|
||||||
|
// Particularly useful with E-Ink displays. Static UI, fewer refreshes.
|
||||||
|
uint8_t timestampHours, timestampMinutes;
|
||||||
|
int32_t daysAgo;
|
||||||
|
bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo);
|
||||||
|
|
||||||
|
if (agoSecs < 120) // last 2 mins?
|
||||||
|
snprintf(timeStr, maxLength, "%u seconds ago", agoSecs);
|
||||||
|
// -- if suitable for timestamp --
|
||||||
|
else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes
|
||||||
|
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE);
|
||||||
|
else if (useTimestamp && daysAgo == 0) // Today
|
||||||
|
snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes);
|
||||||
|
else if (useTimestamp && daysAgo == 1) // Yesterday
|
||||||
|
snprintf(timeStr, maxLength, "Seen yesterday");
|
||||||
|
else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method)
|
||||||
|
snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo);
|
||||||
|
// -- if using time delta instead --
|
||||||
|
else if (agoSecs < 120 * 60) // last 2 hrs
|
||||||
|
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60);
|
||||||
|
// Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data.
|
||||||
|
else if ((agoSecs / 60 / 60) < (730 * 6))
|
||||||
|
snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60);
|
||||||
|
else
|
||||||
|
snprintf(timeStr, maxLength, "unknown age");
|
||||||
|
}
|
||||||
26
src/graphics/TimeFormatters.h
Normal file
26
src/graphics/TimeFormatters.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
#include <airtime.h>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a delta in seconds ago to timestamp information (hours, minutes, days ago).
|
||||||
|
*
|
||||||
|
* @param secondsAgo Number of seconds ago to convert
|
||||||
|
* @param hours Pointer to store the hours (0-23)
|
||||||
|
* @param minutes Pointer to store the minutes (0-59)
|
||||||
|
* @param daysAgo Pointer to store the number of days ago
|
||||||
|
* @return true if conversion was successful, false if invalid input or time not available
|
||||||
|
*/
|
||||||
|
bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human-readable string representing the time ago in a format like "2 days, 3 hours, 15 minutes".
|
||||||
|
*
|
||||||
|
* @param agoSecs Number of seconds ago to convert
|
||||||
|
* @param timeStr Pointer to store the resulting string
|
||||||
|
* @param maxLength Maximum length of the resulting string buffer
|
||||||
|
*/
|
||||||
|
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength);
|
||||||
514
src/graphics/draw/ClockRenderer.cpp
Normal file
514
src/graphics/draw/ClockRenderer.cpp
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
#include "ClockRenderer.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "UIRenderer.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "gps/GeoCoord.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/emotes.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||||
|
#include "nimble/NimbleBluetooth.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace ClockRenderer
|
||||||
|
{
|
||||||
|
bool digitalWatchFace = true;
|
||||||
|
|
||||||
|
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||||
|
{
|
||||||
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||||
|
|
||||||
|
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||||||
|
|
||||||
|
uint16_t topAndBottomX = x + (4 * scale);
|
||||||
|
|
||||||
|
uint16_t quarterCellHeight = cellHeight / 4;
|
||||||
|
|
||||||
|
uint16_t topY = y + quarterCellHeight;
|
||||||
|
uint16_t bottomY = y + (quarterCellHeight * 3);
|
||||||
|
|
||||||
|
display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight);
|
||||||
|
display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
||||||
|
{
|
||||||
|
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
|
||||||
|
// segment {innerIndex + 1}
|
||||||
|
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
|
||||||
|
uint8_t numbers[10][7] = {
|
||||||
|
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
|
||||||
|
{0, 1, 1, 0, 0, 0, 0}, // 1 1
|
||||||
|
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
|
||||||
|
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
|
||||||
|
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
|
||||||
|
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
|
||||||
|
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
|
||||||
|
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||||||
|
{1, 1, 1, 1, 1, 1, 1}, // 8 4
|
||||||
|
{1, 1, 1, 1, 0, 1, 1}, // 9
|
||||||
|
};
|
||||||
|
|
||||||
|
// the width and height of each segment's central rectangle:
|
||||||
|
// _____________________
|
||||||
|
// ⋰| (only this part, |⋱
|
||||||
|
// ⋰ | not including | ⋱
|
||||||
|
// ⋱ | the triangles | ⋰
|
||||||
|
// ⋱| on the ends) |⋰
|
||||||
|
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||||
|
|
||||||
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||||
|
|
||||||
|
// segment x and y coordinates
|
||||||
|
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||||
|
uint16_t segmentOneY = y;
|
||||||
|
|
||||||
|
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||||
|
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||||
|
|
||||||
|
uint16_t segmentThreeX = segmentTwoX;
|
||||||
|
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2;
|
||||||
|
|
||||||
|
uint16_t segmentFourX = segmentOneX;
|
||||||
|
uint16_t segmentFourY = segmentThreeY + segmentWidth + 2;
|
||||||
|
|
||||||
|
uint16_t segmentFiveX = x;
|
||||||
|
uint16_t segmentFiveY = segmentThreeY;
|
||||||
|
|
||||||
|
uint16_t segmentSixX = x;
|
||||||
|
uint16_t segmentSixY = segmentTwoY;
|
||||||
|
|
||||||
|
uint16_t segmentSevenX = segmentOneX;
|
||||||
|
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||||||
|
|
||||||
|
if (numbers[number][0]) {
|
||||||
|
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][1]) {
|
||||||
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][2]) {
|
||||||
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][3]) {
|
||||||
|
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][4]) {
|
||||||
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][5]) {
|
||||||
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][6]) {
|
||||||
|
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
int halfHeight = height / 2;
|
||||||
|
|
||||||
|
// draw central rectangle
|
||||||
|
display->fillRect(x, y, width, height);
|
||||||
|
|
||||||
|
// draw end triangles
|
||||||
|
display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight);
|
||||||
|
|
||||||
|
display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
int halfHeight = height / 2;
|
||||||
|
|
||||||
|
// draw central rectangle
|
||||||
|
display->fillRect(x, y, height, width);
|
||||||
|
|
||||||
|
// draw end triangles
|
||||||
|
display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y);
|
||||||
|
|
||||||
|
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||||||
|
{
|
||||||
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||||
|
|
||||||
|
if (digitalMode) {
|
||||||
|
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
|
||||||
|
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
|
||||||
|
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
|
||||||
|
|
||||||
|
display->drawCircle(centerX, centerY, radius);
|
||||||
|
display->drawCircle(centerX, centerY, radius + 1);
|
||||||
|
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
|
||||||
|
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
|
||||||
|
} else {
|
||||||
|
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||||
|
uint16_t segmentOneY = y;
|
||||||
|
|
||||||
|
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||||
|
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||||
|
|
||||||
|
uint16_t segmentThreeX = segmentOneX;
|
||||||
|
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
|
||||||
|
|
||||||
|
uint16_t segmentFourX = x;
|
||||||
|
uint16_t segmentFourY = y + segmentHeight + 2;
|
||||||
|
|
||||||
|
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||||
|
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||||
|
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||||
|
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// Draw a digital clock
|
||||||
|
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->clear();
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
int line = 1;
|
||||||
|
// === Set Title, Blank for Clock
|
||||||
|
const char *titleStr = "";
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||||
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
|
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
|
char timeString[16];
|
||||||
|
int hour = 0;
|
||||||
|
int minute = 0;
|
||||||
|
int second = 0;
|
||||||
|
if (rtc_sec > 0) {
|
||||||
|
long hms = rtc_sec % SEC_PER_DAY;
|
||||||
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
|
hour = hms / SEC_PER_HOUR;
|
||||||
|
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPM = hour >= 12;
|
||||||
|
// hour = hour > 12 ? hour - 12 : hour;
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
hour %= 12;
|
||||||
|
if (hour == 0)
|
||||||
|
hour = 12;
|
||||||
|
bool isPM = hour >= 12;
|
||||||
|
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||||||
|
} else {
|
||||||
|
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format seconds string
|
||||||
|
char secondString[8];
|
||||||
|
snprintf(secondString, sizeof(secondString), "%02d", second);
|
||||||
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
float scale = 1.5;
|
||||||
|
#else
|
||||||
|
float scale = 0.75;
|
||||||
|
if (isHighResolution) {
|
||||||
|
scale = 1.5;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||||
|
|
||||||
|
// calculate hours:minutes string width
|
||||||
|
uint16_t timeStringWidth = strlen(timeString) * 5;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < strlen(timeString); i++) {
|
||||||
|
char character = timeString[i];
|
||||||
|
|
||||||
|
if (character == ':') {
|
||||||
|
timeStringWidth += segmentHeight;
|
||||||
|
} else {
|
||||||
|
timeStringWidth += segmentWidth + (segmentHeight * 2) + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2);
|
||||||
|
|
||||||
|
uint16_t startingHourMinuteTextX = hourMinuteTextX;
|
||||||
|
|
||||||
|
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2);
|
||||||
|
|
||||||
|
// iterate over characters in hours:minutes string and draw segmented characters
|
||||||
|
for (uint8_t i = 0; i < strlen(timeString); i++) {
|
||||||
|
char character = timeString[i];
|
||||||
|
|
||||||
|
if (character == ':') {
|
||||||
|
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
|
||||||
|
|
||||||
|
hourMinuteTextX += segmentHeight + 6;
|
||||||
|
} else {
|
||||||
|
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
|
||||||
|
|
||||||
|
hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
hourMinuteTextX += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw seconds string
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int xOffset = (isHighResolution) ? 0 : -1;
|
||||||
|
if (hour >= 10) {
|
||||||
|
xOffset += (isHighResolution) ? 32 : 18;
|
||||||
|
}
|
||||||
|
int yOffset = (isHighResolution) ? 3 : 1;
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
||||||
|
isPM ? "pm" : "am");
|
||||||
|
}
|
||||||
|
#ifndef USE_EINK
|
||||||
|
xOffset = (isHighResolution) ? 18 : 10;
|
||||||
|
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||||
|
secondString);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw an analog clock
|
||||||
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
// === Set Title, Blank for Clock
|
||||||
|
const char *titleStr = "";
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||||
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
|
drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// clock face center coordinates
|
||||||
|
int16_t centerX = display->getWidth() / 2;
|
||||||
|
int16_t centerY = display->getHeight() / 2;
|
||||||
|
|
||||||
|
// clock face radius
|
||||||
|
int16_t radius = 0;
|
||||||
|
if (display->getHeight() < display->getWidth()) {
|
||||||
|
radius = (display->getHeight() / 2) * 0.9;
|
||||||
|
} else {
|
||||||
|
radius = (display->getWidth() / 2) * 0.9;
|
||||||
|
}
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
radius = (display->getWidth() / 2) * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// noon (0 deg) coordinates (outermost circle)
|
||||||
|
int16_t noonX = centerX;
|
||||||
|
int16_t noonY = centerY - radius;
|
||||||
|
|
||||||
|
// second hand radius and y coordinate (outermost circle)
|
||||||
|
int16_t secondHandNoonY = noonY + 1;
|
||||||
|
|
||||||
|
// tick mark outer y coordinate; (first nested circle)
|
||||||
|
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||||
|
|
||||||
|
// seconds tick mark inner y coordinate; (second nested circle)
|
||||||
|
double secondsTickMarkInnerNoonY = (double)noonY + 4;
|
||||||
|
if (isHighResolution) {
|
||||||
|
secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hours tick mark inner y coordinate; (third nested circle)
|
||||||
|
double hoursTickMarkInnerNoonY = (double)noonY + 6;
|
||||||
|
if (isHighResolution) {
|
||||||
|
hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// minute hand y coordinate
|
||||||
|
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||||
|
|
||||||
|
// hour string y coordinate
|
||||||
|
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
||||||
|
|
||||||
|
// hour hand radius and y coordinate
|
||||||
|
int16_t hourHandRadius = radius * 0.35;
|
||||||
|
if (isHighResolution) {
|
||||||
|
int16_t hourHandRadius = radius * 0.55;
|
||||||
|
}
|
||||||
|
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||||||
|
|
||||||
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||||||
|
display->drawCircle(centerX, centerY, radius);
|
||||||
|
|
||||||
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
|
if (rtc_sec > 0) {
|
||||||
|
long hms = rtc_sec % SEC_PER_DAY;
|
||||||
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
|
// Tear apart hms into h:m:s
|
||||||
|
int hour = hms / SEC_PER_HOUR;
|
||||||
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
|
bool isPM = hour >= 12;
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
bool isPM = hour >= 12;
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int yOffset = isHighResolution ? 1 : 0;
|
||||||
|
#ifdef USE_EINK
|
||||||
|
yOffset += 3;
|
||||||
|
#endif
|
||||||
|
display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset,
|
||||||
|
isPM ? "pm" : "am");
|
||||||
|
}
|
||||||
|
hour %= 12;
|
||||||
|
if (hour == 0)
|
||||||
|
hour = 12;
|
||||||
|
|
||||||
|
int16_t degreesPerHour = 30;
|
||||||
|
int16_t degreesPerMinuteOrSecond = 6;
|
||||||
|
|
||||||
|
double hourBaseAngle = hour * degreesPerHour;
|
||||||
|
double hourAngleOffset = ((double)minute / 60) * degreesPerHour;
|
||||||
|
double hourAngle = radians(hourBaseAngle + hourAngleOffset);
|
||||||
|
|
||||||
|
double minuteBaseAngle = minute * degreesPerMinuteOrSecond;
|
||||||
|
double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond;
|
||||||
|
double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset);
|
||||||
|
|
||||||
|
double secondAngle = radians(second * degreesPerMinuteOrSecond);
|
||||||
|
|
||||||
|
double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX;
|
||||||
|
double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY;
|
||||||
|
|
||||||
|
double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX;
|
||||||
|
double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY;
|
||||||
|
|
||||||
|
double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX;
|
||||||
|
double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY;
|
||||||
|
|
||||||
|
display->setFont(FONT_MEDIUM);
|
||||||
|
|
||||||
|
// draw minute and hour tick marks and hour numbers
|
||||||
|
for (uint16_t angle = 0; angle < 360; angle += 6) {
|
||||||
|
double angleInRadians = radians(angle);
|
||||||
|
|
||||||
|
double sineAngleInRadians = sin(-angleInRadians);
|
||||||
|
double cosineAngleInRadians = cos(-angleInRadians);
|
||||||
|
|
||||||
|
double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX;
|
||||||
|
double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY;
|
||||||
|
|
||||||
|
if (angle % degreesPerHour == 0) {
|
||||||
|
double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX;
|
||||||
|
double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY;
|
||||||
|
|
||||||
|
// draw hour tick mark
|
||||||
|
display->drawLine(startX, startY, endX, endY);
|
||||||
|
|
||||||
|
static char buffer[2];
|
||||||
|
|
||||||
|
uint8_t hourInt = (angle / 30);
|
||||||
|
|
||||||
|
if (hourInt == 0) {
|
||||||
|
hourInt = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hour number x offset needs to be adjusted for some cases
|
||||||
|
int8_t hourStringXOffset;
|
||||||
|
int8_t hourStringYOffset = 13;
|
||||||
|
|
||||||
|
switch (hourInt) {
|
||||||
|
case 3:
|
||||||
|
hourStringXOffset = 5;
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
hourStringXOffset = 7;
|
||||||
|
break;
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
hourStringXOffset = 8;
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
hourStringXOffset = 13;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hourStringXOffset = 6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
||||||
|
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
||||||
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
#else
|
||||||
|
#ifdef USE_EINK
|
||||||
|
if (isHighResolution) {
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angle % degreesPerMinuteOrSecond == 0) {
|
||||||
|
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||||||
|
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||||||
|
|
||||||
|
if (isHighResolution) {
|
||||||
|
// draw minute tick mark
|
||||||
|
display->drawLine(startX, startY, endX, endY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw hour hand
|
||||||
|
display->drawLine(centerX, centerY, hourX, hourY);
|
||||||
|
|
||||||
|
// draw minute hand
|
||||||
|
display->drawLine(centerX, centerY, minuteX, minuteY);
|
||||||
|
#ifndef USE_EINK
|
||||||
|
// draw second hand
|
||||||
|
display->drawLine(centerX, centerY, secondX, secondY);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ClockRenderer
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
|
#endif
|
||||||
33
src/graphics/draw/ClockRenderer.h
Normal file
33
src/graphics/draw/ClockRenderer.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Forward declarations
|
||||||
|
class Screen;
|
||||||
|
|
||||||
|
namespace ClockRenderer
|
||||||
|
{
|
||||||
|
// Whether we are showing the digital watch face or the analog one
|
||||||
|
extern bool digitalWatchFace;
|
||||||
|
|
||||||
|
// Clock frame functions
|
||||||
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Segmented display functions
|
||||||
|
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1);
|
||||||
|
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1);
|
||||||
|
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
||||||
|
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
||||||
|
|
||||||
|
// UI elements for clock displays
|
||||||
|
// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
||||||
|
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
} // namespace ClockRenderer
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
141
src/graphics/draw/CompassRenderer.cpp
Normal file
141
src/graphics/draw/CompassRenderer.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "CompassRenderer.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "UIRenderer.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "gps/GeoCoord.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace CompassRenderer
|
||||||
|
{
|
||||||
|
|
||||||
|
// Point helper class for compass calculations
|
||||||
|
struct Point {
|
||||||
|
float x, y;
|
||||||
|
Point(float x, float y) : x(x), y(y) {}
|
||||||
|
|
||||||
|
void rotate(float angle)
|
||||||
|
{
|
||||||
|
float cos_a = cos(angle);
|
||||||
|
float sin_a = sin(angle);
|
||||||
|
float new_x = x * cos_a - y * sin_a;
|
||||||
|
float new_y = x * sin_a + y * cos_a;
|
||||||
|
x = new_x;
|
||||||
|
y = new_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scale(float factor)
|
||||||
|
{
|
||||||
|
x *= factor;
|
||||||
|
y *= factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void translate(float dx, float dy)
|
||||||
|
{
|
||||||
|
x += dx;
|
||||||
|
y += dy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
||||||
|
{
|
||||||
|
// Show the compass heading (not implemented in original)
|
||||||
|
// This could draw a "N" indicator or north arrow
|
||||||
|
// For now, we'll draw a simple north indicator
|
||||||
|
// const float radius = 17.0f;
|
||||||
|
if (isHighResolution) {
|
||||||
|
radius += 4;
|
||||||
|
}
|
||||||
|
Point north(0, -radius);
|
||||||
|
north.rotate(-myHeading);
|
||||||
|
north.translate(compassX, compassY);
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
if (isHighResolution) {
|
||||||
|
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
|
} else {
|
||||||
|
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
|
}
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->drawString(north.x, north.y - 3, "N");
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||||
|
{
|
||||||
|
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
||||||
|
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
||||||
|
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
||||||
|
|
||||||
|
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
arrowPoints[i]->rotate(headingRadian);
|
||||||
|
arrowPoints[i]->scale(compassDiam * 0.6);
|
||||||
|
arrowPoints[i]->translate(compassX, compassY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_EINK
|
||||||
|
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||||
|
#else
|
||||||
|
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||||
|
#endif
|
||||||
|
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
||||||
|
{
|
||||||
|
float radians = bearing * DEG_TO_RAD;
|
||||||
|
|
||||||
|
Point tip(0, -size / 2);
|
||||||
|
Point left(-size / 4, size / 4);
|
||||||
|
Point right(size / 4, size / 4);
|
||||||
|
|
||||||
|
tip.rotate(radians);
|
||||||
|
left.rotate(radians);
|
||||||
|
right.rotate(radians);
|
||||||
|
|
||||||
|
tip.translate(x, y);
|
||||||
|
left.translate(x, y);
|
||||||
|
right.translate(x, y);
|
||||||
|
|
||||||
|
display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float estimatedHeading(double lat, double lon)
|
||||||
|
{
|
||||||
|
// Simple magnetic declination estimation
|
||||||
|
// This is a very basic implementation - the original might be more sophisticated
|
||||||
|
return 0.0f; // Return 0 for now, indicating no heading available
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||||
|
{
|
||||||
|
// Calculate appropriate compass diameter based on display size
|
||||||
|
uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight;
|
||||||
|
uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension
|
||||||
|
|
||||||
|
// Ensure minimum and maximum bounds
|
||||||
|
if (maxDiam < 16)
|
||||||
|
maxDiam = 16;
|
||||||
|
if (maxDiam > 64)
|
||||||
|
maxDiam = 64;
|
||||||
|
|
||||||
|
return maxDiam;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calculateBearing(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||||
|
double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
|
||||||
|
double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
|
||||||
|
double bearing = atan2(y, x) * RAD_TO_DEG;
|
||||||
|
return fmod(bearing + 360.0, 360.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace CompassRenderer
|
||||||
|
} // namespace graphics
|
||||||
36
src/graphics/draw/CompassRenderer.h
Normal file
36
src/graphics/draw/CompassRenderer.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "graphics/Screen.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Forward declarations
|
||||||
|
class Screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compass and navigation drawing functions
|
||||||
|
*
|
||||||
|
* Contains all functions related to drawing compass elements, headings,
|
||||||
|
* navigation arrows, and location-based UI components.
|
||||||
|
*/
|
||||||
|
namespace CompassRenderer
|
||||||
|
{
|
||||||
|
// Compass drawing functions
|
||||||
|
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius);
|
||||||
|
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian);
|
||||||
|
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
||||||
|
|
||||||
|
// Navigation and location functions
|
||||||
|
float estimatedHeading(double lat, double lon);
|
||||||
|
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||||
|
|
||||||
|
// Utility functions for bearing calculations
|
||||||
|
float calculateBearing(double lat1, double lon1, double lat2, double lon2);
|
||||||
|
|
||||||
|
} // namespace CompassRenderer
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
619
src/graphics/draw/DebugRenderer.cpp
Normal file
619
src/graphics/draw/DebugRenderer.cpp
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
#include "../Screen.h"
|
||||||
|
#include "DebugRenderer.h"
|
||||||
|
#include "FSCommon.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "Throttle.h"
|
||||||
|
#include "UIRenderer.h"
|
||||||
|
#include "airtime.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "mesh/Channels.h"
|
||||||
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||||
|
#include "sleep.h"
|
||||||
|
|
||||||
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
|
#include "mesh/wifi/WiFiAPClient.h"
|
||||||
|
#include <WiFi.h>
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
#include "mesh/wifi/WiFiAPClient.h"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
#include "modules/StoreForwardModule.h"
|
||||||
|
#endif
|
||||||
|
#include <DisplayFormatters.h>
|
||||||
|
#include <RadioLibInterface.h>
|
||||||
|
#include <target_specific.h>
|
||||||
|
|
||||||
|
using namespace meshtastic;
|
||||||
|
|
||||||
|
// External variables
|
||||||
|
extern graphics::Screen *screen;
|
||||||
|
extern PowerStatus *powerStatus;
|
||||||
|
extern NodeStatus *nodeStatus;
|
||||||
|
extern GPSStatus *gpsStatus;
|
||||||
|
extern Channels channels;
|
||||||
|
extern AirTime *airTime;
|
||||||
|
|
||||||
|
// External functions from Screen.cpp
|
||||||
|
extern bool heartbeat;
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
extern StoreForwardModule *storeForwardModule;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace DebugRenderer
|
||||||
|
{
|
||||||
|
|
||||||
|
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
|
// The coordinates define the left starting point of the text
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||||
|
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
char channelStr[20];
|
||||||
|
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
|
||||||
|
// Display nodes status
|
||||||
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||||
|
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
|
||||||
|
} else {
|
||||||
|
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus);
|
||||||
|
}
|
||||||
|
#if HAS_GPS
|
||||||
|
// Display GPS status
|
||||||
|
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
|
UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus);
|
||||||
|
} else {
|
||||||
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||||
|
UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus);
|
||||||
|
} else {
|
||||||
|
UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
display->setColor(WHITE);
|
||||||
|
// Draw the channel name
|
||||||
|
display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr);
|
||||||
|
// Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo
|
||||||
|
if (moduleConfig.store_forward.enabled) {
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat,
|
||||||
|
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
|
||||||
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
|
||||||
|
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
|
||||||
|
8, imgQuestionL1);
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12,
|
||||||
|
8, imgQuestionL2);
|
||||||
|
#else
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8,
|
||||||
|
8, imgQuestion);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
|
||||||
|
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
|
||||||
|
8, imgSFL1);
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16,
|
||||||
|
8, imgSFL2);
|
||||||
|
#else
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11,
|
||||||
|
8, imgSF);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
// TODO: Raspberry Pi supports more than just the one screen size
|
||||||
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
|
||||||
|
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
|
||||||
|
imgInfoL1);
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8,
|
||||||
|
imgInfoL2);
|
||||||
|
#else
|
||||||
|
display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8,
|
||||||
|
imgInfo);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId);
|
||||||
|
|
||||||
|
// Draw any log messages
|
||||||
|
display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2));
|
||||||
|
|
||||||
|
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
|
||||||
|
#ifdef SHOW_REDRAWS
|
||||||
|
if (heartbeat)
|
||||||
|
display->setPixel(0, 0);
|
||||||
|
heartbeat = !heartbeat;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****************************
|
||||||
|
// * WiFi Screen *
|
||||||
|
// ****************************
|
||||||
|
void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
|
display->clear();
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int line = 1;
|
||||||
|
|
||||||
|
// === Set Title
|
||||||
|
const char *titleStr = "WiFi";
|
||||||
|
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
|
||||||
|
const char *wifiName = config.network.wifi_ssid;
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected");
|
||||||
|
} else {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected");
|
||||||
|
|
||||||
|
char rssiStr[32];
|
||||||
|
snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI());
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], rssiStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
- WL_CONNECTED: assigned when connected to a WiFi network;
|
||||||
|
- WL_NO_SSID_AVAIL: assigned when no SSID are available;
|
||||||
|
- WL_CONNECT_FAILED: assigned when the connection fails for all the attempts;
|
||||||
|
- WL_CONNECTION_LOST: assigned when the connection is lost;
|
||||||
|
- WL_DISCONNECTED: assigned when disconnected from a network;
|
||||||
|
- WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of
|
||||||
|
attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED);
|
||||||
|
- WL_SCAN_COMPLETED: assigned when the scan networks is completed;
|
||||||
|
- WL_NO_SHIELD: assigned when no WiFi shield is present;
|
||||||
|
|
||||||
|
*/
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
char ipStr[64];
|
||||||
|
snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str());
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], ipStr);
|
||||||
|
} else if (WiFi.status() == WL_NO_SSID_AVAIL) {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "SSID Not Found");
|
||||||
|
} else if (WiFi.status() == WL_CONNECTION_LOST) {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "Connection Lost");
|
||||||
|
} else if (WiFi.status() == WL_IDLE_STATUS) {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting");
|
||||||
|
} else if (WiFi.status() == WL_CONNECT_FAILED) {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "Connection Failed");
|
||||||
|
}
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
else {
|
||||||
|
// Codes:
|
||||||
|
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code
|
||||||
|
display->drawString(x, getTextPositions(display)[line++],
|
||||||
|
WiFi.disconnectReasonName(static_cast<wifi_err_reason_t>(getWifiDisconnectReason())));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
else {
|
||||||
|
char statusStr[32];
|
||||||
|
snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status());
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], statusStr);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
char ssidStr[64];
|
||||||
|
snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName);
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], ssidStr);
|
||||||
|
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local");
|
||||||
|
|
||||||
|
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
|
||||||
|
#ifdef SHOW_REDRAWS
|
||||||
|
if (heartbeat)
|
||||||
|
display->setPixel(0, 0);
|
||||||
|
heartbeat = !heartbeat;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
|
// The coordinates define the left starting point of the text
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||||
|
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
char batStr[20];
|
||||||
|
if (powerStatus->getHasBattery()) {
|
||||||
|
int batV = powerStatus->getBatteryVoltageMv() / 1000;
|
||||||
|
int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10;
|
||||||
|
|
||||||
|
snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(),
|
||||||
|
powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' ');
|
||||||
|
|
||||||
|
// Line 1
|
||||||
|
display->drawString(x, y, batStr);
|
||||||
|
if (config.display.heading_bold)
|
||||||
|
display->drawString(x + 1, y, batStr);
|
||||||
|
} else {
|
||||||
|
// Line 1
|
||||||
|
display->drawString(x, y, "USB");
|
||||||
|
if (config.display.heading_bold)
|
||||||
|
display->drawString(x + 1, y, "USB");
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true);
|
||||||
|
|
||||||
|
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode);
|
||||||
|
// if (config.display.heading_bold)
|
||||||
|
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode);
|
||||||
|
|
||||||
|
uint32_t currentMillis = millis();
|
||||||
|
uint32_t seconds = currentMillis / 1000;
|
||||||
|
uint32_t minutes = seconds / 60;
|
||||||
|
uint32_t hours = minutes / 60;
|
||||||
|
uint32_t days = hours / 24;
|
||||||
|
// currentMillis %= 1000;
|
||||||
|
// seconds %= 60;
|
||||||
|
// minutes %= 60;
|
||||||
|
// hours %= 24;
|
||||||
|
|
||||||
|
// Show uptime as days, hours, minutes OR seconds
|
||||||
|
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
|
||||||
|
|
||||||
|
// Line 1 (Still)
|
||||||
|
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||||
|
if (config.display.heading_bold)
|
||||||
|
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||||
|
|
||||||
|
display->setColor(WHITE);
|
||||||
|
|
||||||
|
// Setup string to assemble analogClock string
|
||||||
|
std::string analogClock = "";
|
||||||
|
|
||||||
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
|
if (rtc_sec > 0) {
|
||||||
|
long hms = rtc_sec % SEC_PER_DAY;
|
||||||
|
// hms += tz.tz_dsttime * SEC_PER_HOUR;
|
||||||
|
// hms -= tz.tz_minuteswest * SEC_PER_MIN;
|
||||||
|
// mod `hms` to ensure in positive range of [0...SEC_PER_DAY)
|
||||||
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
|
// Tear apart hms into h:m:s
|
||||||
|
int hour = hms / SEC_PER_HOUR;
|
||||||
|
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
|
char timebuf[12];
|
||||||
|
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
std::string meridiem = "am";
|
||||||
|
if (hour >= 12) {
|
||||||
|
if (hour > 12)
|
||||||
|
hour -= 12;
|
||||||
|
meridiem = "pm";
|
||||||
|
}
|
||||||
|
if (hour == 00) {
|
||||||
|
hour = 12;
|
||||||
|
}
|
||||||
|
snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str());
|
||||||
|
} else {
|
||||||
|
snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec);
|
||||||
|
}
|
||||||
|
analogClock += timebuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line 2
|
||||||
|
display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str());
|
||||||
|
|
||||||
|
// Display Channel Utilization
|
||||||
|
char chUtil[13];
|
||||||
|
snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent());
|
||||||
|
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil);
|
||||||
|
|
||||||
|
#if HAS_GPS
|
||||||
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
|
// Line 3
|
||||||
|
if (config.display.gps_format !=
|
||||||
|
meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
|
||||||
|
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
|
||||||
|
|
||||||
|
// Line 4
|
||||||
|
UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus);
|
||||||
|
} else {
|
||||||
|
UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
|
||||||
|
#ifdef SHOW_REDRAWS
|
||||||
|
if (heartbeat)
|
||||||
|
display->setPixel(0, 0);
|
||||||
|
heartbeat = !heartbeat;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trampoline functions for DebugInfo class access
|
||||||
|
void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
drawFrame(display, state, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
drawFrameSettings(display, state, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
drawFrameWiFi(display, state, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****************************
|
||||||
|
// * LoRa Focused Screen *
|
||||||
|
// ****************************
|
||||||
|
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->clear();
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int line = 1;
|
||||||
|
|
||||||
|
// === Set Title
|
||||||
|
const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
|
||||||
|
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
|
||||||
|
// === First Row: Region / BLE Name ===
|
||||||
|
graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, "");
|
||||||
|
|
||||||
|
uint8_t dmac[6];
|
||||||
|
char shortnameble[35];
|
||||||
|
getMacAddr(dmac);
|
||||||
|
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||||
|
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
|
||||||
|
int textWidth = display->getStringWidth(shortnameble);
|
||||||
|
int nameX = (SCREEN_WIDTH - textWidth);
|
||||||
|
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||||
|
|
||||||
|
// === Second Row: Radio Preset ===
|
||||||
|
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
|
||||||
|
char regionradiopreset[25];
|
||||||
|
const char *region = myRegion ? myRegion->name : NULL;
|
||||||
|
if (region != nullptr) {
|
||||||
|
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||||
|
}
|
||||||
|
textWidth = display->getStringWidth(regionradiopreset);
|
||||||
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
|
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
|
||||||
|
|
||||||
|
// === Third Row: Frequency / ChanNum ===
|
||||||
|
char frequencyslot[35];
|
||||||
|
char freqStr[16];
|
||||||
|
float freq = RadioLibInterface::instance->getFreq();
|
||||||
|
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||||
|
if (config.lora.channel_num == 0) {
|
||||||
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr);
|
||||||
|
} else {
|
||||||
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num);
|
||||||
|
}
|
||||||
|
size_t len = strlen(frequencyslot);
|
||||||
|
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||||
|
frequencyslot[len - 4] = '\0'; // Remove the last three characters
|
||||||
|
}
|
||||||
|
textWidth = display->getStringWidth(frequencyslot);
|
||||||
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
|
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
|
||||||
|
|
||||||
|
// === Fourth Row: Channel Utilization ===
|
||||||
|
const char *chUtil = "ChUtil:";
|
||||||
|
char chUtilPercentage[10];
|
||||||
|
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||||
|
|
||||||
|
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
||||||
|
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||||
|
|
||||||
|
int chutil_bar_width = (isHighResolution) ? 100 : 50;
|
||||||
|
int chutil_bar_height = (isHighResolution) ? 12 : 7;
|
||||||
|
int extraoffset = (isHighResolution) ? 6 : 3;
|
||||||
|
int chutil_percent = airTime->channelUtilizationPercent();
|
||||||
|
|
||||||
|
int centerofscreen = SCREEN_WIDTH / 2;
|
||||||
|
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||||
|
int starting_position = centerofscreen - total_line_content_width;
|
||||||
|
|
||||||
|
display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
|
||||||
|
|
||||||
|
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
||||||
|
if (chutil_percent >= 61) {
|
||||||
|
chutil_percent = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weighting for nonlinear segments
|
||||||
|
float milestone1 = 25;
|
||||||
|
float milestone2 = 40;
|
||||||
|
float weight1 = 0.45; // Weight for 0–25%
|
||||||
|
float weight2 = 0.35; // Weight for 25–40%
|
||||||
|
float weight3 = 0.20; // Weight for 40–100%
|
||||||
|
float totalWeight = weight1 + weight2 + weight3;
|
||||||
|
|
||||||
|
int seg1 = chutil_bar_width * (weight1 / totalWeight);
|
||||||
|
int seg2 = chutil_bar_width * (weight2 / totalWeight);
|
||||||
|
int seg3 = chutil_bar_width * (weight3 / totalWeight);
|
||||||
|
|
||||||
|
int fillRight = 0;
|
||||||
|
|
||||||
|
if (chutil_percent <= milestone1) {
|
||||||
|
fillRight = (seg1 * (chutil_percent / milestone1));
|
||||||
|
} else if (chutil_percent <= milestone2) {
|
||||||
|
fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1)));
|
||||||
|
} else {
|
||||||
|
fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw outline
|
||||||
|
display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height);
|
||||||
|
|
||||||
|
// Fill progress
|
||||||
|
if (fillRight > 0) {
|
||||||
|
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
|
||||||
|
chUtilPercentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****************************
|
||||||
|
// * Memory Screen *
|
||||||
|
// ****************************
|
||||||
|
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->clear();
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
// === Set Title
|
||||||
|
const char *titleStr = "System";
|
||||||
|
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
|
||||||
|
// === Layout ===
|
||||||
|
int line = 1;
|
||||||
|
const int barHeight = 6;
|
||||||
|
const int labelX = x;
|
||||||
|
const int barsOffset = (isHighResolution) ? 24 : 0;
|
||||||
|
const int barX = x + 40 + barsOffset;
|
||||||
|
|
||||||
|
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
||||||
|
if (total == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int percent = (used * 100) / total;
|
||||||
|
|
||||||
|
char combinedStr[24];
|
||||||
|
if (isHighResolution) {
|
||||||
|
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
||||||
|
total / 1024);
|
||||||
|
} else {
|
||||||
|
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
int textWidth = display->getStringWidth(combinedStr);
|
||||||
|
int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6;
|
||||||
|
if (adjustedBarWidth < 10)
|
||||||
|
adjustedBarWidth = 10;
|
||||||
|
|
||||||
|
int fillWidth = (used * adjustedBarWidth) / total;
|
||||||
|
|
||||||
|
// Label
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->drawString(labelX, getTextPositions(display)[line], label);
|
||||||
|
|
||||||
|
// Bar
|
||||||
|
int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
|
||||||
|
|
||||||
|
display->fillRect(barX, barY, fillWidth, barHeight);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
|
||||||
|
// Value string
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Memory values ===
|
||||||
|
uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap();
|
||||||
|
uint32_t heapTotal = memGet.getHeapSize();
|
||||||
|
|
||||||
|
uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram();
|
||||||
|
uint32_t psramTotal = memGet.getPsramSize();
|
||||||
|
|
||||||
|
uint32_t flashUsed = 0, flashTotal = 0;
|
||||||
|
#ifdef ESP32
|
||||||
|
flashUsed = FSCom.usedBytes();
|
||||||
|
flashTotal = FSCom.totalBytes();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t sdUsed = 0, sdTotal = 0;
|
||||||
|
bool hasSD = false;
|
||||||
|
/*
|
||||||
|
#ifdef HAS_SDCARD
|
||||||
|
hasSD = SD.cardType() != CARD_NONE;
|
||||||
|
if (hasSD) {
|
||||||
|
sdUsed = SD.usedBytes();
|
||||||
|
sdTotal = SD.totalBytes();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
*/
|
||||||
|
// === Draw memory rows
|
||||||
|
drawUsageRow("Heap:", heapUsed, heapTotal, true);
|
||||||
|
#ifdef ESP32
|
||||||
|
if (psramUsed > 0) {
|
||||||
|
line += 1;
|
||||||
|
drawUsageRow("PSRAM:", psramUsed, psramTotal);
|
||||||
|
}
|
||||||
|
if (flashTotal > 0) {
|
||||||
|
line += 1;
|
||||||
|
drawUsageRow("Flash:", flashUsed, flashTotal);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (hasSD && sdTotal > 0) {
|
||||||
|
line += 1;
|
||||||
|
drawUsageRow("SD:", sdUsed, sdTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
// System Uptime
|
||||||
|
if (line < 2) {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
line += 1;
|
||||||
|
char appversionstr[35];
|
||||||
|
snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION));
|
||||||
|
int textWidth = display->getStringWidth(appversionstr);
|
||||||
|
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
|
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
|
||||||
|
|
||||||
|
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it
|
||||||
|
line += 1;
|
||||||
|
char uptimeStr[32] = "";
|
||||||
|
uint32_t uptime = millis() / 1000;
|
||||||
|
uint32_t days = uptime / 86400;
|
||||||
|
uint32_t hours = (uptime % 86400) / 3600;
|
||||||
|
uint32_t mins = (uptime % 3600) / 60;
|
||||||
|
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
|
||||||
|
if (days)
|
||||||
|
snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours);
|
||||||
|
else if (hours)
|
||||||
|
snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins);
|
||||||
|
else
|
||||||
|
snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins);
|
||||||
|
textWidth = display->getStringWidth(uptimeStr);
|
||||||
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
|
display->drawString(nameX, getTextPositions(display)[line], uptimeStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace DebugRenderer
|
||||||
|
} // namespace graphics
|
||||||
|
#endif
|
||||||
38
src/graphics/draw/DebugRenderer.h
Normal file
38
src/graphics/draw/DebugRenderer.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Forward declarations
|
||||||
|
class Screen;
|
||||||
|
class DebugInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Debug and diagnostic drawing functions
|
||||||
|
*
|
||||||
|
* Contains all functions related to drawing debug information,
|
||||||
|
* WiFi status, settings screens, and diagnostic data.
|
||||||
|
*/
|
||||||
|
namespace DebugRenderer
|
||||||
|
{
|
||||||
|
// Debug frame functions
|
||||||
|
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Trampoline functions for framework callback compatibility
|
||||||
|
void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// LoRa information display
|
||||||
|
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Memory screen display
|
||||||
|
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
} // namespace DebugRenderer
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
38
src/graphics/draw/DrawRenderers.h
Normal file
38
src/graphics/draw/DrawRenderers.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Master include file for all Screen draw renderers
|
||||||
|
*
|
||||||
|
* This file includes all the individual renderer headers to provide
|
||||||
|
* a convenient single include for accessing all draw functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "graphics/draw/ClockRenderer.h"
|
||||||
|
#include "graphics/draw/CompassRenderer.h"
|
||||||
|
#include "graphics/draw/DebugRenderer.h"
|
||||||
|
#include "graphics/draw/NodeListRenderer.h"
|
||||||
|
#include "graphics/draw/ScreenRenderer.h"
|
||||||
|
#include "graphics/draw/UIRenderer.h"
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Collection of all draw renderers
|
||||||
|
*
|
||||||
|
* This namespace provides access to all the specialized rendering
|
||||||
|
* functions organized by category.
|
||||||
|
*/
|
||||||
|
namespace DrawRenderers
|
||||||
|
{
|
||||||
|
// Re-export all renderer namespaces for convenience
|
||||||
|
using namespace ClockRenderer;
|
||||||
|
using namespace CompassRenderer;
|
||||||
|
using namespace DebugRenderer;
|
||||||
|
using namespace NodeListRenderer;
|
||||||
|
using namespace ScreenRenderer;
|
||||||
|
using namespace UIRenderer;
|
||||||
|
|
||||||
|
} // namespace DrawRenderers
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
457
src/graphics/draw/MenuHandler.cpp
Normal file
457
src/graphics/draw/MenuHandler.cpp
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
#include "ClockRenderer.h"
|
||||||
|
#include "GPS.h"
|
||||||
|
#include "MenuHandler.h"
|
||||||
|
#include "MeshRadio.h"
|
||||||
|
#include "MeshService.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "buzz.h"
|
||||||
|
#include "graphics/Screen.h"
|
||||||
|
#include "graphics/draw/UIRenderer.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
|
#include "modules/CannedMessageModule.h"
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
||||||
|
|
||||||
|
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back",
|
||||||
|
"US",
|
||||||
|
"EU_433",
|
||||||
|
"EU_868",
|
||||||
|
"CN",
|
||||||
|
"JP",
|
||||||
|
"ANZ",
|
||||||
|
"KR",
|
||||||
|
"TW",
|
||||||
|
"RU",
|
||||||
|
"IN",
|
||||||
|
"NZ_865",
|
||||||
|
"TH",
|
||||||
|
"LORA_24",
|
||||||
|
"UA_433",
|
||||||
|
"UA_868",
|
||||||
|
"MY_433",
|
||||||
|
"MY_"
|
||||||
|
"919",
|
||||||
|
"SG_"
|
||||||
|
"923",
|
||||||
|
"PH_433",
|
||||||
|
"PH_868",
|
||||||
|
"PH_915",
|
||||||
|
"ANZ_433"};
|
||||||
|
screen->showOverlayBanner(
|
||||||
|
"Set the LoRa region", duration, optionsArray, 23,
|
||||||
|
[](int selected) -> void {
|
||||||
|
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
||||||
|
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
|
||||||
|
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
||||||
|
if (!owner.is_licensed) {
|
||||||
|
bool keygenSuccess = false;
|
||||||
|
if (config.security.private_key.size == 32) {
|
||||||
|
// public key is derived from private, so this will always have the same result.
|
||||||
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Generate new PKI keys");
|
||||||
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
if (keygenSuccess) {
|
||||||
|
config.security.public_key.size = 32;
|
||||||
|
config.security.private_key.size = 32;
|
||||||
|
owner.public_key.size = 32;
|
||||||
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.lora.tx_enabled = true;
|
||||||
|
initRegion();
|
||||||
|
if (myRegion->dutyCycle < 100) {
|
||||||
|
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||||
|
}
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::TwelveHourPicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||||
|
screen->showOverlayBanner("Time Format", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||||
|
} else if (selected == 1) {
|
||||||
|
config.display.use_12h_clock = true;
|
||||||
|
} else {
|
||||||
|
config.display.use_12h_clock = false;
|
||||||
|
}
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::ClockFacePicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Digital", "Analog"};
|
||||||
|
screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||||
|
} else if (selected == 1) {
|
||||||
|
graphics::ClockRenderer::digitalWatchFace = true;
|
||||||
|
screen->setFrames(Screen::FOCUS_CLOCK);
|
||||||
|
} else {
|
||||||
|
graphics::ClockRenderer::digitalWatchFace = false;
|
||||||
|
screen->setFrames(Screen::FOCUS_CLOCK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::TZPicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back",
|
||||||
|
"US/Hawaii",
|
||||||
|
"US/Alaska",
|
||||||
|
"US/Pacific",
|
||||||
|
"US/Arizona",
|
||||||
|
"US/Mountain",
|
||||||
|
"US/Central",
|
||||||
|
"US/Eastern",
|
||||||
|
"UTC",
|
||||||
|
"EU/Western",
|
||||||
|
"EU/"
|
||||||
|
"Central",
|
||||||
|
"EU/Eastern",
|
||||||
|
"Asia/Kolkata",
|
||||||
|
"Asia/Hong_Kong",
|
||||||
|
"AU/AWST",
|
||||||
|
"AU/ACST",
|
||||||
|
"AU/AEST",
|
||||||
|
"Pacific/NZ"};
|
||||||
|
screen->showOverlayBanner("Pick Timezone", 30000, optionsArray, 17, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||||
|
} else if (selected == 1) { // Hawaii
|
||||||
|
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 2) { // Alaska
|
||||||
|
strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 3) { // Pacific
|
||||||
|
strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 4) { // Arizona
|
||||||
|
strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 5) { // Mountain
|
||||||
|
strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 6) { // Central
|
||||||
|
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 7) { // Eastern
|
||||||
|
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 8) { // UTC
|
||||||
|
strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 9) { // EU/Western
|
||||||
|
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 10) { // EU/Central
|
||||||
|
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 11) { // EU/Eastern
|
||||||
|
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 12) { // Asia/Kolkata
|
||||||
|
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 13) { // China
|
||||||
|
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 14) { // AU/AWST
|
||||||
|
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 15) { // AU/ACST
|
||||||
|
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 16) { // AU/AEST
|
||||||
|
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 17) { // NZ
|
||||||
|
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||||
|
}
|
||||||
|
if (selected != 0) {
|
||||||
|
setenv("TZ", config.device.tzdef, 1);
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::clockMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
|
||||||
|
screen->showOverlayBanner("Clock Action", 30000, optionsArray, 4, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_face_picker;
|
||||||
|
screen->setInterval(0);
|
||||||
|
runASAP = true;
|
||||||
|
} else if (selected == 2) {
|
||||||
|
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
|
||||||
|
screen->setInterval(0);
|
||||||
|
runASAP = true;
|
||||||
|
} else if (selected == 3) {
|
||||||
|
menuHandler::menuQueue = menuHandler::TZ_picker;
|
||||||
|
screen->setInterval(0);
|
||||||
|
runASAP = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::messageResponseMenu()
|
||||||
|
{
|
||||||
|
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
int options;
|
||||||
|
if (kb_found) {
|
||||||
|
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 4;
|
||||||
|
} else {
|
||||||
|
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 3;
|
||||||
|
}
|
||||||
|
#ifdef HAS_I2S
|
||||||
|
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext", "Read Aloud"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 5;
|
||||||
|
#endif
|
||||||
|
screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
screen->dismissCurrentFrame();
|
||||||
|
} else if (selected == 2) {
|
||||||
|
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||||
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||||
|
} else {
|
||||||
|
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||||
|
}
|
||||||
|
} else if (selected == 3) {
|
||||||
|
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||||
|
} else {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef HAS_I2S
|
||||||
|
else if (selected == 4) {
|
||||||
|
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||||
|
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||||
|
|
||||||
|
audioThread->readAloud(msg);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::homeBaseMenu()
|
||||||
|
{
|
||||||
|
int options;
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
|
||||||
|
if (kb_found) {
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg", "New Freetext Msg"};
|
||||||
|
#else
|
||||||
|
static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg", "New Freetext Msg"};
|
||||||
|
#endif
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 5;
|
||||||
|
} else {
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg"};
|
||||||
|
#else
|
||||||
|
static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg"};
|
||||||
|
#endif
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 4;
|
||||||
|
}
|
||||||
|
screen->showOverlayBanner("Home Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
if (digitalRead(PIN_EINK_EN) == HIGH) {
|
||||||
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
|
} else {
|
||||||
|
digitalWrite(PIN_EINK_EN, HIGH);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
screen->setOn(false);
|
||||||
|
#endif
|
||||||
|
} else if (selected == 2) {
|
||||||
|
service->refreshLocalMeshNode();
|
||||||
|
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||||
|
LOG_INFO("Position Sent");
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Node Info Sent");
|
||||||
|
}
|
||||||
|
} else if (selected == 3) {
|
||||||
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||||
|
} else if (selected == 4) {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::favoriteBaseMenu()
|
||||||
|
{
|
||||||
|
int options;
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
|
||||||
|
if (kb_found) {
|
||||||
|
static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 3;
|
||||||
|
} else {
|
||||||
|
static const char *optionsArray[] = {"Back", "New Preset Msg"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 2;
|
||||||
|
}
|
||||||
|
screen->showOverlayBanner("Favorites Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||||
|
} else if (selected == 2) {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::positionBaseMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "GPS Toggle", "Compass Point"};
|
||||||
|
screen->showOverlayBanner("Position Action", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
menuQueue = gps_toggle_menu;
|
||||||
|
} else if (selected == 2) {
|
||||||
|
menuQueue = compass_point_north_menu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::nodeListMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Reset NodeDB"};
|
||||||
|
screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
menuQueue = reset_node_db_menu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::resetNodeDBMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||||
|
screen->showOverlayBanner("Confirm Reset NodeDB", 30000, optionsArray, 2, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
disableBluetooth();
|
||||||
|
LOG_INFO("Initiate node-db reset");
|
||||||
|
nodeDB->resetNodes();
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::compassNorthMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Points Up", "Dynamic"};
|
||||||
|
screen->showOverlayBanner("Needle Direction?", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 1 && config.display.compass_north_top != true) {
|
||||||
|
config.display.compass_north_top = true;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
screen->setFrames();
|
||||||
|
} else if (selected == 2 && config.display.compass_north_top != false) {
|
||||||
|
config.display.compass_north_top = false;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
screen->setFrames();
|
||||||
|
} else if (selected == 0) {
|
||||||
|
menuQueue = position_base_menu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::GPSToggleMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||||
|
screen->showOverlayBanner(
|
||||||
|
"Toggle GPS", 30000, optionsArray, 3,
|
||||||
|
[](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
|
||||||
|
playGPSEnableBeep();
|
||||||
|
gps->enable();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 2) {
|
||||||
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
|
||||||
|
playGPSDisableBeep();
|
||||||
|
gps->disable();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else {
|
||||||
|
menuQueue = position_base_menu;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2); // set inital selection
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::BuzzerModeMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
|
||||||
|
screen->showOverlayBanner(
|
||||||
|
"Beep Action", 30000, optionsArray, 4,
|
||||||
|
[](int selected) -> void {
|
||||||
|
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
},
|
||||||
|
config.device.buzzer_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::switchToMUIMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Yes", "No"};
|
||||||
|
screen->showOverlayBanner("Switch to MUI?", 30000, optionsArray, 2, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
||||||
|
config.bluetooth.enabled = false;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::handleMenuSwitch()
|
||||||
|
{
|
||||||
|
switch (menuQueue) {
|
||||||
|
case menu_none:
|
||||||
|
break;
|
||||||
|
case lora_picker:
|
||||||
|
LoraRegionPicker();
|
||||||
|
break;
|
||||||
|
case TZ_picker:
|
||||||
|
TZPicker();
|
||||||
|
break;
|
||||||
|
case twelve_hour_picker:
|
||||||
|
TwelveHourPicker();
|
||||||
|
break;
|
||||||
|
case clock_face_picker:
|
||||||
|
ClockFacePicker();
|
||||||
|
break;
|
||||||
|
case clock_menu:
|
||||||
|
clockMenu();
|
||||||
|
break;
|
||||||
|
case position_base_menu:
|
||||||
|
positionBaseMenu();
|
||||||
|
break;
|
||||||
|
case gps_toggle_menu:
|
||||||
|
GPSToggleMenu();
|
||||||
|
break;
|
||||||
|
case compass_point_north_menu:
|
||||||
|
compassNorthMenu();
|
||||||
|
break;
|
||||||
|
case reset_node_db_menu:
|
||||||
|
resetNodeDBMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
menuQueue = menu_none;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
#endif
|
||||||
40
src/graphics/draw/MenuHandler.h
Normal file
40
src/graphics/draw/MenuHandler.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
class menuHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum screenMenus {
|
||||||
|
menu_none,
|
||||||
|
lora_picker,
|
||||||
|
TZ_picker,
|
||||||
|
twelve_hour_picker,
|
||||||
|
clock_face_picker,
|
||||||
|
clock_menu,
|
||||||
|
position_base_menu,
|
||||||
|
gps_toggle_menu,
|
||||||
|
compass_point_north_menu,
|
||||||
|
reset_node_db_menu
|
||||||
|
};
|
||||||
|
static screenMenus menuQueue;
|
||||||
|
|
||||||
|
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||||
|
static void handleMenuSwitch();
|
||||||
|
static void clockMenu();
|
||||||
|
static void TZPicker();
|
||||||
|
static void TwelveHourPicker();
|
||||||
|
static void ClockFacePicker();
|
||||||
|
static void messageResponseMenu();
|
||||||
|
static void homeBaseMenu();
|
||||||
|
static void favoriteBaseMenu();
|
||||||
|
static void positionBaseMenu();
|
||||||
|
static void compassNorthMenu();
|
||||||
|
static void GPSToggleMenu();
|
||||||
|
static void BuzzerModeMenu();
|
||||||
|
static void switchToMUIMenu();
|
||||||
|
static void nodeListMenu();
|
||||||
|
static void resetNodeDBMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
447
src/graphics/draw/MessageRenderer.cpp
Normal file
447
src/graphics/draw/MessageRenderer.cpp
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
/*
|
||||||
|
BaseUI
|
||||||
|
|
||||||
|
Developed and Maintained By:
|
||||||
|
- Ronald Garcia (HarukiToreda) – Lead development and implementation.
|
||||||
|
- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing.
|
||||||
|
- TonyG (Tropho) – Project management, structural planning, and testing
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
#include "MessageRenderer.h"
|
||||||
|
|
||||||
|
// Core includes
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/emotes.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "meshUtils.h"
|
||||||
|
|
||||||
|
// Additional includes for UI rendering
|
||||||
|
#include "UIRenderer.h"
|
||||||
|
#include "graphics/TimeFormatters.h"
|
||||||
|
|
||||||
|
// Additional includes for dependencies
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// External declarations
|
||||||
|
extern bool hasUnreadMessage;
|
||||||
|
extern meshtastic_DeviceState devicestate;
|
||||||
|
|
||||||
|
using graphics::Emote;
|
||||||
|
using graphics::emotes;
|
||||||
|
using graphics::numEmotes;
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace MessageRenderer
|
||||||
|
{
|
||||||
|
|
||||||
|
// Simple cache based on text hash
|
||||||
|
static size_t cachedKey = 0;
|
||||||
|
static std::vector<std::string> cachedLines;
|
||||||
|
static std::vector<int> cachedHeights;
|
||||||
|
|
||||||
|
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||||
|
{
|
||||||
|
int cursorX = x;
|
||||||
|
const int fontHeight = FONT_HEIGHT_SMALL;
|
||||||
|
|
||||||
|
// === Step 1: Find tallest emote in the line ===
|
||||||
|
int maxIconHeight = fontHeight;
|
||||||
|
for (size_t i = 0; i < line.length();) {
|
||||||
|
bool matched = false;
|
||||||
|
for (int e = 0; e < emoteCount; ++e) {
|
||||||
|
size_t emojiLen = strlen(emotes[e].label);
|
||||||
|
if (line.compare(i, emojiLen, emotes[e].label) == 0) {
|
||||||
|
if (emotes[e].height > maxIconHeight)
|
||||||
|
maxIconHeight = emotes[e].height;
|
||||||
|
i += emojiLen;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
uint8_t c = static_cast<uint8_t>(line[i]);
|
||||||
|
if ((c & 0xE0) == 0xC0)
|
||||||
|
i += 2;
|
||||||
|
else if ((c & 0xF0) == 0xE0)
|
||||||
|
i += 3;
|
||||||
|
else if ((c & 0xF8) == 0xF0)
|
||||||
|
i += 4;
|
||||||
|
else
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Step 2: Baseline alignment ===
|
||||||
|
int lineHeight = std::max(fontHeight, maxIconHeight);
|
||||||
|
int baselineOffset = (lineHeight - fontHeight) / 2;
|
||||||
|
int fontY = y + baselineOffset;
|
||||||
|
int fontMidline = fontY + fontHeight / 2;
|
||||||
|
|
||||||
|
// === Step 3: Render line in segments ===
|
||||||
|
size_t i = 0;
|
||||||
|
bool inBold = false;
|
||||||
|
|
||||||
|
while (i < line.length()) {
|
||||||
|
// Check for ** start/end for faux bold
|
||||||
|
if (line.compare(i, 2, "**") == 0) {
|
||||||
|
inBold = !inBold;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look ahead for the next emote match
|
||||||
|
size_t nextEmotePos = std::string::npos;
|
||||||
|
const Emote *matchedEmote = nullptr;
|
||||||
|
size_t emojiLen = 0;
|
||||||
|
|
||||||
|
for (int e = 0; e < emoteCount; ++e) {
|
||||||
|
size_t pos = line.find(emotes[e].label, i);
|
||||||
|
if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) {
|
||||||
|
nextEmotePos = pos;
|
||||||
|
matchedEmote = &emotes[e];
|
||||||
|
emojiLen = strlen(emotes[e].label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render normal text segment up to the emote or bold toggle
|
||||||
|
size_t nextControl = std::min(nextEmotePos, line.find("**", i));
|
||||||
|
if (nextControl == std::string::npos)
|
||||||
|
nextControl = line.length();
|
||||||
|
|
||||||
|
if (nextControl > i) {
|
||||||
|
std::string textChunk = line.substr(i, nextControl - i);
|
||||||
|
if (inBold) {
|
||||||
|
// Faux bold: draw twice, offset by 1px
|
||||||
|
display->drawString(cursorX + 1, fontY, textChunk.c_str());
|
||||||
|
}
|
||||||
|
display->drawString(cursorX, fontY, textChunk.c_str());
|
||||||
|
cursorX += display->getStringWidth(textChunk.c_str());
|
||||||
|
i = nextControl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the emote (if found)
|
||||||
|
if (matchedEmote && i == nextEmotePos) {
|
||||||
|
int iconY = fontMidline - matchedEmote->height / 2 - 1;
|
||||||
|
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
|
||||||
|
cursorX += matchedEmote->width + 1;
|
||||||
|
i += emojiLen;
|
||||||
|
} else {
|
||||||
|
// No more emotes — render the rest of the line
|
||||||
|
std::string remaining = line.substr(i);
|
||||||
|
if (inBold) {
|
||||||
|
display->drawString(cursorX + 1, fontY, remaining.c_str());
|
||||||
|
}
|
||||||
|
display->drawString(cursorX, fontY, remaining.c_str());
|
||||||
|
cursorX += display->getStringWidth(remaining.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
// Clear the unread message indicator when viewing the message
|
||||||
|
hasUnreadMessage = false;
|
||||||
|
|
||||||
|
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||||
|
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||||
|
|
||||||
|
display->clear();
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
|
const int navHeight = FONT_HEIGHT_SMALL;
|
||||||
|
const int scrollBottom = SCREEN_HEIGHT - navHeight;
|
||||||
|
const int usableHeight = scrollBottom;
|
||||||
|
const int textWidth = SCREEN_WIDTH;
|
||||||
|
|
||||||
|
bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||||
|
bool isBold = config.display.heading_bold;
|
||||||
|
|
||||||
|
// === Set Title
|
||||||
|
const char *titleStr = "Messages";
|
||||||
|
|
||||||
|
// Check if we have more than an empty message to show
|
||||||
|
char messageBuf[237];
|
||||||
|
snprintf(messageBuf, sizeof(messageBuf), "%s", msg);
|
||||||
|
if (strlen(messageBuf) == 0) {
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
const char *messageString = "No messages";
|
||||||
|
int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2);
|
||||||
|
display->drawString(center_text, getTextPositions(display)[2], messageString);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Header Construction ===
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
|
char headerStr[80];
|
||||||
|
const char *sender = "???";
|
||||||
|
if (node && node->has_user) {
|
||||||
|
if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) {
|
||||||
|
sender = node->user.long_name;
|
||||||
|
} else {
|
||||||
|
sender = node->user.short_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||||
|
uint8_t timestampHours, timestampMinutes;
|
||||||
|
int32_t daysAgo;
|
||||||
|
bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo);
|
||||||
|
|
||||||
|
if (useTimestamp && minutes >= 15 && daysAgo == 0) {
|
||||||
|
std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At";
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
bool isPM = timestampHours >= 12;
|
||||||
|
timestampHours = timestampHours % 12;
|
||||||
|
if (timestampHours == 0)
|
||||||
|
timestampHours = 12;
|
||||||
|
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes,
|
||||||
|
isPM ? "p" : "a", sender);
|
||||||
|
} else {
|
||||||
|
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes,
|
||||||
|
sender);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
|
||||||
|
sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
|
#ifndef EXCLUDE_EMOJI
|
||||||
|
// === Bounce animation setup ===
|
||||||
|
static uint32_t lastBounceTime = 0;
|
||||||
|
static int bounceY = 0;
|
||||||
|
const int bounceRange = 2; // Max pixels to bounce up/down
|
||||||
|
const int bounceInterval = 10; // How quickly to change bounce direction (ms)
|
||||||
|
|
||||||
|
if (now - lastBounceTime >= bounceInterval) {
|
||||||
|
lastBounceTime = now;
|
||||||
|
bounceY = (bounceY + 1) % (bounceRange * 2);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < numEmotes; ++i) {
|
||||||
|
const Emote &e = emotes[i];
|
||||||
|
if (strcmp(msg, e.label) == 0) {
|
||||||
|
int headerY = getTextPositions(display)[1]; // same as scrolling header line
|
||||||
|
display->drawString(x + 3, headerY, headerStr);
|
||||||
|
if (isInverted && isBold)
|
||||||
|
display->drawString(x + 4, headerY, headerStr);
|
||||||
|
|
||||||
|
// Draw separator (same as scroll version)
|
||||||
|
for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
|
||||||
|
display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the emote below the header line + separator + nav
|
||||||
|
int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight;
|
||||||
|
int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange;
|
||||||
|
display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap);
|
||||||
|
|
||||||
|
// Draw header at the end to sort out overlapping elements
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// === Generate the cache key ===
|
||||||
|
size_t currentKey = (size_t)mp.from;
|
||||||
|
currentKey ^= ((size_t)mp.to << 8);
|
||||||
|
currentKey ^= ((size_t)mp.rx_time << 16);
|
||||||
|
currentKey ^= ((size_t)mp.id << 24);
|
||||||
|
|
||||||
|
if (cachedKey != currentKey) {
|
||||||
|
LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
|
||||||
|
|
||||||
|
// Cache miss - regenerate lines and heights
|
||||||
|
cachedLines = generateLines(display, headerStr, messageBuf, textWidth);
|
||||||
|
cachedHeights = calculateLineHeights(cachedLines, emotes);
|
||||||
|
cachedKey = currentKey;
|
||||||
|
} else {
|
||||||
|
// Cache hit but update the header line with current time information
|
||||||
|
cachedLines[0] = std::string(headerStr);
|
||||||
|
// The header always has a fixed height since it doesn't contain emotes
|
||||||
|
// As per calculateLineHeights logic for lines without emotes:
|
||||||
|
cachedHeights[0] = FONT_HEIGHT_SMALL - 2;
|
||||||
|
if (cachedHeights[0] < 8)
|
||||||
|
cachedHeights[0] = 8; // minimum safety
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Scrolling logic ===
|
||||||
|
int totalHeight = 0;
|
||||||
|
for (size_t i = 1; i < cachedHeights.size(); ++i) {
|
||||||
|
totalHeight += cachedHeights[i];
|
||||||
|
}
|
||||||
|
int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height
|
||||||
|
int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back());
|
||||||
|
|
||||||
|
static float scrollY = 0.0f;
|
||||||
|
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
|
||||||
|
static bool waitingToReset = false, scrollStarted = false;
|
||||||
|
|
||||||
|
// === Smooth scrolling adjustment ===
|
||||||
|
// You can tweak this divisor to change how smooth it scrolls.
|
||||||
|
// Lower = smoother, but can feel slow.
|
||||||
|
float delta = (now - lastTime) / 400.0f;
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
const float scrollSpeed = 2.0f; // pixels per second
|
||||||
|
|
||||||
|
// Delay scrolling start by 2 seconds
|
||||||
|
if (scrollStartDelay == 0)
|
||||||
|
scrollStartDelay = now;
|
||||||
|
if (!scrollStarted && now - scrollStartDelay > 2000)
|
||||||
|
scrollStarted = true;
|
||||||
|
|
||||||
|
if (totalHeight > usableScrollHeight) {
|
||||||
|
if (scrollStarted) {
|
||||||
|
if (!waitingToReset) {
|
||||||
|
scrollY += delta * scrollSpeed;
|
||||||
|
if (scrollY >= scrollStop) {
|
||||||
|
scrollY = scrollStop;
|
||||||
|
waitingToReset = true;
|
||||||
|
pauseStart = lastTime;
|
||||||
|
}
|
||||||
|
} else if (lastTime - pauseStart > 3000) {
|
||||||
|
scrollY = 0;
|
||||||
|
waitingToReset = false;
|
||||||
|
scrollStarted = false;
|
||||||
|
scrollStartDelay = lastTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrollY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int scrollOffset = static_cast<int>(scrollY);
|
||||||
|
int yOffset = -scrollOffset + getTextPositions(display)[1];
|
||||||
|
for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
|
||||||
|
display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Render visible lines ===
|
||||||
|
renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold);
|
||||||
|
|
||||||
|
// Draw header at the end to sort out overlapping elements
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)
|
||||||
|
{
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
lines.push_back(std::string(headerStr)); // Header line is always first
|
||||||
|
|
||||||
|
std::string line, word;
|
||||||
|
for (int i = 0; messageBuf[i]; ++i) {
|
||||||
|
char ch = messageBuf[i];
|
||||||
|
if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 &&
|
||||||
|
(unsigned char)messageBuf[i + 2] == 0x99) {
|
||||||
|
ch = '\''; // plain apostrophe
|
||||||
|
i += 2; // skip over the extra UTF-8 bytes
|
||||||
|
}
|
||||||
|
if (ch == '\n') {
|
||||||
|
if (!word.empty())
|
||||||
|
line += word;
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
line.clear();
|
||||||
|
word.clear();
|
||||||
|
} else if (ch == ' ') {
|
||||||
|
line += word + ' ';
|
||||||
|
word.clear();
|
||||||
|
} else {
|
||||||
|
word += ch;
|
||||||
|
std::string test = line + word;
|
||||||
|
// Keep these lines for diagnostics
|
||||||
|
// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
|
||||||
|
// LOG_INFO("Current String: %s", test.c_str());
|
||||||
|
if (display->getStringWidth(test.c_str()) > textWidth) {
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
line = word;
|
||||||
|
word.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!word.empty())
|
||||||
|
line += word;
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes)
|
||||||
|
{
|
||||||
|
std::vector<int> rowHeights;
|
||||||
|
|
||||||
|
for (const auto &_line : lines) {
|
||||||
|
int lineHeight = FONT_HEIGHT_SMALL;
|
||||||
|
bool hasEmote = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < numEmotes; ++i) {
|
||||||
|
const Emote &e = emotes[i];
|
||||||
|
if (_line.find(e.label) != std::string::npos) {
|
||||||
|
lineHeight = std::max(lineHeight, e.height);
|
||||||
|
hasEmote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tighter spacing if no emotes on this line
|
||||||
|
if (!hasEmote) {
|
||||||
|
lineHeight -= 2; // reduce by 2px for tighter spacing
|
||||||
|
if (lineHeight < 8)
|
||||||
|
lineHeight = 8; // minimum safety
|
||||||
|
}
|
||||||
|
|
||||||
|
rowHeights.push_back(lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowHeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||||
|
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
int lineY = yOffset;
|
||||||
|
for (size_t j = 0; j < i; ++j)
|
||||||
|
lineY += rowHeights[j];
|
||||||
|
if (lineY > -rowHeights[i] && lineY < scrollBottom) {
|
||||||
|
if (i == 0 && isInverted) {
|
||||||
|
display->drawString(x, lineY, lines[i].c_str());
|
||||||
|
if (isBold)
|
||||||
|
display->drawString(x, lineY, lines[i].c_str());
|
||||||
|
} else {
|
||||||
|
drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MessageRenderer
|
||||||
|
} // namespace graphics
|
||||||
|
#endif
|
||||||
30
src/graphics/draw/MessageRenderer.h
Normal file
30
src/graphics/draw/MessageRenderer.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "OLEDDisplay.h"
|
||||||
|
#include "OLEDDisplayUi.h"
|
||||||
|
#include "graphics/emotes.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace MessageRenderer
|
||||||
|
{
|
||||||
|
|
||||||
|
// Text and emote rendering
|
||||||
|
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
|
||||||
|
|
||||||
|
/// Draws the text message frame for displaying received messages
|
||||||
|
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Function to generate lines with word wrapping
|
||||||
|
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
|
||||||
|
|
||||||
|
// Function to calculate heights for each line
|
||||||
|
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
|
||||||
|
|
||||||
|
// Function to render the message content
|
||||||
|
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||||
|
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
|
||||||
|
|
||||||
|
} // namespace MessageRenderer
|
||||||
|
} // namespace graphics
|
||||||
572
src/graphics/draw/NodeListRenderer.cpp
Normal file
572
src/graphics/draw/NodeListRenderer.cpp
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
#include "CompassRenderer.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "NodeListRenderer.h"
|
||||||
|
#include "UIRenderer.h"
|
||||||
|
#include "gps/GeoCoord.h"
|
||||||
|
#include "gps/RTC.h" // for getTime() function
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
|
#include "meshUtils.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// Forward declarations for functions defined in Screen.cpp
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
extern bool haveGlyphs(const char *str);
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
// Global screen instance
|
||||||
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace NodeListRenderer
|
||||||
|
{
|
||||||
|
|
||||||
|
// Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here
|
||||||
|
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display)
|
||||||
|
{
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
uint8_t rowMask = (1 << row);
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
uint8_t colData = pgm_read_byte(&bitmapXBM[col]);
|
||||||
|
if (colData & rowMask) {
|
||||||
|
// Note: rows become X, columns become Y after transpose
|
||||||
|
display->fillRect(x + row * 2, y + col * 2, 2, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static variables for dynamic cycling
|
||||||
|
static NodeListMode currentMode = MODE_LAST_HEARD;
|
||||||
|
static int scrollIndex = 0;
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Utility Functions
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
|
||||||
|
{
|
||||||
|
static char nodeName[16] = "?";
|
||||||
|
if (node->has_user && strlen(node->user.short_name) > 0) {
|
||||||
|
bool valid = true;
|
||||||
|
const char *name = node->user.short_name;
|
||||||
|
for (size_t i = 0; i < strlen(name); i++) {
|
||||||
|
uint8_t c = (uint8_t)name[i];
|
||||||
|
if (c < 32 || c > 126) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valid) {
|
||||||
|
strncpy(nodeName, name, sizeof(nodeName) - 1);
|
||||||
|
nodeName[sizeof(nodeName) - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strcpy(nodeName, "?");
|
||||||
|
}
|
||||||
|
return nodeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *getCurrentModeTitle(int screenWidth)
|
||||||
|
{
|
||||||
|
switch (currentMode) {
|
||||||
|
case MODE_LAST_HEARD:
|
||||||
|
return "Last Heard";
|
||||||
|
case MODE_HOP_SIGNAL:
|
||||||
|
#ifdef USE_EINK
|
||||||
|
return "Hops/Sig";
|
||||||
|
#else
|
||||||
|
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
|
||||||
|
#endif
|
||||||
|
case MODE_DISTANCE:
|
||||||
|
return "Distance";
|
||||||
|
default:
|
||||||
|
return "Nodes";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use dynamic timing based on mode
|
||||||
|
unsigned long getModeCycleIntervalMs()
|
||||||
|
{
|
||||||
|
return 3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bearing between two lat/lon points
|
||||||
|
float calculateBearing(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||||
|
double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
|
||||||
|
double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
|
||||||
|
double bearing = atan2(y, x) * RAD_TO_DEG;
|
||||||
|
return fmod(bearing + 360.0, 360.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||||
|
{
|
||||||
|
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||||
|
{
|
||||||
|
int columnWidth = display->getWidth() / 2;
|
||||||
|
int separatorX = x + columnWidth - 2;
|
||||||
|
for (int y = yStart; y <= yEnd; y += 2) {
|
||||||
|
display->setPixel(separatorX, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY)
|
||||||
|
{
|
||||||
|
if (totalEntries <= visibleNodeRows * columns)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int scrollbarX = display->getWidth() - 2;
|
||||||
|
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||||
|
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||||
|
int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
|
||||||
|
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||||
|
|
||||||
|
for (int i = 0; i < thumbHeight; i++) {
|
||||||
|
display->setPixel(scrollbarX, thumbY + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Entry Renderers
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
|
{
|
||||||
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||||
|
|
||||||
|
const char *nodeName = getSafeNodeName(node);
|
||||||
|
|
||||||
|
char timeStr[10];
|
||||||
|
uint32_t seconds = sinceLastSeen(node);
|
||||||
|
if (seconds == 0 || seconds == UINT32_MAX) {
|
||||||
|
snprintf(timeStr, sizeof(timeStr), "?");
|
||||||
|
} else {
|
||||||
|
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||||
|
snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"),
|
||||||
|
(days ? days
|
||||||
|
: hours ? hours
|
||||||
|
: minutes),
|
||||||
|
(days ? 'd'
|
||||||
|
: hours ? 'h'
|
||||||
|
: 'm'));
|
||||||
|
}
|
||||||
|
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
|
||||||
|
if (node->is_favorite) {
|
||||||
|
if (isHighResolution) {
|
||||||
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
|
} else {
|
||||||
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int rightEdge = x + columnWidth - timeOffset;
|
||||||
|
if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time
|
||||||
|
rightEdge -= 1;
|
||||||
|
int textWidth = display->getStringWidth(timeStr);
|
||||||
|
display->drawString(rightEdge - textWidth, y, timeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
|
{
|
||||||
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
|
||||||
|
int nameMaxWidth = columnWidth - 25;
|
||||||
|
int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||||
|
int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||||
|
|
||||||
|
int barsXOffset = columnWidth - barsOffset;
|
||||||
|
|
||||||
|
const char *nodeName = getSafeNodeName(node);
|
||||||
|
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
|
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||||
|
if (node->is_favorite) {
|
||||||
|
if (isHighResolution) {
|
||||||
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
|
} else {
|
||||||
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw signal strength bars
|
||||||
|
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||||
|
int barWidth = 2;
|
||||||
|
int barStartX = x + barsXOffset;
|
||||||
|
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||||
|
|
||||||
|
for (int b = 0; b < 4; b++) {
|
||||||
|
if (b < bars) {
|
||||||
|
int height = (b * 2);
|
||||||
|
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw hop count
|
||||||
|
char hopStr[6] = "";
|
||||||
|
if (node->has_hops_away && node->hops_away > 0)
|
||||||
|
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
||||||
|
|
||||||
|
if (hopStr[0] != '\0') {
|
||||||
|
int rightEdge = x + columnWidth - hopOffset;
|
||||||
|
int textWidth = display->getStringWidth(hopStr);
|
||||||
|
display->drawString(rightEdge - textWidth, y, hopStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
|
{
|
||||||
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||||
|
|
||||||
|
const char *nodeName = getSafeNodeName(node);
|
||||||
|
char distStr[10] = "";
|
||||||
|
|
||||||
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||||
|
double lat1 = ourNode->position.latitude_i * 1e-7;
|
||||||
|
double lon1 = ourNode->position.longitude_i * 1e-7;
|
||||||
|
double lat2 = node->position.latitude_i * 1e-7;
|
||||||
|
double lon2 = node->position.longitude_i * 1e-7;
|
||||||
|
|
||||||
|
double earthRadiusKm = 6371.0;
|
||||||
|
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
||||||
|
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||||
|
|
||||||
|
double a =
|
||||||
|
sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2);
|
||||||
|
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||||
|
double distanceKm = earthRadiusKm * c;
|
||||||
|
|
||||||
|
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||||
|
double miles = distanceKm * 0.621371;
|
||||||
|
if (miles < 0.1) {
|
||||||
|
int feet = (int)(miles * 5280);
|
||||||
|
if (feet < 1000)
|
||||||
|
snprintf(distStr, sizeof(distStr), "%dft", feet);
|
||||||
|
else
|
||||||
|
snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max
|
||||||
|
} else {
|
||||||
|
int roundedMiles = (int)(miles + 0.5);
|
||||||
|
if (roundedMiles < 1000)
|
||||||
|
snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles);
|
||||||
|
else
|
||||||
|
snprintf(distStr, sizeof(distStr), "999"); // Max display cap
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (distanceKm < 1.0) {
|
||||||
|
int meters = (int)(distanceKm * 1000);
|
||||||
|
if (meters < 1000)
|
||||||
|
snprintf(distStr, sizeof(distStr), "%dm", meters);
|
||||||
|
else
|
||||||
|
snprintf(distStr, sizeof(distStr), "1k");
|
||||||
|
} else {
|
||||||
|
int km = (int)(distanceKm + 0.5);
|
||||||
|
if (km < 1000)
|
||||||
|
snprintf(distStr, sizeof(distStr), "%dk", km);
|
||||||
|
else
|
||||||
|
snprintf(distStr, sizeof(distStr), "999");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||||
|
if (node->is_favorite) {
|
||||||
|
if (isHighResolution) {
|
||||||
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
|
} else {
|
||||||
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen(distStr) > 0) {
|
||||||
|
int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||||
|
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||||
|
int rightEdge = x + columnWidth - offset;
|
||||||
|
int textWidth = display->getStringWidth(distStr);
|
||||||
|
display->drawString(rightEdge - textWidth, y, distStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
|
{
|
||||||
|
switch (currentMode) {
|
||||||
|
case MODE_LAST_HEARD:
|
||||||
|
drawEntryLastHeard(display, node, x, y, columnWidth);
|
||||||
|
break;
|
||||||
|
case MODE_HOP_SIGNAL:
|
||||||
|
drawEntryHopSignal(display, node, x, y, columnWidth);
|
||||||
|
break;
|
||||||
|
case MODE_DISTANCE:
|
||||||
|
drawNodeDistance(display, node, x, y, columnWidth);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
|
{
|
||||||
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
|
||||||
|
// Adjust max text width depending on column and screen width
|
||||||
|
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||||
|
|
||||||
|
const char *nodeName = getSafeNodeName(node);
|
||||||
|
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||||
|
if (node->is_favorite) {
|
||||||
|
if (isHighResolution) {
|
||||||
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
|
} else {
|
||||||
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||||
|
double userLat, double userLon)
|
||||||
|
{
|
||||||
|
if (!nodeDB->hasValidPosition(node))
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||||
|
|
||||||
|
int centerX = x + columnWidth - arrowXOffset;
|
||||||
|
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||||
|
|
||||||
|
double nodeLat = node->position.latitude_i * 1e-7;
|
||||||
|
double nodeLon = node->position.longitude_i * 1e-7;
|
||||||
|
float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon);
|
||||||
|
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||||
|
float angle = relativeBearing * DEG_TO_RAD;
|
||||||
|
|
||||||
|
// Shrink size by 2px
|
||||||
|
int size = FONT_HEIGHT_SMALL - 5;
|
||||||
|
float halfSize = size / 2.0;
|
||||||
|
|
||||||
|
// Point of the arrow
|
||||||
|
int tipX = centerX + halfSize * cos(angle);
|
||||||
|
int tipY = centerY - halfSize * sin(angle);
|
||||||
|
|
||||||
|
float baseAngle = radians(35);
|
||||||
|
float sideLen = halfSize * 0.95;
|
||||||
|
float notchInset = halfSize * 0.35;
|
||||||
|
|
||||||
|
// Left and right corners
|
||||||
|
int leftX = centerX + sideLen * cos(angle + PI - baseAngle);
|
||||||
|
int leftY = centerY - sideLen * sin(angle + PI - baseAngle);
|
||||||
|
|
||||||
|
int rightX = centerX + sideLen * cos(angle + PI + baseAngle);
|
||||||
|
int rightY = centerY - sideLen * sin(angle + PI + baseAngle);
|
||||||
|
|
||||||
|
// Center notch (cut-in)
|
||||||
|
int notchX = centerX - notchInset * cos(angle);
|
||||||
|
int notchY = centerY + notchInset * sin(angle);
|
||||||
|
|
||||||
|
// Draw the chevron-style arrowhead
|
||||||
|
display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY);
|
||||||
|
display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Main Screen Functions
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||||
|
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
||||||
|
{
|
||||||
|
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||||
|
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||||
|
|
||||||
|
int columnWidth = display->getWidth() / 2;
|
||||||
|
|
||||||
|
display->clear();
|
||||||
|
|
||||||
|
// Draw the battery/time header
|
||||||
|
graphics::drawCommonHeader(display, x, y, title);
|
||||||
|
|
||||||
|
// Space below header
|
||||||
|
y += COMMON_HEADER_HEIGHT;
|
||||||
|
|
||||||
|
int totalEntries = nodeDB->getNumMeshNodes();
|
||||||
|
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||||
|
|
||||||
|
int visibleNodeRows = totalRowsAvailable;
|
||||||
|
int totalColumns = 2;
|
||||||
|
|
||||||
|
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||||
|
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
|
||||||
|
startIndex++; // skip own node
|
||||||
|
}
|
||||||
|
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||||
|
|
||||||
|
int yOffset = 0;
|
||||||
|
int col = 0;
|
||||||
|
int lastNodeY = y;
|
||||||
|
int shownCount = 0;
|
||||||
|
int rowCount = 0;
|
||||||
|
|
||||||
|
for (int i = startIndex; i < endIndex; ++i) {
|
||||||
|
int xPos = x + (col * columnWidth);
|
||||||
|
int yPos = y + yOffset;
|
||||||
|
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
|
||||||
|
|
||||||
|
if (extras) {
|
||||||
|
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||||
|
yOffset += rowYOffset;
|
||||||
|
shownCount++;
|
||||||
|
rowCount++;
|
||||||
|
|
||||||
|
if (rowCount >= totalRowsAvailable) {
|
||||||
|
yOffset = 0;
|
||||||
|
rowCount = 0;
|
||||||
|
col++;
|
||||||
|
if (col > (totalColumns - 1))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw column separator
|
||||||
|
if (shownCount > 0) {
|
||||||
|
const int firstNodeY = y + 3;
|
||||||
|
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int scrollStartY = y + 3;
|
||||||
|
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Screen Frame Functions
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
#ifndef USE_EINK
|
||||||
|
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
// Static variables to track mode and duration
|
||||||
|
static NodeListMode lastRenderedMode = MODE_COUNT;
|
||||||
|
static unsigned long modeStartTime = 0;
|
||||||
|
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
// On very first call (on boot or state enter)
|
||||||
|
if (lastRenderedMode == MODE_COUNT) {
|
||||||
|
currentMode = MODE_LAST_HEARD;
|
||||||
|
modeStartTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time to switch to next mode?
|
||||||
|
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||||
|
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
|
||||||
|
modeStartTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render screen based on currentMode
|
||||||
|
const char *title = getCurrentModeTitle(display->getWidth());
|
||||||
|
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
|
||||||
|
|
||||||
|
// Track the last mode to avoid reinitializing modeStartTime
|
||||||
|
lastRenderedMode = currentMode;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_EINK
|
||||||
|
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
const char *title = "Last Heard";
|
||||||
|
drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
const char *title = "Hops/Sig";
|
||||||
|
#else
|
||||||
|
|
||||||
|
const char *title = "Hops/Signal";
|
||||||
|
#endif
|
||||||
|
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
const char *title = "Distance";
|
||||||
|
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
float heading = 0;
|
||||||
|
bool validHeading = false;
|
||||||
|
double lat = 0;
|
||||||
|
double lon = 0;
|
||||||
|
|
||||||
|
#if HAS_GPS
|
||||||
|
if (screen->hasHeading()) {
|
||||||
|
heading = screen->getHeading(); // degrees
|
||||||
|
validHeading = true;
|
||||||
|
} else {
|
||||||
|
heading = screen->estimatedHeading(lat, lon);
|
||||||
|
validHeading = !isnan(heading);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!validHeading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||||
|
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields)
|
||||||
|
{
|
||||||
|
// The coordinates define the left starting point of the text
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const char **f = fields;
|
||||||
|
int xo = x, yo = y;
|
||||||
|
while (*f) {
|
||||||
|
display->drawString(xo, yo, *f);
|
||||||
|
if ((display->getColor() == BLACK) && config.display.heading_bold)
|
||||||
|
display->drawString(xo + 1, yo, *f);
|
||||||
|
|
||||||
|
display->setColor(WHITE);
|
||||||
|
yo += FONT_HEIGHT_SMALL;
|
||||||
|
if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) {
|
||||||
|
xo += SCREEN_WIDTH / 2;
|
||||||
|
yo = 0;
|
||||||
|
}
|
||||||
|
f++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace NodeListRenderer
|
||||||
|
} // namespace graphics
|
||||||
|
#endif
|
||||||
62
src/graphics/draw/NodeListRenderer.h
Normal file
62
src/graphics/draw/NodeListRenderer.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "graphics/Screen.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Forward declarations
|
||||||
|
class Screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Node list and entry rendering functions
|
||||||
|
*
|
||||||
|
* Contains all functions related to drawing node lists and individual node entries
|
||||||
|
* including last heard, hop signal, distance, and compass views.
|
||||||
|
*/
|
||||||
|
namespace NodeListRenderer
|
||||||
|
{
|
||||||
|
// Entry renderer function types
|
||||||
|
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
||||||
|
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
||||||
|
|
||||||
|
// Node list mode enumeration
|
||||||
|
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
|
||||||
|
|
||||||
|
// Main node list screen function
|
||||||
|
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||||
|
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
||||||
|
double lon = 0);
|
||||||
|
|
||||||
|
// Entry renderers
|
||||||
|
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||||
|
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||||
|
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||||
|
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||||
|
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||||
|
|
||||||
|
// Extras renderers
|
||||||
|
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||||
|
double userLat, double userLon);
|
||||||
|
|
||||||
|
// Screen frame functions
|
||||||
|
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
const char *getCurrentModeTitle(int screenWidth);
|
||||||
|
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
|
||||||
|
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
||||||
|
|
||||||
|
// Bitmap drawing function
|
||||||
|
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);
|
||||||
|
|
||||||
|
} // namespace NodeListRenderer
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
320
src/graphics/draw/NotificationRenderer.cpp
Normal file
320
src/graphics/draw/NotificationRenderer.cpp
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
|
||||||
|
#include "DisplayFormatters.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "NotificationRenderer.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace meshtastic;
|
||||||
|
|
||||||
|
// External references to global variables from Screen.cpp
|
||||||
|
extern std::vector<std::string> functionSymbol;
|
||||||
|
extern std::string functionSymbolString;
|
||||||
|
extern bool hasUnreadMessage;
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
char NotificationRenderer::inEvent = INPUT_BROKER_NONE;
|
||||||
|
int8_t NotificationRenderer::curSelected = 0;
|
||||||
|
char NotificationRenderer::alertBannerMessage[256] = {0};
|
||||||
|
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
|
||||||
|
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
|
||||||
|
const char **NotificationRenderer::optionsArrayPtr = nullptr;
|
||||||
|
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
||||||
|
bool NotificationRenderer::pauseBanner = false;
|
||||||
|
|
||||||
|
// Used on boot when a certificate is being created
|
||||||
|
void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->drawString(64 + x, y, "Creating SSL certificate");
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
yield();
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
if ((millis() / 1000) % 2) {
|
||||||
|
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . .");
|
||||||
|
} else {
|
||||||
|
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
|
{
|
||||||
|
if (!isOverlayBannerShowing() || pauseBanner)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// === Layout Configuration ===
|
||||||
|
constexpr uint16_t vPadding = 2;
|
||||||
|
|
||||||
|
// Setup font and alignment
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
uint16_t optionWidths[alertBannerOptions] = {0};
|
||||||
|
uint16_t maxWidth = 0;
|
||||||
|
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
|
||||||
|
uint16_t lineWidths[MAX_LINES] = {0};
|
||||||
|
uint16_t lineLengths[MAX_LINES] = {0};
|
||||||
|
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||||
|
uint16_t lineCount = 0;
|
||||||
|
char lineBuffer[40] = {0};
|
||||||
|
|
||||||
|
// Parse lines
|
||||||
|
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||||
|
lineStarts[lineCount] = alertBannerMessage;
|
||||||
|
|
||||||
|
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||||
|
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||||
|
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||||
|
if (lineStarts[lineCount + 1][0] == '\n')
|
||||||
|
lineStarts[lineCount + 1] += 1;
|
||||||
|
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||||
|
if (lineWidths[lineCount] > maxWidth)
|
||||||
|
maxWidth = lineWidths[lineCount];
|
||||||
|
lineCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure option widths
|
||||||
|
for (int i = 0; i < alertBannerOptions; i++) {
|
||||||
|
optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true);
|
||||||
|
if (optionWidths[i] > maxWidth)
|
||||||
|
maxWidth = optionWidths[i];
|
||||||
|
if (optionWidths[i] + arrowsWidth > maxWidth)
|
||||||
|
maxWidth = optionWidths[i] + arrowsWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle input
|
||||||
|
if (alertBannerOptions > 0) {
|
||||||
|
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
|
||||||
|
curSelected--;
|
||||||
|
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
|
curSelected++;
|
||||||
|
} else if (inEvent == INPUT_BROKER_SELECT) {
|
||||||
|
alertBannerCallback(curSelected);
|
||||||
|
alertBannerMessage[0] = '\0';
|
||||||
|
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||||
|
alertBannerMessage[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curSelected == -1)
|
||||||
|
curSelected = alertBannerOptions - 1;
|
||||||
|
if (curSelected == alertBannerOptions)
|
||||||
|
curSelected = 0;
|
||||||
|
} else {
|
||||||
|
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
|
||||||
|
alertBannerMessage[0] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inEvent = INPUT_BROKER_NONE;
|
||||||
|
if (alertBannerMessage[0] == '\0')
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint16_t totalLines = lineCount + alertBannerOptions;
|
||||||
|
|
||||||
|
uint16_t screenHeight = display->height();
|
||||||
|
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||||
|
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||||
|
uint8_t linesShown = lineCount;
|
||||||
|
const char *linePointers[visibleTotalLines]; // this is sort of a dynamic allocation
|
||||||
|
|
||||||
|
// copy the linestarts to display to the linePointers holder
|
||||||
|
for (int i = 0; i < lineCount; i++) {
|
||||||
|
linePointers[i] = lineStarts[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t firstOptionToShow = 0;
|
||||||
|
if (alertBannerOptions > 0) {
|
||||||
|
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
|
||||||
|
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
|
||||||
|
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
|
||||||
|
else
|
||||||
|
firstOptionToShow = curSelected - 1;
|
||||||
|
} else {
|
||||||
|
firstOptionToShow = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||||
|
if (i == curSelected) {
|
||||||
|
strncpy(lineBuffer, "> ", 3);
|
||||||
|
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
|
||||||
|
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
|
||||||
|
lineBuffer[39] = '\0';
|
||||||
|
linePointers[linesShown] = lineBuffer;
|
||||||
|
} else {
|
||||||
|
linePointers[linesShown] = optionsArrayPtr[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alertBannerOptions > 0) {
|
||||||
|
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth);
|
||||||
|
} else {
|
||||||
|
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||||
|
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
|
||||||
|
{
|
||||||
|
bool is_picker = false;
|
||||||
|
uint16_t lineCount = 0;
|
||||||
|
// === Layout Configuration ===
|
||||||
|
constexpr uint16_t hPadding = 5;
|
||||||
|
constexpr uint16_t vPadding = 2;
|
||||||
|
bool needs_bell = false;
|
||||||
|
uint16_t lineWidths[totalLines] = {0};
|
||||||
|
|
||||||
|
if (maxWidth != 0)
|
||||||
|
is_picker = true;
|
||||||
|
// seelction box
|
||||||
|
|
||||||
|
while (lineCount < totalLines) {
|
||||||
|
if (lines[lineCount] != nullptr) {
|
||||||
|
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], strlen(lines[lineCount]), true);
|
||||||
|
if (!is_picker) {
|
||||||
|
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
|
||||||
|
if (lineWidths[lineCount] > maxWidth)
|
||||||
|
maxWidth = lineWidths[lineCount];
|
||||||
|
}
|
||||||
|
lineCount++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// count lines
|
||||||
|
|
||||||
|
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||||
|
if (needs_bell) {
|
||||||
|
if (isHighResolution && boxWidth <= 150)
|
||||||
|
boxWidth += 26;
|
||||||
|
if (!isHighResolution && boxWidth <= 100)
|
||||||
|
boxWidth += 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t screenHeight = display->height();
|
||||||
|
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||||
|
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||||
|
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||||
|
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||||
|
|
||||||
|
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||||
|
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||||
|
|
||||||
|
// === Draw Box ===
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||||
|
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||||
|
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||||
|
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||||
|
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||||
|
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||||
|
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||||
|
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
|
||||||
|
// === Draw Content ===
|
||||||
|
int16_t lineY = boxTop + vPadding;
|
||||||
|
for (int i = 0; i < lineCount; i++) {
|
||||||
|
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||||
|
if (needs_bell && i == 0) {
|
||||||
|
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||||
|
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
|
||||||
|
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this is a pop-up or a pick list
|
||||||
|
if (alertBannerOptions > 0 && i == 0) {
|
||||||
|
// Pick List
|
||||||
|
display->setColor(WHITE);
|
||||||
|
int background_yOffset = 1;
|
||||||
|
// Determine if we have low hanging characters
|
||||||
|
if (strchr(lines[i], 'p') || strchr(lines[i], 'g') || strchr(lines[i], 'y') || strchr(lines[i], 'j')) {
|
||||||
|
background_yOffset = -1;
|
||||||
|
}
|
||||||
|
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
int yOffset = 3;
|
||||||
|
display->drawString(textX, lineY - yOffset, lines[i]);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||||
|
} else {
|
||||||
|
// Pop-up
|
||||||
|
LOG_WARN("x%u y%u %s", textX, lineY, lines[i]);
|
||||||
|
display->drawString(textX, lineY, lines[i]);
|
||||||
|
lineY += (effectiveLineHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Scroll Bar (Thicker, inside box, not over title) ===
|
||||||
|
if (totalLines > visibleTotalLines) {
|
||||||
|
const uint8_t scrollBarWidth = 5;
|
||||||
|
const uint8_t scrollPadding = 2;
|
||||||
|
|
||||||
|
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||||
|
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
|
||||||
|
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||||
|
|
||||||
|
float ratio = (float)visibleTotalLines / totalLines;
|
||||||
|
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||||
|
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||||
|
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||||
|
|
||||||
|
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||||
|
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the last text message we received
|
||||||
|
void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_MEDIUM);
|
||||||
|
|
||||||
|
char tempBuf[24];
|
||||||
|
snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code);
|
||||||
|
display->drawString(0 + x, 0 + y, tempBuf);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display->setFont(FONT_MEDIUM);
|
||||||
|
display->drawString(64 + x, y, "Updating");
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(),
|
||||||
|
"Please be patient and do not power off.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NotificationRenderer::isOverlayBannerShowing()
|
||||||
|
{
|
||||||
|
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
|
#endif
|
||||||
33
src/graphics/draw/NotificationRenderer.h
Normal file
33
src/graphics/draw/NotificationRenderer.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OLEDDisplay.h"
|
||||||
|
#include "OLEDDisplayUi.h"
|
||||||
|
#define MAX_LINES 5
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
class NotificationRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static char inEvent;
|
||||||
|
static int8_t curSelected;
|
||||||
|
static char alertBannerMessage[256];
|
||||||
|
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
||||||
|
static std::function<void(int)> alertBannerCallback;
|
||||||
|
|
||||||
|
static bool pauseBanner;
|
||||||
|
|
||||||
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||||
|
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||||
|
|
||||||
|
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
static bool isOverlayBannerShowing();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
1233
src/graphics/draw/UIRenderer.cpp
Normal file
1233
src/graphics/draw/UIRenderer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
88
src/graphics/draw/UIRenderer.h
Normal file
88
src/graphics/draw/UIRenderer.h
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "graphics/Screen.h"
|
||||||
|
#include "graphics/emotes.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#define HOURS_IN_MONTH 730
|
||||||
|
|
||||||
|
// Forward declarations for status types
|
||||||
|
namespace meshtastic
|
||||||
|
{
|
||||||
|
class PowerStatus;
|
||||||
|
class NodeStatus;
|
||||||
|
class GPSStatus;
|
||||||
|
} // namespace meshtastic
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Forward declarations
|
||||||
|
class Screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief UI utility drawing functions
|
||||||
|
*
|
||||||
|
* Contains utility functions for drawing common UI elements, overlays,
|
||||||
|
* battery indicators, and other shared graphical components.
|
||||||
|
*/
|
||||||
|
class UIRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Common UI elements
|
||||||
|
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
||||||
|
int node_offset = 0, bool show_total = true, String additional_words = "");
|
||||||
|
|
||||||
|
// GPS status functions
|
||||||
|
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
|
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
|
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
|
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
|
|
||||||
|
// Layout and utility functions
|
||||||
|
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
|
||||||
|
|
||||||
|
// Overlay and special screens
|
||||||
|
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||||
|
|
||||||
|
// Navigation bar overlay
|
||||||
|
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
|
||||||
|
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Icon and screen drawing functions
|
||||||
|
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Compass and location screen
|
||||||
|
static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
static NodeNum currentFavoriteNodeNum;
|
||||||
|
|
||||||
|
// OEM screens
|
||||||
|
#ifdef USERPREFS_OEM_TEXT
|
||||||
|
static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_EINK
|
||||||
|
/// Used on eink displays while in deep sleep
|
||||||
|
static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
/// Used on eink displays when screen updates are paused
|
||||||
|
static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
||||||
|
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
|
||||||
|
|
||||||
|
// Message filtering
|
||||||
|
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
|
||||||
|
// Check if the display can render a string (detect special chars; emoji)
|
||||||
|
static bool haveGlyphs(const char *str);
|
||||||
|
}; // namespace UIRenderer
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
225
src/graphics/emotes.cpp
Normal file
225
src/graphics/emotes.cpp
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
#include "emotes.h"
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
// Always define Emote list and count
|
||||||
|
const Emote emotes[] = {
|
||||||
|
#ifndef EXCLUDE_EMOJI
|
||||||
|
// --- Thumbs ---
|
||||||
|
{"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // 👍 Thumbs Up
|
||||||
|
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down
|
||||||
|
|
||||||
|
// --- Smileys (Multiple Unicode Aliases) ---
|
||||||
|
{"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes
|
||||||
|
{"\U0001F600", smiley, smiley_width, smiley_height}, // 😀 Grinning Face
|
||||||
|
{"\U0001F642", smiley, smiley_width, smiley_height}, // 🙂 Slightly Smiling Face
|
||||||
|
{"\U0001F609", smiley, smiley_width, smiley_height}, // 😉 Winking Face
|
||||||
|
{"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes
|
||||||
|
|
||||||
|
// --- Question/Alert ---
|
||||||
|
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
|
||||||
|
{"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark
|
||||||
|
|
||||||
|
// --- Laughing Faces ---
|
||||||
|
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
|
||||||
|
{"\U0001F923", haha, haha_width, haha_height}, // 🤣 Rolling on the Floor Laughing
|
||||||
|
{"\U0001F606", haha, haha_width, haha_height}, // 😆 Smiling with Open Mouth and Closed Eyes
|
||||||
|
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
|
||||||
|
{"\U0001F604", haha, haha_width, haha_height}, // 😄 Grinning Face with Smiling Eyes
|
||||||
|
|
||||||
|
// --- Gestures and People ---
|
||||||
|
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
|
||||||
|
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face
|
||||||
|
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones
|
||||||
|
|
||||||
|
// --- Weather ---
|
||||||
|
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
|
||||||
|
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
|
||||||
|
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
|
||||||
|
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
|
||||||
|
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
|
||||||
|
|
||||||
|
// --- Misc Faces ---
|
||||||
|
{"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns
|
||||||
|
|
||||||
|
// --- Hearts (Multiple Unicode Aliases) ---
|
||||||
|
{"\u2764\uFE0F", heart, heart_width, heart_height}, // ❤️ Red Heart
|
||||||
|
{"\U0001F9E1", heart, heart_width, heart_height}, // 🧡 Orange Heart
|
||||||
|
{"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation
|
||||||
|
{"\U00002764", heart, heart_width, heart_height}, // ❤ Red Heart (legacy)
|
||||||
|
{"\U0001F495", heart, heart_width, heart_height}, // 💕 Two Hearts
|
||||||
|
{"\U0001F496", heart, heart_width, heart_height}, // 💖 Sparkling Heart
|
||||||
|
{"\U0001F497", heart, heart_width, heart_height}, // 💗 Growing Heart
|
||||||
|
{"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow
|
||||||
|
|
||||||
|
// --- Objects ---
|
||||||
|
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
|
||||||
|
{"\U0001F514", bell_icon, bell_icon_width, bell_icon_height} // 🔔 Bell
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
const int numEmotes = sizeof(emotes) / sizeof(emotes[0]);
|
||||||
|
|
||||||
|
#ifndef EXCLUDE_EMOJI
|
||||||
|
const unsigned char thumbup[] PROGMEM = {
|
||||||
|
0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00,
|
||||||
|
0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00,
|
||||||
|
0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01,
|
||||||
|
0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00,
|
||||||
|
0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char thumbdown[] PROGMEM = {
|
||||||
|
0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00,
|
||||||
|
0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01,
|
||||||
|
0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00,
|
||||||
|
0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00,
|
||||||
|
0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char smiley[] PROGMEM = {
|
||||||
|
0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02,
|
||||||
|
0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10,
|
||||||
|
0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
|
||||||
|
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20,
|
||||||
|
0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04,
|
||||||
|
0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00};
|
||||||
|
|
||||||
|
const unsigned char question[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00,
|
||||||
|
0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00,
|
||||||
|
0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00,
|
||||||
|
0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char bang[] PROGMEM = {
|
||||||
|
0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F,
|
||||||
|
0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F,
|
||||||
|
0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F,
|
||||||
|
0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F,
|
||||||
|
0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char haha[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00,
|
||||||
|
0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00,
|
||||||
|
0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F,
|
||||||
|
0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01,
|
||||||
|
0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00,
|
||||||
|
0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char wave_icon[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00,
|
||||||
|
0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02,
|
||||||
|
0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00,
|
||||||
|
0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00,
|
||||||
|
0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00,
|
||||||
|
0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char cowboy[] PROGMEM = {
|
||||||
|
0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F,
|
||||||
|
0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F,
|
||||||
|
0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00,
|
||||||
|
0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08,
|
||||||
|
0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03,
|
||||||
|
0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char deadmau5[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00,
|
||||||
|
0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00,
|
||||||
|
0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07,
|
||||||
|
0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00,
|
||||||
|
0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC,
|
||||||
|
0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00,
|
||||||
|
0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF,
|
||||||
|
0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
|
||||||
|
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char sun[] PROGMEM = {
|
||||||
|
0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03,
|
||||||
|
0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00,
|
||||||
|
0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E,
|
||||||
|
0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00,
|
||||||
|
0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03,
|
||||||
|
0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char rain[] PROGMEM = {
|
||||||
|
0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00,
|
||||||
|
0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20,
|
||||||
|
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00,
|
||||||
|
0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00,
|
||||||
|
0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C,
|
||||||
|
0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char cloud[] PROGMEM = {
|
||||||
|
0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00,
|
||||||
|
0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01,
|
||||||
|
0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10,
|
||||||
|
0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
|
||||||
|
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10,
|
||||||
|
0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char fog[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01,
|
||||||
|
0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00,
|
||||||
|
0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char devil[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E,
|
||||||
|
0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06,
|
||||||
|
0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C,
|
||||||
|
0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C,
|
||||||
|
0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01,
|
||||||
|
0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char heart[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18,
|
||||||
|
0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37,
|
||||||
|
0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F,
|
||||||
|
0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03,
|
||||||
|
0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00,
|
||||||
|
0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char poo[] PROGMEM = {
|
||||||
|
0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00,
|
||||||
|
0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00,
|
||||||
|
0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00,
|
||||||
|
0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04,
|
||||||
|
0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20,
|
||||||
|
0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F,
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char bell_icon[] PROGMEM = {
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000,
|
||||||
|
0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000,
|
||||||
|
0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000,
|
||||||
|
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
|
||||||
|
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
|
||||||
|
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
|
||||||
|
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000,
|
||||||
|
0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011,
|
||||||
|
0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100,
|
||||||
|
0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000,
|
||||||
|
0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
86
src/graphics/emotes.h
Normal file
86
src/graphics/emotes.h
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
// === Emote List ===
|
||||||
|
struct Emote {
|
||||||
|
const char *label;
|
||||||
|
const unsigned char *bitmap;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const Emote emotes[/* numEmotes */];
|
||||||
|
extern const int numEmotes;
|
||||||
|
|
||||||
|
#ifndef EXCLUDE_EMOJI
|
||||||
|
// === Emote Bitmaps ===
|
||||||
|
#define thumbs_height 25
|
||||||
|
#define thumbs_width 25
|
||||||
|
extern const unsigned char thumbup[] PROGMEM;
|
||||||
|
extern const unsigned char thumbdown[] PROGMEM;
|
||||||
|
|
||||||
|
#define smiley_height 30
|
||||||
|
#define smiley_width 30
|
||||||
|
extern const unsigned char smiley[] PROGMEM;
|
||||||
|
|
||||||
|
#define question_height 25
|
||||||
|
#define question_width 25
|
||||||
|
extern const unsigned char question[] PROGMEM;
|
||||||
|
|
||||||
|
#define bang_height 30
|
||||||
|
#define bang_width 30
|
||||||
|
extern const unsigned char bang[] PROGMEM;
|
||||||
|
|
||||||
|
#define haha_height 30
|
||||||
|
#define haha_width 30
|
||||||
|
extern const unsigned char haha[] PROGMEM;
|
||||||
|
|
||||||
|
#define wave_icon_height 30
|
||||||
|
#define wave_icon_width 30
|
||||||
|
extern const unsigned char wave_icon[] PROGMEM;
|
||||||
|
|
||||||
|
#define cowboy_height 30
|
||||||
|
#define cowboy_width 30
|
||||||
|
extern const unsigned char cowboy[] PROGMEM;
|
||||||
|
|
||||||
|
#define deadmau5_height 30
|
||||||
|
#define deadmau5_width 60
|
||||||
|
extern const unsigned char deadmau5[] PROGMEM;
|
||||||
|
|
||||||
|
#define sun_height 30
|
||||||
|
#define sun_width 30
|
||||||
|
extern const unsigned char sun[] PROGMEM;
|
||||||
|
|
||||||
|
#define rain_height 30
|
||||||
|
#define rain_width 30
|
||||||
|
extern const unsigned char rain[] PROGMEM;
|
||||||
|
|
||||||
|
#define cloud_height 30
|
||||||
|
#define cloud_width 30
|
||||||
|
extern const unsigned char cloud[] PROGMEM;
|
||||||
|
|
||||||
|
#define fog_height 25
|
||||||
|
#define fog_width 25
|
||||||
|
extern const unsigned char fog[] PROGMEM;
|
||||||
|
|
||||||
|
#define devil_height 30
|
||||||
|
#define devil_width 30
|
||||||
|
extern const unsigned char devil[] PROGMEM;
|
||||||
|
|
||||||
|
#define heart_height 30
|
||||||
|
#define heart_width 30
|
||||||
|
extern const unsigned char heart[] PROGMEM;
|
||||||
|
|
||||||
|
#define poo_height 30
|
||||||
|
#define poo_width 30
|
||||||
|
extern const unsigned char poo[] PROGMEM;
|
||||||
|
|
||||||
|
#define bell_icon_width 30
|
||||||
|
#define bell_icon_height 30
|
||||||
|
extern const unsigned char bell_icon[] PROGMEM;
|
||||||
|
#endif // EXCLUDE_EMOJI
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
@@ -6,7 +6,12 @@ const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0
|
|||||||
0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54,
|
0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54,
|
||||||
0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E};
|
0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E};
|
||||||
|
|
||||||
const uint8_t imgSatellite[] PROGMEM = {0x70, 0x71, 0x22, 0xFA, 0xFA, 0x22, 0x71, 0x70};
|
#define imgSatellite_width 8
|
||||||
|
#define imgSatellite_height 8
|
||||||
|
const uint8_t imgSatellite[] PROGMEM = {
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, 0b11011011, 0b00011000,
|
||||||
|
};
|
||||||
|
|
||||||
const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C};
|
const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C};
|
||||||
const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08,
|
const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08,
|
||||||
0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22};
|
0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22};
|
||||||
@@ -14,11 +19,9 @@ const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3
|
|||||||
const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF};
|
const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF};
|
||||||
const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF};
|
const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF};
|
||||||
|
|
||||||
#if defined(DISPLAY_CLOCK_FRAME)
|
|
||||||
const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0xe3, 0x1f,
|
const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0xe3, 0x1f,
|
||||||
0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
|
0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
|
||||||
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
|
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
|
||||||
#endif
|
|
||||||
|
|
||||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
|
||||||
@@ -37,181 +40,249 @@ const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1,
|
|||||||
const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, 0x15, 0x85, 0xf5};
|
const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, 0x15, 0x85, 0xf5};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef EXCLUDE_EMOJI
|
// === Horizontal battery ===
|
||||||
#define thumbs_height 25
|
// Basic battery design and all related pieces
|
||||||
#define thumbs_width 25
|
const unsigned char batteryBitmap_h_bottom[] PROGMEM = {
|
||||||
static unsigned char thumbup[] PROGMEM = {
|
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
|
||||||
0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00,
|
0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||||
0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00,
|
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000};
|
||||||
0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01,
|
|
||||||
0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00,
|
const unsigned char batteryBitmap_h_top[] PROGMEM = {
|
||||||
0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00,
|
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
|
||||||
|
0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
|
||||||
|
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
|
||||||
|
|
||||||
|
// Lightning Bolt
|
||||||
|
const unsigned char lightning_bolt_h[] PROGMEM = {
|
||||||
|
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100,
|
||||||
|
0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
|
||||||
|
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000};
|
||||||
|
|
||||||
|
// === Vertical battery ===
|
||||||
|
// Basic battery design and all related pieces
|
||||||
|
const unsigned char batteryBitmap_v[] PROGMEM = {0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110};
|
||||||
|
// This is the left and right bars for the fill in
|
||||||
|
const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010, 0b10000010};
|
||||||
|
// Lightning Bolt
|
||||||
|
const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100};
|
||||||
|
|
||||||
|
#define mail_width 10
|
||||||
|
#define mail_height 7
|
||||||
|
static const unsigned char mail[] PROGMEM = {
|
||||||
|
0b11111111, 0b00, // Top line
|
||||||
|
0b10000001, 0b00, // Edges
|
||||||
|
0b11000011, 0b00, // Diagonals start
|
||||||
|
0b10100101, 0b00, // Inner M part
|
||||||
|
0b10011001, 0b00, // Inner M part
|
||||||
|
0b10000001, 0b00, // Edges
|
||||||
|
0b11111111, 0b00 // Bottom line
|
||||||
};
|
};
|
||||||
|
|
||||||
static unsigned char thumbdown[] PROGMEM = {
|
// 📬 Mail / Message
|
||||||
0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00,
|
const uint8_t icon_mail[] PROGMEM = {
|
||||||
0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01,
|
0b11111111, // ████████ top border
|
||||||
0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00,
|
0b10000001, // █ █ sides
|
||||||
0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00,
|
0b11000011, // ██ ██ diagonal
|
||||||
0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00,
|
0b10100101, // █ █ █ █ inner M
|
||||||
|
0b10011001, // █ ██ █ inner M
|
||||||
|
0b10000001, // █ █ sides
|
||||||
|
0b10000001, // █ █ sides
|
||||||
|
0b11111111 // ████████ bottom
|
||||||
};
|
};
|
||||||
|
|
||||||
#define smiley_height 30
|
// 📍 GPS Screen / Location Pin
|
||||||
#define smiley_width 30
|
const unsigned char icon_compass[] PROGMEM = {
|
||||||
static unsigned char smiley[] PROGMEM = {
|
0x3C, // Row 0: ..####..
|
||||||
0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02,
|
0x52, // Row 1: .#..#.#.
|
||||||
0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10,
|
0x91, // Row 2: #...#..#
|
||||||
0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
|
0x91, // Row 3: #...#..#
|
||||||
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20,
|
0x91, // Row 4: #...#..#
|
||||||
0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04,
|
0x81, // Row 5: #......#
|
||||||
0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00};
|
0x42, // Row 6: .#....#.
|
||||||
|
0x3C // Row 7: ..####..
|
||||||
#define question_height 25
|
|
||||||
#define question_width 25
|
|
||||||
static unsigned char question[] PROGMEM = {
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00,
|
|
||||||
0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00,
|
|
||||||
0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00,
|
|
||||||
0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#define bang_height 30
|
const uint8_t icon_radio[] PROGMEM = {
|
||||||
#define bang_width 30
|
0x0F, // Row 0: ####....
|
||||||
static unsigned char bang[] PROGMEM = {
|
0x10, // Row 1: ....#...
|
||||||
0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F,
|
0x27, // Row 2: ###..#..
|
||||||
0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F,
|
0x48, // Row 3: ...#..#.
|
||||||
0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F,
|
0x93, // Row 4: ##..#..#
|
||||||
0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F,
|
0xA4, // Row 5: ..#..#.#
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F,
|
0xA8, // Row 6: ...#.#.#
|
||||||
0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07,
|
0xA9 // Row 7: #..#.#.#
|
||||||
};
|
};
|
||||||
|
|
||||||
#define haha_height 30
|
// 🪙 Memory Icon
|
||||||
#define haha_width 30
|
const uint8_t icon_memory[] PROGMEM = {
|
||||||
static unsigned char haha[] PROGMEM = {
|
0x24, // Row 0: ..#..#..
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00,
|
0x3C, // Row 1: ..####..
|
||||||
0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00,
|
0xC3, // Row 2: ##....##
|
||||||
0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F,
|
0x5A, // Row 3: .#.##.#.
|
||||||
0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01,
|
0x5A, // Row 4: .#.##.#.
|
||||||
0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00,
|
0xC3, // Row 5: ##....##
|
||||||
0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x3C, // Row 6: ..####..
|
||||||
|
0x24 // Row 7: ..#..#..
|
||||||
};
|
};
|
||||||
|
|
||||||
#define wave_icon_height 30
|
// 🌐 Wi-Fi
|
||||||
#define wave_icon_width 30
|
const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110,
|
||||||
static unsigned char wave_icon[] PROGMEM = {
|
0b11011011, 0b00011000, 0b00011000, 0b00000000};
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00,
|
|
||||||
0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02,
|
const uint8_t icon_nodes[] PROGMEM = {
|
||||||
0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00,
|
0xF9, // Row 0 #..#######
|
||||||
0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00,
|
0x00, // Row 1
|
||||||
0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00,
|
0xF9, // Row 2 #..#######
|
||||||
0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, // Row 3
|
||||||
|
0xF9, // Row 4 #..#######
|
||||||
|
0x00, // Row 5
|
||||||
|
0xF9, // Row 6 #..#######
|
||||||
|
0x00 // Row 7
|
||||||
};
|
};
|
||||||
|
|
||||||
#define cowboy_height 30
|
// ➤ Chevron Triangle Arrow Icon (8x8)
|
||||||
#define cowboy_width 30
|
const uint8_t icon_list[] PROGMEM = {
|
||||||
static unsigned char cowboy[] PROGMEM = {
|
0x10, // Row 0: ...#....
|
||||||
0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F,
|
0x10, // Row 1: ...#....
|
||||||
0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F,
|
0x38, // Row 2: ..###...
|
||||||
0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00,
|
0x38, // Row 3: ..###...
|
||||||
0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08,
|
0x7C, // Row 4: .#####..
|
||||||
0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03,
|
0x6C, // Row 5: .##.##..
|
||||||
0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00,
|
0xC6, // Row 6: ##...##.
|
||||||
|
0x82 // Row 7: #.....#.
|
||||||
};
|
};
|
||||||
|
|
||||||
#define deadmau5_height 30
|
// 📶 Signal Bars Icon (left to right, small to large with spacing)
|
||||||
#define deadmau5_width 60
|
const uint8_t icon_signal[] PROGMEM = {
|
||||||
static unsigned char deadmau5[] PROGMEM = {
|
0b00000000, // ░░░░░░░
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00,
|
0b10000000, // ░░░░░░░
|
||||||
0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00,
|
0b10100000, // ░░░░█░█
|
||||||
0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07,
|
0b10100000, // ░░░░█░█
|
||||||
0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00,
|
0b10101000, // ░░█░█░█
|
||||||
0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC,
|
0b10101000, // ░░█░█░█
|
||||||
0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00,
|
0b10101010, // █░█░█░█
|
||||||
0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF,
|
0b11111111 // ███████
|
||||||
0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
|
|
||||||
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#define sun_width 30
|
// ↔️ Distance / Measurement Icon (double-ended arrow)
|
||||||
#define sun_height 30
|
const uint8_t icon_distance[] PROGMEM = {
|
||||||
static unsigned char sun[] PROGMEM = {
|
0b00000000, // ░░░░░░░░
|
||||||
0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03,
|
0b10000001, // █░░░░░█ arrowheads
|
||||||
0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00,
|
0b01000010, // ░█░░░█░
|
||||||
0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E,
|
0b00100100, // ░░█░█░░
|
||||||
0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00,
|
0b00011000, // ░░░██░░ center
|
||||||
0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03,
|
0b00100100, // ░░█░█░░
|
||||||
0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00,
|
0b01000010, // ░█░░░█░
|
||||||
|
0b10000001 // █░░░░░█
|
||||||
};
|
};
|
||||||
|
|
||||||
#define rain_width 30
|
// ⚠️ Error / Fault
|
||||||
#define rain_height 30
|
const uint8_t icon_error[] PROGMEM = {
|
||||||
static unsigned char rain[] PROGMEM = {
|
0b00011000, // ░░░██░░░
|
||||||
0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00,
|
0b00011000, // ░░░██░░░
|
||||||
0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20,
|
0b00011000, // ░░░██░░░
|
||||||
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00,
|
0b00011000, // ░░░██░░░
|
||||||
0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00,
|
0b00000000, // ░░░░░░░░
|
||||||
0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C,
|
0b00011000, // ░░░██░░░
|
||||||
0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00,
|
0b00000000, // ░░░░░░░░
|
||||||
|
0b00000000 // ░░░░░░░░
|
||||||
};
|
};
|
||||||
|
|
||||||
#define cloud_height 30
|
// 🏠 Optimized Home Icon (8x8)
|
||||||
#define cloud_width 30
|
const uint8_t icon_home[] PROGMEM = {
|
||||||
static unsigned char cloud[] PROGMEM = {
|
0b00011000, // ██
|
||||||
0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00,
|
0b00111100, // ████
|
||||||
0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01,
|
0b01111110, // ██████
|
||||||
0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10,
|
0b11111111, // ███████
|
||||||
0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
|
0b11000011, // ██ ██
|
||||||
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10,
|
0b11011011, // ██ ██ ██
|
||||||
0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03,
|
0b11011011, // ██ ██ ██
|
||||||
|
0b11111111 // ███████
|
||||||
};
|
};
|
||||||
|
|
||||||
#define fog_height 25
|
// 🔧 Generic module (gear-like shape)
|
||||||
#define fog_width 25
|
const uint8_t icon_module[] PROGMEM = {
|
||||||
static unsigned char fog[] PROGMEM = {
|
0b00011000, // ░░░██░░░
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0b00111100, // ░░████░░
|
||||||
0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01,
|
0b01111110, // ░██████░
|
||||||
0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00,
|
0b11011011, // ██░██░██
|
||||||
0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00,
|
0b11011011, // ██░██░██
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0b01111110, // ░██████░
|
||||||
|
0b00111100, // ░░████░░
|
||||||
|
0b00011000 // ░░░██░░░
|
||||||
};
|
};
|
||||||
|
|
||||||
#define devil_height 30
|
#define mute_symbol_width 8
|
||||||
#define devil_width 30
|
#define mute_symbol_height 8
|
||||||
static unsigned char devil[] PROGMEM = {
|
const uint8_t mute_symbol[] PROGMEM = {
|
||||||
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E,
|
0b00011001, // █
|
||||||
0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06,
|
0b00100110, // █
|
||||||
0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C,
|
0b00100100, // ████
|
||||||
0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C,
|
0b01001010, // █ █ █
|
||||||
0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01,
|
0b01010010, // █ █ █
|
||||||
0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0b01100010, // ████████
|
||||||
|
0b11111111, // █ █
|
||||||
|
0b10011000, // █
|
||||||
};
|
};
|
||||||
|
|
||||||
#define heart_height 30
|
#define mute_symbol_big_width 16
|
||||||
#define heart_width 30
|
#define mute_symbol_big_height 16
|
||||||
static unsigned char heart[] PROGMEM = {
|
const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000,
|
||||||
0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18,
|
0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000,
|
||||||
0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37,
|
0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000,
|
||||||
0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F,
|
0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010,
|
||||||
0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03,
|
0b10000000, 0b01000001, 0b00000000, 0b10000000};
|
||||||
0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00,
|
|
||||||
0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00,
|
// Bell icon for Alert Message
|
||||||
|
#define bell_alert_width 8
|
||||||
|
#define bell_alert_height 8
|
||||||
|
const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010,
|
||||||
|
0b01000010, 0b01000010, 0b11111111, 0b00011000};
|
||||||
|
|
||||||
|
#define key_symbol_width 8
|
||||||
|
#define key_symbol_height 8
|
||||||
|
const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001,
|
||||||
|
0b10101001, 0b10000110, 0b00000000, 0b00000000};
|
||||||
|
|
||||||
|
#define placeholder_width 8
|
||||||
|
#define placeholder_height 8
|
||||||
|
const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111,
|
||||||
|
0b11111111, 0b11111111, 0b11111111, 0b11111111};
|
||||||
|
|
||||||
|
#define icon_node_width 8
|
||||||
|
#define icon_node_height 8
|
||||||
|
static const uint8_t icon_node[] PROGMEM = {
|
||||||
|
0x10, // #
|
||||||
|
0x10, // # ← antenna
|
||||||
|
0x10, // #
|
||||||
|
0xFE, // ####### ← device top
|
||||||
|
0x82, // # #
|
||||||
|
0xAA, // # # # # ← body with pattern
|
||||||
|
0x92, // # # #
|
||||||
|
0xFE // ####### ← device base
|
||||||
};
|
};
|
||||||
|
|
||||||
#define poo_width 30
|
#define bluetoothdisabled_width 8
|
||||||
#define poo_height 30
|
#define bluetoothdisabled_height 8
|
||||||
static unsigned char poo[] PROGMEM = {
|
const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100,
|
||||||
0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00,
|
0b01001100, 0b00000000, 0b00000000, 0b00000000};
|
||||||
0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00,
|
|
||||||
0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00,
|
#define smallbulletpoint_width 8
|
||||||
0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04,
|
#define smallbulletpoint_height 8
|
||||||
0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20,
|
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
|
||||||
0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F,
|
0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||||
};
|
|
||||||
#endif
|
// Digital Clock
|
||||||
|
#define digital_icon_clock_width 8
|
||||||
|
#define digital_icon_clock_height 8
|
||||||
|
const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
|
||||||
|
0b10010001, 0b10000001, 0b01000010, 0b00111100};
|
||||||
|
// Analog Clock
|
||||||
|
#define analog_icon_clock_width 8
|
||||||
|
#define analog_icon_clock_height 8
|
||||||
|
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
|
||||||
|
0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
||||||
|
|
||||||
#include "img/icon.xbm"
|
#include "img/icon.xbm"
|
||||||
|
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
|
||||||
@@ -5,7 +5,7 @@ E-Ink display driver
|
|||||||
- Manufacturer: DKE
|
- Manufacturer: DKE
|
||||||
- Size: 2.13 inch
|
- Size: 2.13 inch
|
||||||
- Resolution: 122px x 250px
|
- Resolution: 122px x 250px
|
||||||
- Flex connector marking: FPC-7528B
|
- Flex connector marking (not a unique identifier): FPC-7528B
|
||||||
|
|
||||||
Note: this is from an older generation of DKE panels, which still used Solomon Systech controller ICs.
|
Note: this is from an older generation of DKE panels, which still used Solomon Systech controller ICs.
|
||||||
DKE's website suggests that the latest DEPG0213BN displays may use Fitipower controllers instead.
|
DKE's website suggests that the latest DEPG0213BN displays may use Fitipower controllers instead.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ E-Ink display driver
|
|||||||
- Manufacturer: DKE
|
- Manufacturer: DKE
|
||||||
- Size: 2.9 inch
|
- Size: 2.9 inch
|
||||||
- Resolution: 128px x 296px
|
- Resolution: 128px x 296px
|
||||||
- Flex connector marking: FPC-7519 rev.b
|
- Flex connector marking (not a unique identifier): FPC-7519 rev.b
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ void GDEY0154D67::configScanning()
|
|||||||
{
|
{
|
||||||
// "Driver output control"
|
// "Driver output control"
|
||||||
sendCommand(0x01);
|
sendCommand(0x01);
|
||||||
sendData(0xC7);
|
sendData(0xC7); // Scan until gate 199 (200px vertical res.)
|
||||||
sendData(0x00);
|
sendData(0x00);
|
||||||
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
|
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||||
@@ -52,10 +49,10 @@ void GDEY0154D67::detachFromUpdate()
|
|||||||
{
|
{
|
||||||
switch (updateType) {
|
switch (updateType) {
|
||||||
case FAST:
|
case FAST:
|
||||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
return beginPolling(50, 300); // At least 300ms for fast refresh
|
||||||
case FULL:
|
case FULL:
|
||||||
default:
|
default:
|
||||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
return beginPolling(100, 1500); // At least 1.5 seconds for full refresh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
@@ -5,7 +5,7 @@ E-Ink display driver
|
|||||||
- Manufacturer: Goodisplay
|
- Manufacturer: Goodisplay
|
||||||
- Size: 1.54 inch
|
- Size: 1.54 inch
|
||||||
- Resolution: 200px x 200px
|
- Resolution: 200px x 200px
|
||||||
- Flex connector marking: FPC-B001
|
- Flex connector marking (not a unique identifier): FPC-B001
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -31,9 +31,9 @@ class GDEY0154D67 : public SSD16XX
|
|||||||
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void configScanning() override;
|
void configScanning() override;
|
||||||
virtual void configWaveform() override;
|
void configWaveform() override;
|
||||||
virtual void configUpdateSequence() override;
|
void configUpdateSequence() override;
|
||||||
void detachFromUpdate() override;
|
void detachFromUpdate() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ void GDEY0213B74::configScanning()
|
|||||||
sendData(0xF9);
|
sendData(0xF9);
|
||||||
sendData(0x00);
|
sendData(0x00);
|
||||||
sendData(0x00);
|
sendData(0x00);
|
||||||
|
|
||||||
// To-do: delete this method?
|
|
||||||
// Values set here might be redundant: F9, 00, 00 seems to be default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ E-Ink display driver
|
|||||||
- Manufacturer: Goodisplay
|
- Manufacturer: Goodisplay
|
||||||
- Size: 2.13 inch
|
- Size: 2.13 inch
|
||||||
- Resolution: 250px x 122px
|
- Resolution: 250px x 122px
|
||||||
- Flex connector marking: FPC-A002
|
- Flex connector marking (not a unique identifier):
|
||||||
|
- FPC-A002
|
||||||
|
- FPC-A005 20.06.15 TRX
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
61
src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp
Normal file
61
src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#include "./HINK_E0213A289.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Map the display controller IC's output to the connected panel
|
||||||
|
void HINK_E0213A289::configScanning()
|
||||||
|
{
|
||||||
|
// "Driver output control"
|
||||||
|
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||||
|
sendCommand(0x01);
|
||||||
|
sendData(0xF9); // Maximum gate # (249, bits 0-7)
|
||||||
|
sendData(0x00); // Maximum gate # (bit 8)
|
||||||
|
sendData(0x00); // (Do not invert scanning order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 HINK_E0213A289::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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 HINK_E0213A289::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 HINK_E0213A289::detachFromUpdate()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
44
src/graphics/niche/Drivers/EInk/HINK_E0213A289.h
Normal file
44
src/graphics/niche/Drivers/EInk/HINK_E0213A289.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- HINK_E0213A289
|
||||||
|
- Manufacturer: Holitech
|
||||||
|
- Size: 2.13 inch
|
||||||
|
- Resolution: 122px x 250px
|
||||||
|
- Flex connector label (not a unique identifier): FPC-7528B
|
||||||
|
|
||||||
|
Note: as of Feb. 2025, these panels are used for "WeActStudio 2.13in B&W" display modules
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./SSD16XX.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
class HINK_E0213A289 : public SSD16XX
|
||||||
|
{
|
||||||
|
// Display properties
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t width = 122;
|
||||||
|
static constexpr uint32_t height = 250;
|
||||||
|
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||||
|
|
||||||
|
public:
|
||||||
|
HINK_E0213A289() : SSD16XX(width, height, supported, 1) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void configScanning() override;
|
||||||
|
void configWaveform() override;
|
||||||
|
void configUpdateSequence() override;
|
||||||
|
void detachFromUpdate() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
@@ -5,7 +5,7 @@ E-Ink display driver
|
|||||||
- Manufacturer: Holitech
|
- Manufacturer: Holitech
|
||||||
- Size: 4.2 inch
|
- Size: 4.2 inch
|
||||||
- Resolution: 400px x 300px
|
- Resolution: 400px x 300px
|
||||||
- Flex connector marking: HINK-E042A07-FPC-A1
|
- Flex connector marking (not a unique identifier): HINK-E042A07-FPC-A1
|
||||||
- Silver sticker with QR code, marked: HE042A87
|
- Silver sticker with QR code, marked: HE042A87
|
||||||
|
|
||||||
Note: as of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules
|
Note: as of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ E-Ink display driver
|
|||||||
- Manufacturer: WISEVAST
|
- Manufacturer: WISEVAST
|
||||||
- Size: 2.13 inch
|
- Size: 2.13 inch
|
||||||
- Resolution: 122px x 255px
|
- Resolution: 122px x 255px
|
||||||
- Flex connector marking: Soldering connector, no connector is needed
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ E-Ink display driver
|
|||||||
- Manufacturer: Wisevast
|
- Manufacturer: Wisevast
|
||||||
- Size: 2.13 inch
|
- Size: 2.13 inch
|
||||||
- Resolution: 122px x 250px
|
- Resolution: 122px x 250px
|
||||||
- Flex connector marking: HINK-E0213A162-FPC-A0 (Hidden, printed on back-side)
|
- Flex connector marking (not a unique identifier): HINK-E0213A162-FPC-A0 (Hidden, printed on back-side)
|
||||||
|
|
||||||
Note: this display uses an uncommon controller IC, Fitipower JD79656.
|
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.
|
It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays.
|
||||||
|
|||||||
59
src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp
Normal file
59
src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#include "./ZJY128296_029EAAMFGN.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
// Map the display controller IC's output to the connected panel
|
||||||
|
void ZJY128296_029EAAMFGN::configScanning()
|
||||||
|
{
|
||||||
|
// "Driver output control"
|
||||||
|
// Scan gates from 0 to 295 (vertical resolution 296px)
|
||||||
|
sendCommand(0x01);
|
||||||
|
sendData(0x27); // Number of gates (295, bits 0-7)
|
||||||
|
sendData(0x01); // Number of gates (295, bit 8)
|
||||||
|
sendData(0x00); // (Do not invert scanning order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ZJY128296_029EAAMFGN::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 ZJY128296_029EAAMFGN::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 ZJY128296_029EAAMFGN::detachFromUpdate()
|
||||||
|
{
|
||||||
|
switch (updateType) {
|
||||||
|
case FAST:
|
||||||
|
return beginPolling(50, 300); // At least 300ms for fast refresh
|
||||||
|
case FULL:
|
||||||
|
default:
|
||||||
|
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
44
src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h
Normal file
44
src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- ZJY128296-029EAAMFGN
|
||||||
|
- Manufacturer: Zhongjingyuan
|
||||||
|
- Size: 2.9 inch
|
||||||
|
- Resolution: 128px x 296px
|
||||||
|
- Flex connector label (not a unique identifier): FPC-A005 20.06.15 TRX
|
||||||
|
|
||||||
|
Note: as of Feb. 2025, these panels are used for "WeActStudio 2.9in B&W" display modules
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./SSD16XX.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
class ZJY128296_029EAAMFGN : 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:
|
||||||
|
ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void configScanning() override;
|
||||||
|
void configWaveform() override;
|
||||||
|
void configUpdateSequence() override;
|
||||||
|
void detachFromUpdate() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
32
src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h
Normal file
32
src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver
|
||||||
|
- ZJY200200-0154DAAMFGN
|
||||||
|
- Manufacturer: Zhongjingyuan
|
||||||
|
- Size: 1.54 inch
|
||||||
|
- Resolution: 200px x 200px
|
||||||
|
- Flex connector marking: FPC-B001
|
||||||
|
|
||||||
|
Note: as of Feb. 2025, these panels are used for "WeActStudio 1.54in B&W" display modules
|
||||||
|
|
||||||
|
This *is* a distinct panel, however the driver is currently identical to GDEY0154D67
|
||||||
|
We recognize it as separate now, to avoid breaking any custom builds if the drivers do need to diverge in future.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./GDEY0154D67.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
|
||||||
|
typedef GDEY0154D67 ZJY200200_0154DAAMFGN;
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
@@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD
|
|||||||
enum MenuAction {
|
enum MenuAction {
|
||||||
NO_ACTION,
|
NO_ACTION,
|
||||||
SEND_PING,
|
SEND_PING,
|
||||||
|
STORE_CANNEDMESSAGE_SELECTION,
|
||||||
|
SEND_CANNEDMESSAGE,
|
||||||
SHUTDOWN,
|
SHUTDOWN,
|
||||||
NEXT_TILE,
|
NEXT_TILE,
|
||||||
TOGGLE_BACKLIGHT,
|
TOGGLE_BACKLIGHT,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
|
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
|
#include "Router.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -31,6 +32,12 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
|||||||
if (settings->optionalMenuItems.backlight) {
|
if (settings->optionalMenuItems.backlight) {
|
||||||
backlight = Drivers::LatchingBacklight::getInstance();
|
backlight = Drivers::LatchingBacklight::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the Canned Message store
|
||||||
|
// This is a shared nicheGraphics component
|
||||||
|
// - handles loading & parsing the canned messages
|
||||||
|
// - handles setting / getting of canned messages via apps (Client API Admin Messages)
|
||||||
|
cm.store = CannedMessageStore::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onForeground()
|
void InkHUD::MenuApplet::onForeground()
|
||||||
@@ -65,6 +72,10 @@ void InkHUD::MenuApplet::onForeground()
|
|||||||
|
|
||||||
void InkHUD::MenuApplet::onBackground()
|
void InkHUD::MenuApplet::onBackground()
|
||||||
{
|
{
|
||||||
|
// Discard any data we generated while selecting a canned message
|
||||||
|
// Frees heap mem
|
||||||
|
freeCannedMessageResources();
|
||||||
|
|
||||||
// If device has a backlight which isn't controlled by aux button:
|
// If device has a backlight which isn't controlled by aux button:
|
||||||
// Item in options submenu allows keeping backlight on after menu is closed
|
// 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 this item is deselected we will turn backlight off again, now that menu is closing
|
||||||
@@ -153,6 +164,16 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
|||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case STORE_CANNEDMESSAGE_SELECTION:
|
||||||
|
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEND_CANNEDMESSAGE:
|
||||||
|
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
||||||
|
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
|
||||||
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
|
||||||
|
break;
|
||||||
|
|
||||||
case ROTATE:
|
case ROTATE:
|
||||||
inkhud->rotate();
|
inkhud->rotate();
|
||||||
break;
|
break;
|
||||||
@@ -260,9 +281,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SEND:
|
case SEND:
|
||||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
populateSendPage();
|
||||||
// Todo: canned messages
|
break;
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
|
||||||
|
case CANNEDMESSAGE_RECIPIENT:
|
||||||
|
populateRecipientPage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OPTIONS:
|
case OPTIONS:
|
||||||
@@ -497,6 +520,8 @@ void InkHUD::MenuApplet::populateAutoshowPage()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create MenuItem entries to select our definition of "Recent"
|
||||||
|
// Controls how long data will remain in any "Recents" flavored applets
|
||||||
void InkHUD::MenuApplet::populateRecentsPage()
|
void InkHUD::MenuApplet::populateRecentsPage()
|
||||||
{
|
{
|
||||||
// How many values are shown for use to choose from
|
// How many values are shown for use to choose from
|
||||||
@@ -510,6 +535,112 @@ void InkHUD::MenuApplet::populateRecentsPage()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MenuItem entries for the "send" page
|
||||||
|
// Dynamically creates menu items based on available canned messages
|
||||||
|
void InkHUD::MenuApplet::populateSendPage()
|
||||||
|
{
|
||||||
|
// Position / NodeInfo packet
|
||||||
|
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||||
|
|
||||||
|
// One menu item for each canned message
|
||||||
|
uint8_t count = cm.store->size();
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
// Gather the information for this item
|
||||||
|
CannedMessages::MessageItem messageItem;
|
||||||
|
messageItem.rawText = cm.store->at(i);
|
||||||
|
messageItem.label = parse(messageItem.rawText);
|
||||||
|
|
||||||
|
// Store the item (until the menu closes)
|
||||||
|
cm.messageItems.push_back(messageItem);
|
||||||
|
|
||||||
|
// Create a menu item
|
||||||
|
const char *itemText = cm.messageItems.back().label.c_str();
|
||||||
|
items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically create MenuItem entries for possible canned message destinations
|
||||||
|
// All available channels are shown
|
||||||
|
// Favorite nodes are shown, provided we don't have an *excessive* amount
|
||||||
|
void InkHUD::MenuApplet::populateRecipientPage()
|
||||||
|
{
|
||||||
|
// Create recipient data (and menu items) for any channels
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||||
|
// Get the channel, and check if it's enabled
|
||||||
|
meshtastic_Channel &channel = channels.getByIndex(i);
|
||||||
|
if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CannedMessages::RecipientItem r;
|
||||||
|
|
||||||
|
// Set index
|
||||||
|
r.channelIndex = channel.index;
|
||||||
|
|
||||||
|
// Set a label for the menu item
|
||||||
|
r.label = "Ch " + to_string(i) + ": ";
|
||||||
|
if (channel.role == meshtastic_Channel_Role_PRIMARY)
|
||||||
|
r.label += "Primary";
|
||||||
|
else
|
||||||
|
r.label += parse(channel.settings.name);
|
||||||
|
|
||||||
|
// Add to the list of recipients
|
||||||
|
cm.recipientItems.push_back(r);
|
||||||
|
|
||||||
|
// Add a menu item for this recipient
|
||||||
|
const char *itemText = cm.recipientItems.back().label.c_str();
|
||||||
|
items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create recipient data (and menu items) for favorite nodes
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t nodeCount = nodeDB->getNumMeshNodes();
|
||||||
|
uint32_t favoriteCount = 0;
|
||||||
|
|
||||||
|
// Count favorites
|
||||||
|
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||||
|
if (nodeDB->getMeshNodeByIndex(i)->is_favorite)
|
||||||
|
favoriteCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add favorites if the number is reasonable
|
||||||
|
// Don't want some monstrous list that takes 100 clicks to reach exit
|
||||||
|
if (favoriteCount < 20) {
|
||||||
|
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Skip node if not a favorite
|
||||||
|
if (!node->is_favorite)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CannedMessages::RecipientItem r;
|
||||||
|
|
||||||
|
r.dest = node->num;
|
||||||
|
r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?)
|
||||||
|
|
||||||
|
// Set a label for the menu item
|
||||||
|
r.label = "DM: ";
|
||||||
|
if (node->has_user)
|
||||||
|
r.label += parse(node->user.long_name);
|
||||||
|
else
|
||||||
|
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
|
||||||
|
|
||||||
|
// Add to the list of recipients
|
||||||
|
cm.recipientItems.push_back(r);
|
||||||
|
|
||||||
|
// Add a menu item for this recipient
|
||||||
|
const char *itemText = cm.recipientItems.back().label.c_str();
|
||||||
|
items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
|
||||||
// Renders the panel shown at the top of the root menu.
|
// Renders the panel shown at the top of the root menu.
|
||||||
// Displays the clock, and several other pieces of instantaneous system info,
|
// 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.
|
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||||
@@ -619,4 +750,37 @@ uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
|
|||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a text message to the mesh
|
||||||
|
// Used to send our canned messages
|
||||||
|
void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message)
|
||||||
|
{
|
||||||
|
meshtastic_MeshPacket *p = router->allocForSending();
|
||||||
|
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||||
|
p->to = dest;
|
||||||
|
p->channel = channel;
|
||||||
|
p->want_ack = true;
|
||||||
|
p->decoded.payload.size = strlen(message);
|
||||||
|
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||||
|
|
||||||
|
// Tack on a bell character if requested
|
||||||
|
if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) {
|
||||||
|
p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character
|
||||||
|
p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator
|
||||||
|
p->decoded.payload.size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||||
|
|
||||||
|
service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free up any heap mmemory we'd used while selecting / sending canned messages
|
||||||
|
void InkHUD::MenuApplet::freeCannedMessageResources()
|
||||||
|
{
|
||||||
|
cm.selectedMessageItem = nullptr;
|
||||||
|
cm.selectedRecipientItem = nullptr;
|
||||||
|
cm.messageItems.clear();
|
||||||
|
cm.recipientItems.clear();
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -6,10 +6,12 @@
|
|||||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||||
#include "graphics/niche/InkHUD/Persistence.h"
|
#include "graphics/niche/InkHUD/Persistence.h"
|
||||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||||
|
#include "graphics/niche/Utils/CannedMessageStore.h"
|
||||||
|
|
||||||
#include "./MenuItem.h"
|
#include "./MenuItem.h"
|
||||||
#include "./MenuPage.h"
|
#include "./MenuPage.h"
|
||||||
|
|
||||||
|
#include "Channels.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
|
|
||||||
namespace NicheGraphics::InkHUD
|
namespace NicheGraphics::InkHUD
|
||||||
@@ -36,12 +38,18 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
|
|
||||||
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||||
void showPage(MenuPage page); // Load and display a MenuPage
|
void showPage(MenuPage page); // Load and display a MenuPage
|
||||||
|
|
||||||
|
void populateSendPage(); // Dynamically create MenuItems including canned messages
|
||||||
|
void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message
|
||||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||||
|
|
||||||
uint16_t getSystemInfoPanelHeight();
|
uint16_t getSystemInfoPanelHeight();
|
||||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||||
|
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
|
||||||
|
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
|
||||||
|
|
||||||
MenuPage currentPage = MenuPage::ROOT;
|
MenuPage currentPage = MenuPage::ROOT;
|
||||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||||
@@ -51,6 +59,37 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
|
|
||||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||||
|
|
||||||
|
// Data for selecting and sending canned messages via the menu
|
||||||
|
// Placed into a sub-class for organization only
|
||||||
|
class CannedMessages
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Share NicheGraphics component
|
||||||
|
// Handles loading, getting, setting
|
||||||
|
CannedMessageStore *store;
|
||||||
|
|
||||||
|
// One canned message
|
||||||
|
// Links the menu item to the true message text
|
||||||
|
struct MessageItem {
|
||||||
|
std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed
|
||||||
|
std::string rawText; // The message which will be sent, if this item is selected
|
||||||
|
} *selectedMessageItem;
|
||||||
|
|
||||||
|
// One possible destination for a canned message
|
||||||
|
// Links the menu item to the intended recipient
|
||||||
|
// May represent either broadcast or DM
|
||||||
|
struct RecipientItem {
|
||||||
|
std::string label; // Shown in menu
|
||||||
|
NodeNum dest = NODENUM_BROADCAST;
|
||||||
|
uint8_t channelIndex = 0;
|
||||||
|
} *selectedRecipientItem;
|
||||||
|
|
||||||
|
// These lists are generated when the menu page is populated
|
||||||
|
// Cleared onBackground (when MenuApplet closes)
|
||||||
|
std::vector<MessageItem> messageItems;
|
||||||
|
std::vector<RecipientItem> recipientItems;
|
||||||
|
} cm;
|
||||||
|
|
||||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
enum MenuPage : uint8_t {
|
enum MenuPage : uint8_t {
|
||||||
ROOT, // Initial menu page
|
ROOT, // Initial menu page
|
||||||
SEND,
|
SEND,
|
||||||
|
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
||||||
OPTIONS,
|
OPTIONS,
|
||||||
APPLETS,
|
APPLETS,
|
||||||
AUTOSHOW,
|
AUTOSHOW,
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ using namespace NicheGraphics;
|
|||||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||||
|
|
||||||
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex)
|
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
||||||
|
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex)
|
||||||
{
|
{
|
||||||
// Create the message store
|
// Create the message store
|
||||||
// Will shortly attempt to load messages from RAM, if applet is active
|
// Will shortly attempt to load messages from RAM, if applet is active
|
||||||
@@ -69,8 +70,7 @@ void InkHUD::ThreadedMessageApplet::onRender()
|
|||||||
|
|
||||||
// Grab data for message
|
// Grab data for message
|
||||||
MessageStore::Message &m = store->messages.at(i);
|
MessageStore::Message &m = store->messages.at(i);
|
||||||
bool outgoing = (m.sender == 0);
|
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
|
||||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
|
|
||||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||||
|
|
||||||
// Cache bottom Y of message text
|
// Cache bottom Y of message text
|
||||||
@@ -171,54 +171,54 @@ void InkHUD::ThreadedMessageApplet::onRender()
|
|||||||
void InkHUD::ThreadedMessageApplet::onActivate()
|
void InkHUD::ThreadedMessageApplet::onActivate()
|
||||||
{
|
{
|
||||||
loadMessagesFromFlash();
|
loadMessagesFromFlash();
|
||||||
textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage
|
loopbackOk = true; // Allow us to handle messages generated on the node (canned messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code which runs when the applet stop running
|
// Code which runs when the applet stop running
|
||||||
// This might be happen at shutdown, or if user disables the applet at run-time
|
// This might be at shutdown, or if the user disables the applet at run-time, via the menu
|
||||||
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
||||||
{
|
{
|
||||||
textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage
|
loopbackOk = false; // Slightly reduce our impact if the applet is disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new text messages
|
// Handle new text messages
|
||||||
// These might be incoming, from the mesh, or outgoing from phone
|
// These might be incoming, from the mesh, or outgoing from phone
|
||||||
// Each instance of the ThreadMessageApplet will only listen on one specific channel
|
// Each instance of the ThreadMessageApplet will only listen on one specific channel
|
||||||
// Method should return 0, to indicate general success to TextMessageModule
|
ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
|
||||||
{
|
{
|
||||||
// Abort if applet fully deactivated
|
// Abort if applet fully deactivated
|
||||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
|
||||||
if (!isActive())
|
if (!isActive())
|
||||||
return 0;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Abort if wrong channel
|
// Abort if wrong channel
|
||||||
if (p->channel != this->channelIndex)
|
if (mp.channel != this->channelIndex)
|
||||||
return 0;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Abort if message was a DM
|
// Abort if message was a DM
|
||||||
if (p->to != NODENUM_BROADCAST)
|
if (mp.to != NODENUM_BROADCAST)
|
||||||
return 0;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Extract info into our slimmed-down "StoredMessage" type
|
// Extract info into our slimmed-down "StoredMessage" type
|
||||||
MessageStore::Message newMessage;
|
MessageStore::Message newMessage;
|
||||||
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||||
newMessage.sender = p->from;
|
newMessage.sender = mp.from;
|
||||||
newMessage.channelIndex = p->channel;
|
newMessage.channelIndex = mp.channel;
|
||||||
newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]);
|
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||||
|
|
||||||
// Store newest message at front
|
// Store newest message at front
|
||||||
// These records are used when rendering, and also stored in flash at shutdown
|
// These records are used when rendering, and also stored in flash at shutdown
|
||||||
store->messages.push_front(newMessage);
|
store->messages.push_front(newMessage);
|
||||||
|
|
||||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||||
if (getFrom(p) != nodeDB->getNodeNum())
|
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||||
requestAutoshow();
|
requestAutoshow();
|
||||||
|
|
||||||
// Redraw the applet, perhaps.
|
// Redraw the applet, perhaps.
|
||||||
requestUpdate(); // Want to update display, if applet is foreground
|
requestUpdate(); // Want to update display, if applet is foreground
|
||||||
|
|
||||||
return 0;
|
// Tell Module 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
|
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
|
|
||||||
class Applet;
|
class Applet;
|
||||||
|
|
||||||
class ThreadedMessageApplet : public Applet
|
class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||||
@@ -41,16 +41,11 @@ class ThreadedMessageApplet : public Applet
|
|||||||
void onActivate() override;
|
void onActivate() override;
|
||||||
void onDeactivate() override;
|
void onDeactivate() override;
|
||||||
void onShutdown() override;
|
void onShutdown() override;
|
||||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Used to register our text message callback
|
|
||||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
|
||||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *>(this,
|
|
||||||
&ThreadedMessageApplet::onReceiveTextMessage);
|
|
||||||
|
|
||||||
void saveMessagesToFlash();
|
void saveMessagesToFlash();
|
||||||
void loadMessagesFromFlash();
|
void loadMessagesFromFlash();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
#include "./Events.h"
|
#include "./Events.h"
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "buzz.h"
|
||||||
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|
||||||
#include "./Applet.h"
|
#include "./Applet.h"
|
||||||
#include "./SystemApplet.h"
|
#include "./SystemApplet.h"
|
||||||
#include "graphics/niche/FlashData.h"
|
#include "graphics/niche/Utils/FlashData.h"
|
||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ void InkHUD::Events::begin()
|
|||||||
rebootObserver.observe(¬ifyReboot);
|
rebootObserver.observe(¬ifyReboot);
|
||||||
textMessageObserver.observe(textMessageModule);
|
textMessageObserver.observe(textMessageModule);
|
||||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||||
adminMessageObserver.observe(adminModule);
|
adminMessageObserver.observe((Observable<AdminModule_ObserverData *> *)adminModule);
|
||||||
#endif
|
#endif
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
lightSleepObserver.observe(¬ifyLightSleep);
|
lightSleepObserver.observe(¬ifyLightSleep);
|
||||||
@@ -37,6 +38,13 @@ void InkHUD::Events::begin()
|
|||||||
|
|
||||||
void InkHUD::Events::onButtonShort()
|
void InkHUD::Events::onButtonShort()
|
||||||
{
|
{
|
||||||
|
// Audio feedback (via buzzer)
|
||||||
|
// Short low tone
|
||||||
|
playBoop();
|
||||||
|
// Cancel any beeping, buzzing, blinking
|
||||||
|
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||||
|
bool dismissedExt = dismissExternalNotification();
|
||||||
|
|
||||||
// Check which system applet wants to handle the button press (if any)
|
// Check which system applet wants to handle the button press (if any)
|
||||||
SystemApplet *consumer = nullptr;
|
SystemApplet *consumer = nullptr;
|
||||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
@@ -49,12 +57,16 @@ void InkHUD::Events::onButtonShort()
|
|||||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||||
if (consumer)
|
if (consumer)
|
||||||
consumer->onButtonShortPress();
|
consumer->onButtonShortPress();
|
||||||
else
|
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||||
inkhud->nextApplet();
|
inkhud->nextApplet();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::Events::onButtonLong()
|
void InkHUD::Events::onButtonLong()
|
||||||
{
|
{
|
||||||
|
// Audio feedback (via buzzer)
|
||||||
|
// Low tone, longer than playBoop
|
||||||
|
playBeep();
|
||||||
|
|
||||||
// Check which system applet wants to handle the button press (if any)
|
// Check which system applet wants to handle the button press (if any)
|
||||||
SystemApplet *consumer = nullptr;
|
SystemApplet *consumer = nullptr;
|
||||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
@@ -102,6 +114,10 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
|
|||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||||
delay(1000); // Cooldown, before potentially yanking display power
|
delay(1000); // Cooldown, before potentially yanking display power
|
||||||
|
|
||||||
|
// InkHUD shutdown complete
|
||||||
|
// Firmware shutdown continues for several seconds more; flash write still pending
|
||||||
|
playShutdownMelody();
|
||||||
|
|
||||||
return 0; // We agree: deep sleep now
|
return 0; // We agree: deep sleep now
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,14 +192,15 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||||
}
|
}
|
||||||
|
|
||||||
int InkHUD::Events::onAdminMessage(const meshtastic_AdminMessage *message)
|
int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data)
|
||||||
{
|
{
|
||||||
switch (message->which_payload_variant) {
|
switch (data->request->which_payload_variant) {
|
||||||
// Factory reset
|
// Factory reset
|
||||||
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
||||||
case meshtastic_AdminMessage_factory_reset_device_tag:
|
case meshtastic_AdminMessage_factory_reset_device_tag:
|
||||||
case meshtastic_AdminMessage_factory_reset_config_tag:
|
case meshtastic_AdminMessage_factory_reset_config_tag:
|
||||||
eraseOnReboot = true;
|
eraseOnReboot = true;
|
||||||
|
*data->result = AdminMessageHandleResult::HANDLED;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -204,4 +221,24 @@ int InkHUD::Events::beforeLightSleep(void *unused)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Silence all ongoing beeping, blinking, buzzing, coming from the external notification module
|
||||||
|
// Returns true if an external notification was active, and we dismissed it
|
||||||
|
// Button handling changes depending on our result
|
||||||
|
bool InkHUD::Events::dismissExternalNotification()
|
||||||
|
{
|
||||||
|
// Abort if not using external notifications
|
||||||
|
if (!moduleConfig.external_notification.enabled)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Abort if nothing to dismiss
|
||||||
|
if (!externalNotificationModule->nagging())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Stop the beep buzz blink
|
||||||
|
externalNotificationModule->stopNow();
|
||||||
|
|
||||||
|
// Inform that we did indeed dismiss an external notification
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -13,7 +13,7 @@ however this class handles general events which concern InkHUD as a whole, e.g.
|
|||||||
|
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#include "Observer.h"
|
#include "modules/AdminModule.h"
|
||||||
|
|
||||||
#include "./InkHUD.h"
|
#include "./InkHUD.h"
|
||||||
#include "./Persistence.h"
|
#include "./Persistence.h"
|
||||||
@@ -33,7 +33,7 @@ class Events
|
|||||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||||
int beforeReboot(void *unused); // Prepare for reboot
|
int beforeReboot(void *unused); // Prepare for reboot
|
||||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||||
int onAdminMessage(const meshtastic_AdminMessage *message); // Handle incoming admin messages
|
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
int beforeLightSleep(void *unused); // Prepare for light sleep
|
int beforeLightSleep(void *unused); // Prepare for light sleep
|
||||||
#endif
|
#endif
|
||||||
@@ -54,14 +54,17 @@ class Events
|
|||||||
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
||||||
|
|
||||||
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
||||||
CallbackObserver<Events, const meshtastic_AdminMessage *> adminMessageObserver =
|
CallbackObserver<Events, AdminModule_ObserverData *> adminMessageObserver =
|
||||||
CallbackObserver<Events, const meshtastic_AdminMessage *>(this, &Events::onAdminMessage);
|
CallbackObserver<Events, AdminModule_ObserverData *>(this, &Events::onAdminMessage);
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
// Get notified when the system is entering light sleep
|
// Get notified when the system is entering light sleep
|
||||||
CallbackObserver<Events, void *> lightSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeLightSleep);
|
CallbackObserver<Events, void *> lightSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeLightSleep);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// End any externalNotification beeping, buzzing, blinking etc
|
||||||
|
bool dismissExternalNotification();
|
||||||
|
|
||||||
// If set, InkHUD's data will be erased during onReboot
|
// If set, InkHUD's data will be erased during onReboot
|
||||||
bool eraseOnReboot = false;
|
bool eraseOnReboot = false;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user