Compare commits

...

77 Commits

Author SHA1 Message Date
vidplace7
e8e6b39bc9 Add libuv to GitHub Actions 2025-03-18 15:12:29 -04:00
Jorropo
bf7afd657a replace symlink to point to my fork
Allows anyone to test the PR
2025-03-18 20:03:38 +01:00
vidplace7
ca951caa38 Add libuv to Linux packaging 2025-03-18 20:03:38 +01:00
Jorropo
16a1c9f148 Add UDP multicast support on linux.
We tested it an it works.

This is really hacky to say the least.
2025-03-18 20:03:38 +01:00
Jorropo
af8b64e84e pass pointer to UDP multicast packet to protobuf decoder (#6333)
The packet.readBytes API is not available on all targets:
- RP2040 & RP2340
- yet to be written portduino API

Instead pass the data buffer as-is.
It also removes a memcpy which do not need to exists.

I've tested it successfully on a tbeam.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-03-16 19:36:33 -05:00
Jorropo
96ba94843b Send UDP packets to multicast address rather than broadcast address (#6331)
A smart router or switch is able to snoop the multicast address to
only send the packets to nodes listening on the multicast address.

Before all machines reachable on the L2 layer would receive the packet.
2025-03-16 19:36:02 -05:00
Thomas Göttgens
2d565c2921 trunk'd 2025-03-16 16:18:12 +01:00
HarukiToreda
2525111c39 E-ink partial refresh limitation removed for free text screen (#6201) 2025-03-16 16:15:33 +01:00
dylanli
64b9cfe199 update seeed-xiao-nrf52840-kit board defination (#6318)
- Due to the lack of pins, we have temporarily removed the button. There are some technical solutions that can solve this problem, and we are currently exploring and researching them
2025-03-16 16:04:24 +01:00
Ben Meadors
dc100e4d3e Cleanup 2025-03-16 08:19:46 -05:00
Thomas Göttgens
1640fb105d new device: Lilygo T-Eth-Elite (#6321) 2025-03-15 14:15:35 +01:00
github-actions[bot]
99e42b4d22 [create-pull-request] automated change (#6323)
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
2025-03-15 07:03:53 -05:00
Thomas Göttgens
79233fe99d mainline tlora v3 (#6322) 2025-03-15 11:30:58 +01:00
Chris Danis
f66784ed2a Don't allow is_managed without any valid admin_keys (#6310) 2025-03-14 10:10:38 -05:00
github-actions[bot]
f198d5d49f Upgrade trunk to 1.22.11 (#6316)
Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com>
2025-03-14 06:58:08 -05:00
dependabot[bot]
4d34b3d73c Bump dorny/test-reporter from 1.9.1 to 2.0.0 in /.github/workflows (#6309)
Bumps [dorny/test-reporter](https://github.com/dorny/test-reporter) from 1.9.1 to 2.0.0.
- [Release notes](https://github.com/dorny/test-reporter/releases)
- [Changelog](https://github.com/dorny/test-reporter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dorny/test-reporter/compare/v1.9.1...v2.0.0)

---
updated-dependencies:
- dependency-name: dorny/test-reporter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 08:32:49 -05:00
paragonnov
8efe8a2ea3 Fix KR920's Tx power limitation (#6307) 2025-03-13 18:14:41 +08:00
Kalle Lilja
499ea56e3b update devcontainer (#6299) 2025-03-12 15:32:34 -05:00
Ben Meadors
2473af6995 45 days stale 2025-03-12 12:43:55 -05:00
github-actions[bot]
508ab171d6 Upgrade trunk (#6295)
Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com>
2025-03-12 06:22:24 -05:00
Manuel
ec59f7d7dd fix packet queue full (#6292) 2025-03-11 18:59:44 -05:00
Kalle Lilja
f4c79530ec update gitattributes for windows (#6289) 2025-03-11 13:05:51 -05:00
Mark Trevor Birss
e9effb9fff Update platformio.ini (#6286) 2025-03-11 15:45:20 +02:00
Mark Trevor Birss
cb6dfb66d2 Update ME25LS01/MS24SF1 comment out upload port (#6285)
* Update platformio.ini

* Update platformio.ini

* Update platformio.ini
2025-03-11 14:56:12 +02:00
github-actions[bot]
8795a63427 Upgrade trunk (#6283)
Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com>
2025-03-11 06:26:45 -05:00
Mark Trevor Birss
186e509607 Update esp32-s3-pico.json (#6284)
* Update esp32-s3-pico.json

* Update esp32-s3-pico.json
2025-03-11 13:11:11 +02:00
Manuel
7c3eddebc2 device-ui: exFat support (#6279) 2025-03-10 16:42:29 -05:00
Ben Meadors
78b4eff568 Bump 2025-03-10 11:57:39 -05:00
Kalle Lilja
3c1f92ce84 Update device-install scripts (#6267)
* fix example

* check for firmware- filename

* add powershell formatter setting

* add crlf for ps1

* formatting

* check for firmware- filename

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-03-09 06:43:16 -05:00
todd-herbert
5de6bc1851 Fix excluded_modules metadata with InkHUD (#6272) 2025-03-08 19:06:32 -06:00
Austin
c54fc5b7c5 Thread in harmony (#6271) 2025-03-08 16:36:55 -06:00
github-actions[bot]
94de2315c1 [create-pull-request] automated change (#6266)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-03-08 06:22:11 -06:00
Chris Danis
7f17747d8c NodeInfo exchange: don't bother if too far away (#6260)
When we receive a NodeInfo from a new node, if it is more than 2 hops
beyond our configured hop limit away from us, don't bother to send a
NodeInfo back to it.

In my dense urban environment, I see many nodes that are >= 5 hops away,
but sending their NodeInfo with a hopStart of 6 or 7.  In most cases
I can imagine, this seems like a waste of airtime.
2025-03-08 09:33:23 +08:00
Austin
16a0dce83c Ebyte E77 (STM32) DevKit support (#6255) 2025-03-07 17:37:54 -06:00
Austin
3fd47d9713 Actions: Move version bump into release_channels (#6258) 2025-03-07 06:38:15 -06:00
Tom Fifield
284598ed56 Add detection support for LTR390UV Sensor (#6009)
* Add detection support for LTR390UV Sensor

The LTR390 is a UV sensor. This patch adds detection support, for
a future patch that will add the full sensor support.

* Update ScanI2C.h
2025-03-07 18:51:38 +08:00
github-actions[bot]
2a3e1f904d Upgrade trunk (#6257)
Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com>
2025-03-07 09:12:08 +01:00
Mark Trevor Birss
60e46cd765 Update platformio.ini (#6245) 2025-03-07 14:21:06 +08:00
Tom Fifield
563747c5cd Flag semgrep to not run on self-hosted (#6256)
The semgrep action runs inside a docker container,
and docker in podman just doesn't work.
2025-03-07 11:54:32 +08:00
Chris Danis
5c77d42345 i2c: 0x45 can also be an SHT35 (#6249) 2025-03-07 09:49:55 +08:00
Tom Fifield
f0a2ae9ff3 Give Semgrep permission to write its report (#6253)
Previously semgrep had read-all permission. This patch limits read
slightly and adds write permissions to security-events.
2025-03-07 08:52:54 +08:00
Kalle Lilja
f7afa9a81e [Task]: 2.6 device-install scripts (#6248)
* update device-install.bat

* add device-install unittest

* update device-update.bat

* update uf2-convert.bat

* update regen-protos.bat

* update rem

* bump version

* update device-install.sh

* add esptool

* move esptool to setup.sh

* trunk check+fmt

* update uf2-convert.bat
2025-03-06 16:58:08 -06:00
Ben Meadors
c8bd6c32cc Correct HW_MODEL 2025-03-06 08:43:03 -06:00
Mark Trevor Birss
f6a9e7d741 Add initial support for CrowPanel ESP32 5.79” E-paper HMI (#6233) 2025-03-06 11:28:43 +01:00
todd-herbert
e6a98b1d6b InkHUD refactoring (#6216)
* chore: todo.txt
* chore: comments
* fix: no fast refresh on VME290
Reverts a line of code which was accidentally committed
* refactor: god class
Divide the behavior from the old WindowManager class into several subclasses which each have a clear role.
* refactor: cppcheck medium warnings
Enough to pass github CI for now
* refactor: updateType selection
* refactor: don't use a setter for the shared AppletFonts
* fix: update prioritization
forceUpdate calls weren't being prioritized
* refactor: remove unhelpful logging
getTimeString is used for parsing our own time, but also the timestamps of messages. The "one time only" log printing will likely fire in unhelpful situations.
* fix: " "
* refactor: get rid of types.h file for enums
* Keep that sneaky todo file out of commits
2025-03-06 11:25:41 +01:00
Tavis
b2ef92a328 add rain data from ws85 (#6242)
add rain data as 1h and 24h
2025-03-06 10:55:08 +01:00
Andrik45719
b25db1f42c E22-400M SX126X_DIO3_TCXO_VOLTAGE fix (#6232)
Added TCXO voltage setting for SX1268 based module to fix error:
Calibration failed, device errors: 0x20
SX126x init result -707
2025-03-06 09:44:53 +01:00
Austin
a924b9d94a Small Fix: Don't run Dependabot on protobufs (#6241) 2025-03-06 09:36:27 +01:00
Chris Danis
f5e0e282b6 environment: add DPS310 high-accuracy barometer (#6237)
* dps310: initial scan support

* dps310 sensor support

* new protobufs

* new protobufs

* address cr

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-03-06 11:58:18 +08:00
github-actions[bot]
a3a9b2fe84 [create-pull-request] automated change (#6240)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-03-05 20:50:20 -06:00
Ben Meadors
6c8058e1d8 Update SEEED_XIAO_NRF52840_KIT (#6239) 2025-03-06 09:44:22 +08:00
dylanli
445efe9e21 Add support for seeed_xiao_nrf52840_kit (#6231)
* add support for seeed_xiao_nrf52840_kit

* Update platformio.ini remove board level define
2025-03-05 16:22:25 -06:00
Austin
b96b027926 Consume device-ui as a pio library (#6193) 2025-03-05 16:19:59 -06:00
github-actions[bot]
239e5412b3 [create-pull-request] automated change (#6235)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-03-05 14:53:51 -06:00
Michael Gjelsø
ede3f7b702 Changes for 2.6 device_install (#6206)
* Changes for 2.6 device_install

For #6186
Added 2 new arguments --tft and -tft-16mb
Some checks are added.
Before it would try to write all files to the device, if there was more than ONE littlefs-* or littlefswebui-* in the directory.

Added OTA Offsets for 8 and 16mb (fix)
Thanks to @caveman99 for spotting it.

* The missing SET

Added a missing SET.

Thanks to @ThatKalle

* Fix and more checks.

Added Checks to make sure, that --tft and --tft-16mb can't be used with a non tft bin file.
Added error messages on files not found.
Removed a "ECHO" that shouldn't be there.

* Fixes to device-install.sh

Replace /bin/sh with /bin/bash for better string handling.
Removed a SET that doesn't belong in the .sh file.
Better checking for TFT and non TFT build, based on filename.
Corrected a mix of TAB & SPACE indent.

* Update device-install.bat

Corrected a mix of TAB & SPACE indent.

* Update device-install.bat

Double ELSE block at the end of file, one removed.

* Update device-install.bat

Added more reliable method to display the scripts own name in help menu.
Fixed case sensitive options -p and -P
Added some VAR cleanup.
Changed the detect method on BLEOTA.
Changed some wording.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-03-05 13:07:26 -06:00
Mictronics
f0f2cd0e0e RAK11310 Fix build with latest Arduino framework (#6227) 2025-03-04 21:39:10 +01:00
Matt Andreko
fdbadc992c Enable GPS functionality for RAK4631_eth_gw variant (#6229) 2025-03-04 21:27:57 +01:00
Tom Fifield
2391982c1d Only call GPS Probe commands once per family (#6114)
In the GPS probe code we write commands on the serial line and
 determine which GPS we have based on the result.

GPS units in the same family sometimes use the same command,
 but return different results (eg AG3335 and AG3332 both use $PAIR021*39).
Currently we run the command once per GPS. Instead we should run each
command only once per family, record the result, and select the GNSS MODEL
 based on the result, which is what this patch does.

Before the change, we put 12 commands on the serial bus.
Now we only put 6.

This should markedly improve the speed and reliability of GPS detection.

Fixes https://github.com/meshtastic/firmware/issues/5193

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-03-04 09:47:06 -06:00
dependabot[bot]
41875d245e Bump lib/device-ui from 5c6156d to 22f9ac0 (#6215)
Bumps [lib/device-ui](https://github.com/meshtastic/device-ui) from `5c6156d` to `22f9ac0`.
- [Commits](5c6156d2aa...22f9ac01ea)

---
updated-dependencies:
- dependency-name: lib/device-ui
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-04 09:45:29 -06:00
github-actions[bot]
95bcd7ab0b Upgrade trunk (#6223)
Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com>
2025-03-04 09:45:02 -06:00
github-actions[bot]
050f0016c4 [create-pull-request] automated change (#6221) 2025-03-04 01:56:35 +01:00
Thomas Göttgens
6715662281 don't build the niche* stuff for non-inkHUD builds. (#6217) 2025-03-03 09:10:47 -06:00
Mictronics
b6562e175f RAK11310 support for RAK12002 RTC added. (#6210) 2025-03-03 11:08:02 +01:00
Thomas Göttgens
f89f916f96 Revert "Trunk: Add clang-tidy (#6171)" (#6203)
This reverts commit 12fde696c1.
2025-03-02 13:58:37 +01:00
Mictronics
43a6e711da RAK11310: Update to last building platform package and possibly fix for #5361 (#6202) 2025-03-02 13:15:30 +01:00
Mark Trevor Birss
63b20e358f Create lora-raxda-rock2f-starter-edition-hat.yaml (#6192)
* Create lora-raxda-rock2f-starter-edition-hat.yaml

* Update lora-raxda-rock2f-starter-edition-hat.yaml
2025-03-02 06:14:07 -06:00
Austin
12fde696c1 Trunk: Add clang-tidy (#6171) 2025-03-02 11:27:53 +01:00
Ben Meadors
5c8f1fb46b Enable external (UART) GPS support on WM1110 tracker dev board (#6189) 2025-03-01 08:27:43 -06:00
Mark Trevor Birss
ce38ac10d1 Create lora-starter-edition-sx1262-i2c.yaml and lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml (#6162)
* Create lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml

* Create lora-starter-edition-sx1262-i2c.yaml

* Update lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml

* Update lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml

* Update lora-starter-edition-sx1262-i2c.yaml
2025-03-01 07:14:04 -06:00
github-actions[bot]
d5ec205572 Upgrade trunk (#6188)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-03-01 06:58:39 -06:00
GUVWAF
9893d24c62 Only request all NodeInfo/Position on fresh install (#6184)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-03-01 06:57:44 -06:00
github-actions[bot]
ab61cd65d1 Upgrade trunk (#6178)
Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com>
2025-03-01 06:57:12 -06:00
Austin
baef8dce79 Switch pio_deps to native-tft for flatpak (#6187)
Consumed in flatpak for "offline" builds.
2025-03-01 06:56:49 -06:00
Ben Meadors
99d3e5eb70 2.6 changes (#5806)
* 2.6 protos

* [create-pull-request] automated change (#5789)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Hello world support for UDP broadcasts over the LAN on ESP32 (#5779)

* UDP local area network meshing on ESP32

* Logs

* Comment

* Update UdpMulticastThread.h

* Changes

* Only use router->send

* Make NodeDatabase (and file) independent of DeviceState (#5813)

* Make NodeDatabase (and file) independent of DeviceState

* 70

* Remove logging statement no longer needed

* Explicitly set CAD symbols, improve slot time calculation and adjust CW size accordingly (#5772)

* File system persistence fixes

* [create-pull-request] automated change (#6000)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Update ref

* Back to 80

* [create-pull-request] automated change (#6002)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* 2.6 <- Next hop router (#6005)

* Initial version of NextHopRouter

* Set original hop limit in header flags

* Short-circuit to FloodingRouter for broadcasts

* If packet traveled 1 hop, set `relay_node` as `next_hop` for the original transmitter

* Set last byte to 0xFF if it ended at 0x00
As per an idea of @S5NC

* Also update next-hop based on received DM for us

* temp

* Add 1 retransmission for intermediate hops when using NextHopRouter

* Add next_hop and relayed_by in PacketHistory for setting next-hop and handle flooding fallback

* Update protos, store multiple relayers

* Remove next-hop update logic from NeighborInfoModule

* Fix retransmissions

* Improve ACKs for repeated packets and responses

* Stop retransmission even if there's not relay node

* Revert perhapsRebroadcast()

* Remove relayer if we cancel a transmission

* Better checking for fallback to flooding

* Fix newlines in traceroute print logs

* Stop retransmission for original packet

* Use relayID

* Also when want_ack is set, we should try to retransmit

* Fix cppcheck error

* Fix 'router' not in scope error

* Fix another cppcheck error

* Check for hop_limit and also update next hop when `hop_start == hop_limit` on ACK
Also check for broadcast in `getNextHop()`

* Formatting and correct NUM_RETRANSMISSIONS

* Update protos

* Start retransmissions in NextHopRouter if ReliableRouter didn't do it

* Handle repeated/fallback to flooding packets properly
First check if it's not still in the TxQueue

* Guard against clients setting `next_hop`/`relay_node`

* Don't cancel relay if we were the assigned next-hop

* Replies (e.g. tapback emoji) are also a valid confirmation of receipt

---------

Co-authored-by: GUVWAF <thijs@havinga.eu>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com>

* fix "native" compiler errors/warnings NodeDB.h

* fancy T-Deck / SenseCAP Indicator / unPhone / PICOmputer-S3 TFT screen (#3259)

* lib update: light theme

* fix merge issue

* lib update: home buttons + button try-fix

* lib update: icon color fix

* lib update: fix instability/crash on notification

* update lib: timezone

* timezone label

* lib update: fix set owner

* fix spiLock in RadioLibInterface

* add picomputer tft build

* picomputer build

* fix compiler error std::find()

* fix merge

* lib update: theme runtime config

* lib update: packet logger + T-Deck Plus

* lib update: mesh detector

* lib update: fix brightness & trackball crash

* try-fix less paranoia

* sensecap indicator updates

* lib update: indicator fix

* lib update: statistic & some fixes

* lib-update: other T-Deck touch driver

* use custom touch driver for Indicator

* lower tft task prio

* prepare LVGL ST7789 driver

* lib update: try-fix audio

* Drop received packets from self

* Additional decoded packet ignores

* Honor flip & color for Heltec T114 and T190 (#4786)

* Honor TFT_MESH color if defined for Heltec T114 or T190

* Temporary: point lib_deps at fork of Heltec's ST7789 library
For demo only, until ST7789 is merged

* Update lib_deps; tidy preprocessor logic

* Download debian files after firmware zip

* set title for protobufs bump PR (#4792)

* set title for version bump PR (#4791)

* Enable Dependabot

* chore: trunk fmt

* fix dependabot syntax (#4795)

* fix dependabot syntax

* Update dependabot.yml

* Update dependabot.yml

* Bump peter-evans/create-pull-request from 6 to 7 in /.github/workflows (#4797)

* Bump docker/build-push-action from 5 to 6 in /.github/workflows (#4800)

* Actions: Semgrep Images have moved from returntocorp to semgrep (#4774)

https://hub.docker.com/r/returntocorp/semgrep notes: "We've moved!
 Official Docker images for Semgrep now available at semgrep/semgrep."

Patch updates our CI workflow for these images.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Bump meshtestic from `31ee3d9` to `37245b3` (#4799)

Bumps [meshtestic](https://github.com/meshtastic/meshTestic) from `31ee3d9` to `37245b3`.
- [Commits](31ee3d90c8...37245b3d61)

---
updated-dependencies:
- dependency-name: meshtestic
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [create-pull-request] automated change (#4789)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Bump pnpm/action-setup from 2 to 4 in /.github/workflows (#4798)

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2 to 4.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2...v4)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Raspberry Pico2 - needs protos

* Re-order doDeepSleep (#4802)

Make sure PMU sleep takes place before I2C ends

* [create-pull-request] automated change

* heltec-wireless-bridge
requires Proto PR first

* feat: trigger class update when protobufs are changed

* meshtastic/ is a test suite; protobufs/ contains protobufs;

* Update platform-native to pick up portduino crash fix (#4807)

* Hopefully extract and commit to meshtastic.github.io

* CI fixes

* [Board] DIY "t-energy-s3_e22" (#4782)

* New variant "t-energy-s3_e22"

- Lilygo T-Energy-S3
- NanoVHF "Mesh-v1.06-TTGO-T18" board
- Ebyte E22 Series

* add board_level = extra

* Update variant.h

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>

* Consolidate variant build steps (#4806)

* poc: consolidate variant build steps

* use build-variant action

* only checkout once and clean up after run

* Revert "Consolidate variant build steps (#4806)" (#4816)

This reverts commit 9f8d86cb25.

* Make Ublox code more readable (#4727)

* Simplify Ublox code

Ublox comes in a myriad of versions and settings. Presently our
configuration code does a lot of branching based on versions being
or not being present.

This patch adds version detection earlier in the piece and branches
on the set gnssModel instead to create separate setup methods for Ublox 6,
Ublox 7/8/9, and Ublox10.

Additionally, adds a macro to make the code much shorter and more
readable.

* Make trunk happy

* Make trunk happy

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Consider the LoRa header when checking packet length

* Minor fix (#4666)

* Minor fixes

It turns out setting a map value with the index notation causes
an lookup that can be avoided with emplace. Apply this to one line in
the StoreForward module.

Fix also Cppcheck-determined highly minor performance increase by
passing gpiochipname as a const reference :)

The amount of cycles used on this laptop while learning about these
callouts from cppcheck is unlikely to ever be more than the cycles
saved by the fixes ;)

* Update PortduinoGlue.cpp

* Revert "Update classes on protobufs update" (#4824)

* Revert "Update classes on protobufs update"

* remove quotes to fix trunk.

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>

* Implement optional second I2C bus for NRF52840
Enabled at compile-time if WIRE_INFERFACES_COUNT defined as 2

* Add I2C bus to Heltec T114 header pins
SDA: P0.13
SCL: P0.16

Uses bus 1, leaving bus 0 routed to the unpopulated footprint for the RTC (general future-proofing)

* Tidier macros

* Swap SDA and SCL
SDA=P0.16, SCL=P0.13

* Refactor and consolidate time window logic (#4826)

* Refactor and consolidate windowing logic

* Trunk

* Fixes

* More

* Fix braces and remove unused now variables.

There was a brace in src/mesh/RadioLibInterface.cpp that was breaking
compile on some architectures.

Additionally, there were some brace errors in
src/modules/Telemetry/AirQualityTelemetry.cpp
src/modules/Telemetry/EnvironmentTelemetry.cpp
src/mesh/wifi/WiFiAPClient.cpp

Move throttle include in WifiAPClient.cpp to top.

Add Default.h to sleep.cpp

rest of files just remove unused now variables.

* Remove a couple more meows

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>

* Rename message length headers and set payload max to 255 (#4827)

* Rename message length headers and set payload max to 255

* Add MESHTASTIC_PKC_OVERHEAD

* compare to MESHTASTIC_HEADER_LENGTH

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>

* Check for null before printing debug (#4835)

* fix merge

* try-fix crash

* lib update: fix neighbors

* fix GPIO0 mode after I2S audio

* lib update: audio fix

* lib update: fixes and improvements

* extra

* added ILI9342 (from master)

* device-ui persistency

* review update

* fix request, add handled

* fix merge issue

* fix merge issue

* remove newline

* remove newlines from debug log

* playing with locks; but needs more testing

* diy mesh-tab initial files

* board definition for mesh-tab (not yet used)

* use DISPLAY_SET_RESOLUTION to avoid hw dependency in code

* no telemetry for Indicator

* 16MB partition for Indicator

* 8MB partition for Indicator

* stability: add SPI lock before saving via littleFS

* dummy for config transfer (#5154)

* update indicator (due to compile and linker errors)

* remove faulty partition line

* fix missing include

* update indicator board

* update mesh-tab ILI9143 TFT

* fix naming

* mesh-tab targets

* try: disable duplicate locks

* fix nodeDB erase loop when free mem returns invalid value (0, -1).

* upgrade toolchain for nrf52 to gcc 9.3.1

* try-fix (workaround) T-Deck audio crash

* update mesh-tab tft configs

* set T-Deck audio to unused 48 (mem mclk)

* swap mclk to gpio 21

* update meshtab voltage divider

* update mesh-tab ini

* Fixed the issue that indicator device uploads via rp2040 serial port in some cases.

* Fixed the issue that the touch I2C address definition was not effective.

* Fixed the issue that the wifi configuration saved to RAM did not take effect.

* rotation fix; added ST7789 3.2" display

* dreamcatcher: assign GPIO44 to audio mclk

* mesh-tab touch updates

* add mesh-tab powersave as default

* fix DIO1 wakeup

* mesh-tab: enable alert message menu

* Streamline board definitions for first tech preview. (#5390)

* Streamline board definitions for first tech preview. TBD: Indicator Support

* add point-of-checkin

* use board/unphone.json

---------

Co-authored-by: mverch67 <manuel.verch@gmx.de>

* fix native targets

* add RadioLib debugging options for (T-Deck)

* fix T-Deck build

* fix native tft targets for rpi

* remove wrong debug defines

* t-deck-tft button is handled in device-ui

* disable default lightsleep for indicator

* Windows Support - Trunk and Platformio (#5397)

* Add support for GPG

* Add usb device support

* Add trunk.io to devcontainer

* Trunk things

* trunk fmt

* formatting

* fix trivy/DS002, checkov/CKV_DOCKER_3

* hide docker extension popup

* fix trivy/DS026, checkov/CKV_DOCKER_2

* fix radioLib warnings for T-Deck target

* wake screen with button only

* use custom touch driver

* define wake button for unphone

* use board definition for mesh-tab

* mesh-tab rotation upside-down

* update platform native

* use MESH_TAB hardware model definition

* radioLib update (fix crash/assert)

* reference seeed indicator fix commit arduino-esp32

* Remove unneeded file change :)

* disable serial module and tcp socket api for standalone devices (#5591)

* disable serial module and tcp socket api for standalone devices
* just disable webserver, leave wifi available
* disable socket api

* mesh-tab: lower I2C touch frequency

* log error when packet queue is full

* add more locking for shared SPI devices (#5595)

* add more locking for shared SPI devices
* call initSPI before the lock is used
* remove old one
* don't double lock
* Add missing unlock
* More missing unlocks
* Add locks to SafeFile, remove from `readcb`, introduce some LockGuards
* fix lock in setupSDCard()
* pull radiolib trunk with SPI-CS fixes
* change ContentHandler to Constructor type locks, where applicable

---------

Co-authored-by: mverch67 <manuel.verch@gmx.de>
Co-authored-by: GUVWAF <thijs@havinga.eu>
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>

* T-Deck: revert back to lovyanGFX touch driver

* T-Deck: increase allocated PSRAM by 50%

* mesh-tab: streamline target definitions

* update RadioLib 7.1.2

* mesh-tab: fix touch rotation 4.0 inch display

* Mesh-Tab platformio: 4.0inch: increase SPI frequency to max

* mesh-tab: fix rotation for 3.5 IPS capacitive display

* mesh-tab: fix rotation for 3.2 IPS capacitive display

* restructure device-ui library into sub-directories

* preparations for generic DisplayDriverFactory

* T-Deck: increase LVGL memory size

* update lib

* trunk fmt

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Jason Murray <15822260+scruplelesswizard@users.noreply.github.com>
Co-authored-by: Jason Murray <jason@chaosaffe.io>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: virgil <virgil.wang.cj@gmail.com>
Co-authored-by: Mark Trevor Birss <markbirss@gmail.com>
Co-authored-by: Kalle Lilja <15094562+ThatKalle@users.noreply.github.com>
Co-authored-by: GUVWAF <thijs@havinga.eu>

* Version this

* Update platformio.ini (#6006)

* tested higher speed and it works

* Un-extra

* Add -tft environments to the ci matrix

* Exclude unphone tft for now. Something is wonky

* fixed Indicator touch issue (causing IO expander issues), added more RAM

* update lib

* fixed Indicator touch issue (causing IO expander issues), added more RAM (#6013)

* increase T-Deck PSRAM to avoid too early out-of-memory when messages fill up the storage

* update device-ui lib

* Fix T-Deck SD card detection (#6023)

* increase T-Deck PSRAM to avoid too early out-of-memory when messages fill up the storage

* fix SDCard for T-Deck; allow SPI frequency config

* meshtasticd: Add X11 480x480 preset (#6020)

* Littlefs per device

* 2.6 update

* [create-pull-request] automated change (#6037)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* InkHUD UI for E-Ink (#6034)

* Decouple ButtonThread from sleep.cpp
Reorganize sleep observables. Don't call ButtonThread methods inside doLightSleep. Instead, handle in class with new lightsleep Observables.

* InkHUD: initial commit (WIP)
Publicly discloses the current work in progress. Not ready for use.

* feat: battery icon

* chore: implement meshtastic/firmware #5454
Clean up some inline functions

* feat: menu & settings for "jump to applet"

* Remove the beforeRender pattern
It hugely complicates things. If we can achieve acceptable performance without it, so much the better.

* Remove previous Map Applet
Needs re-implementation to work without the beforeRender pattern

* refactor: reimplement map applet
Doesn't require own position
Doesn't require the beforeRender pattern to precalculate; now all-at-once in render
Lays groundwork for fixed-size map with custom background image

* feat: autoshow
Allow user to select which applets (if any) should be automatically brought to foreground when they have new data to display

* refactor: tidy-up applet constructors
misc. jobs including:
- consistent naming
- move initializer-list-only constructors to header
- give derived applets unique identifiers for MeshModule and OSThread logging

* hotfix: autoshow always uses FAST update
In future, it *will* often use FAST, but this will be controlled by a WindowManager component which has not yet been written.
Hotfixed, in case anybody is attempting to use this development version on their deployed devices.

* refactor: bringToForeground no longer requests FAST update
In situations where an applet has moved to foreground because of user input, requestUpdate can be manually called, to upgrade to FAST refresh.
More permanent solution for #23e1dfc

* refactor: extract string storage from ThreadedMessageApplet
Separates the code responsible for storing the limited message history, which was previously part of the ThreadedMessageApplet.
We're now also using this code to store the "most recent message". Previously, this was stored in the `InkHUD::settings` struct, which was much less space-efficient.
We're also now storing the latest DM, laying the foundation for an applet to display only DMs, which will complement the threaded message applet.

* fix: text wrapping
Attempts to fix a disparity between `Applet::printWrapped` and `Applet::getWrappedTextHeight`, which would occasionally cause a ThreadedMessageApplet message to render "too short", overlapping other text.

* fix: purge old constructor
This one slipped through the last commit..

* feat: DM Applet
Useful in combination with the ThreadedMessageApplets, which don't show DMs

* fix: applets shouldn't handle events while deactivated
Only one or two applets were actually doing this, but I'm making a habit of having all applets return early from their event handling methods (as good practice), even if those methods are disabled elsewhere (e.g. not observing observable, return false from wantPacket)

* refactor: allow requesting update without requesting autoshow
Some applets may want to redraw, if they are displayed, but not feel the information is worth being brought to foreground for. Example: ActiveNodesApplet, when purging old nodes from list.

* feat: custom "Recently Active" duration
Allows users to tailor how long nodes will appear in the "Recents" applets, to suit the activity level of their mesh.

* refactor: rename some applets

* fix: autoshow

* fix: getWrappedTextHeight
Remove the "simulate" option from printWrapped; too hard to keep inline with genuine printing (because of AdafruitGFX Fonts' xAdvance, mabye?). Instead of simulating, we printWrapped as normal, and discard pixel output by setting crop. Both methods are similarly inefficient, apparently.

* fix: text wrapping in ThreadedMessageApplet
Wrong arguments were passed to Applet::printWrapped

* feat: notifications for text messages
Only shown if current applet does not already display the same info. Autoshow takes priority over notifications, if both would be used to display the same info.

* feat: optimize FAST vs FULL updates
New UpdateMediator class counts the number of each update type, and suggets which one to use, if the code doesn't already have an explicit prefence. Also performs "maintenance refreshes" unprovoked if display is not given an opportunity to before a FULL refresh through organic use.

* chore: update todo list

* fix: rare lock-up of buttons

* refactor: backlight
Replaces the initial proof-of-concept frontlight code for T-Echo
Presses less than 5 seconds momentarily illuminate the display
Presses longer than 5 seconds latch the light, requiring another tap to disable
If user has previously removed the T-Echo's capacitive touch button (some DIY projects), the light is controlled by the on-screen menu. This fallback is used by all T-Echo devices, until a press of the capacitive touch button is detected.

* feat: change tile with aux button
Applied to VM-E290.
Working as is, but a refactor of WindowManager::render is expected shortly, which will also tidy code from this push.

* fix: specify out-of-the-box tile assignments
Prevents placeholder applet showing on initial boot, for devices which use a mult-tile layout by default (VM-E290)

* fix: verify settings version when loading

* fix: wrong settings version

* refactor: remove unimplemented argument from requestUpdate
Specified whether or not to update "async", however the implementation was slightly broken, Applet::requestUpdate is only handled next time WindowManager::runOnce is called. This didn't allow code to actually await an update, which was misleading.

* refactor: renaming
Applet::render becomes Applet::onRender.
Tile::displayedApplet becomes Tile::assignedApplet.
New onRender method name allows us to move some of the pre and post render code from WindowManager into new Applet::render method, which will call onRender for us.

* refactor: rendering
Bit of a tidy-up. No intended change in behavior.

* fix: optimize refresh times
Shorter wait between retrying update if display was previously busy.
Set anticipated update durations closer to observed values. No signifacant performance increase, but does decrease the amount of polling required.

* feat: blocking update for E-Ink
Option to wait for display update to complete before proceeding. Important when shutting down the device.

* refactor: allow system applets to lock rendering
Temporarily prevents other applets from rendering.

* feat: boot and shutdown screens

* feat: BluetoothStatus
Adds a meshtastic::Status object which exposes the state of the Bluetooth connection. Intends to allow decoupling of UI code.

* feat: Bluetooth pairing screen

* fix: InkHUD defaults not honored

* fix: random Bluetooth pin for NicheGraphics UIs

* chore: button interrupts tested

* fix: emoji reactions show as blank messages

* fix: autoshow and notification triggered by outgoing message

* feat: save InkHUD data before reboot
Implemented with a new Observable. Previously, config and a few recent messages were saved on shutdown. These were lost if the device rebooted, for example when firmware settings were changed by a client. Now, the InkHUD config and recent messages saved on reboot, the same as during an intentional shutdown.

* feat: imperial distances
Controlled by the config.display.units setting

* fix: hide features which are not yet implemented

* refactor: faster rendering
Previously, only tiles which requested update were re-rendered. Affected tiles had their region blanked before render, pixel by pixel. Benchmarking revealed that it is significantly faster to memset the framebuffer and redraw all tiles.

* refactor: tile ownership
Tiles and Applets now maintain a reciprocal link, which is enforced by asserts. Less confusing than the old situation, where an applet and a tile may disagree on their relationship. Empty tiles are now identified by a nullptr *Applet, instead of by having the placeholderApplet assigned.

* fix: notifications and battery when menu open
Do render notifications in front of menu; don't render battery icon in front of menu.

* fix: simpler defaults
Don't expose new users to multiplexed applets straight away: make them enable the feature for themselves.

* fix: Inputs::TwoButton interrupts, when only one button in use

* fix: ensure display update is complete when ESP32 enters light sleep
Many panels power down automatically, but some require active intervention from us. If light sleep (ESP32) occurs during a display update, these panels could potentially remain powered on, applying voltage the pixels for an extended period of time, and potentially damaging the display.

* fix: honor per-variant user tile limit
Set as the default value for InkHUD::settings.userTiles.maxCount in nicheGraphics.h

* feat: initial InkHUD support for Wireless Paper v1.1 and VM-E213

* refactor: Heard and Recents Applets
Tidier code, significant speed boost. Possibly no noticable change in responsiveness, but rendering now spends much less time blocking execution, which is important for correction functioning of the other firmware components.

* refactor: use a common pio base config
Easier to make any future PlatformIO config changes

* feat: tips
Show information that we think the user might find helpful. Some info shown first boot only. Other info shown when / if relevant.

* fix: text wrapping for '\n'
Previously, the newline was honored, but the adojining word was not printed.

* Decouple ButtonThread from sleep.cpp
Reorganize sleep observables. Don't call ButtonThread methods inside doLightSleep. Instead, handle in class with new lightsleep Observables.

* feat: BluetoothStatus
Adds a meshtastic::Status object which exposes the state of the Bluetooth connection. Intends to allow decoupling of UI code.

* feat: observable for reboot

* refactor: Heltec VM-E290 installDefaultConfig

* fix: random Bluetooth pin for NicheGraphics UIs

* update device-ui: fix touch/crash issue while light sleep

* Collect inkhud

* fix: InkHUD shouldn't nag about timezone (#6040)

* Guard eink drivers w/ MESHTASTIC_INCLUDE_NICHE_GRAPHICS

* Case sensitive perhaps?

* More case-sensitivity instances

* Moar

* RTC

* Yet another case issue!

* Sigh...

* MUI: BT programming mode (#6046)

* allow BT connection with disabled MUI

* Update device-ui

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* MUI: fix nag timeout, disable BT programming mode for native (#6052)

* allow BT connection with disabled MUI

* Update device-ui

* MUI: fix nag timeout default and remove programming mode for native

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* remove debuglog leftover

* Wireless Paper: remove stray board_level = extra (#6060)

Makes sure the InkHUD version gets build into the release zip

* Fixed persistence stragglers from NodeDB / Device State divorce (#6059)

* Increase `MAX_THREADS` for InkHUD variants with WiFi (#6064)

* Licensed usage compliance (#6047)

* Prevent psk and legacy admin channel on licensed mode

* Move it

* Consolidate warning strings

* More holes

* Device UI submodule bump

* Prevent licensed users from rebroadcasting unlicensed traffic (#6068)

* Prevent licensed users from rebroadcasting unlicensed traffic

* Added method and enum to make user license status more clear

* MUI: move UI initialization out of main.cpp and adding lightsleep observer + mutex (#6078)

* added device-ui to lightSleep observers for handling graceful sleep; refactoring main.cpp

* bump lib version

* Update device-ui

* unPhone TFT: include into build, enable SD card, increase PSRAM (#6082)

* unPhone-tft: include into build, enable SD card, increase assigned PSRAM

* lib update

* Backup / migrate pub private keys when upgrading to new files in 2.6 (#6096)

* Save a backup of pub/private keys before factory reset

* Fix licensed mode warning

* Unlock spi on else file doesn't exist

* Update device-ui

* Update protos and device-ui

* [create-pull-request] automated change (#6129)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Proto

* [create-pull-request] automated change (#6131)

* Proto update for backup

* [create-pull-request] automated change (#6133)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Update protobufs

* Space

* [create-pull-request] automated change (#6144)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Protos

* [create-pull-request] automated change (#6152)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Updeet

* device-ui lib update

* fix channel OK button

* device-lib update: fix settings panel -> no scrolling

* device-ui lib: last minute update

* defined(SENSECAP_INDICATOR)

* MUI hot-fix pub/priv keys

* MUI hot-fix username dialog

* MUI: BT programming mode button

* Update protobufs

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com>
Co-authored-by: GUVWAF <thijs@havinga.eu>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: mverch67 <manuel.verch@gmx.de>
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Jason Murray <15822260+scruplelesswizard@users.noreply.github.com>
Co-authored-by: Jason Murray <jason@chaosaffe.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: virgil <virgil.wang.cj@gmail.com>
Co-authored-by: Mark Trevor Birss <markbirss@gmail.com>
Co-authored-by: Kalle Lilja <15094562+ThatKalle@users.noreply.github.com>
Co-authored-by: rcarteraz <robert.l.carter2@gmail.com>
2025-03-01 06:18:33 -06:00
github-actions[bot]
088fce7d11 [create-pull-request] automated change (#6181)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-03-01 05:09:59 -06:00
github-actions[bot]
b46bf16385 Upgrade trunk (#6160)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-02-27 13:49:59 +08:00
Rick Mark
1c827f5512 DevContainers: Include meshtasticd dependencies (#5699)
* Include meshtasticd dependencies

* Remove device-ui checkin

* Add trunk rules matching other Dockerfiles

---------

Co-authored-by: vidplace7 <vidplace7@gmail.com>
2025-02-27 13:01:34 +08:00
265 changed files with 16602 additions and 829 deletions

View File

@@ -1,9 +1,10 @@
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12
USER root
# trunk-ignore(terrascan/AC_DOCKER_0002): Known terrascan issue
# trunk-ignore(hadolint/DL3008): Use latest version of packages
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
ca-certificates \
@@ -27,9 +28,15 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
hwdata \
gpg \
gnupg2 \
libusb-1.0-0-dev \
libuv1-dev \
libi2c-dev \
libxcb-xkb-dev \
libxkbcommon-dev \
libinput-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pipx install platformio==6.1.15
RUN pipx install platformio
COPY 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules

View File

@@ -1,3 +1,6 @@
#!/usr/bin/env sh
git submodule update --init
pip install --no-cache-dir setuptools
pipx install esptool

5
.gitattributes vendored
View File

@@ -1,4 +1,5 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
*.cmd text eol=crlf
*.bat text eol=crlf
*.ps1 text eol=crlf
*.{sh,[sS][hH]} text eol=lf

View File

@@ -20,7 +20,7 @@ runs:
shell: bash
run: |
sudo apt-get -y update --fix-missing
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev libuv1-dev lsb-release
- name: Setup Python
uses: actions/setup-python@v5

View File

@@ -11,4 +11,4 @@ runs:
- name: Install libs needed for native build
shell: bash
run: |
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev

View File

@@ -19,6 +19,8 @@ updates:
interval: daily
time: "05:00"
timezone: US/Pacific
ignore:
- dependency-name: protobufs
- package-ecosystem: github-actions
directory: /.github/workflows
schedule:

View File

@@ -4,7 +4,7 @@ on:
workflow_call:
secrets:
PPA_GPG_PRIVATE_KEY:
required: true
required: false
inputs:
series:
description: Ubuntu/Debian series to target

View File

@@ -135,10 +135,11 @@ jobs:
build_location: local
secrets: inherit
package-pio-deps-native:
package-pio-deps-native-tft:
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native
pio_env: native-tft
secrets: inherit
test-native:
@@ -288,7 +289,7 @@ jobs:
needs:
- gather-artifacts
- build-debian-src
- package-pio-deps-native
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -324,18 +325,18 @@ jobs:
merge-multiple: true
path: ./output/debian-src
- name: Download native pio deps
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v4
with:
pattern: platformio-deps-native-${{ steps.version.outputs.long }}
pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }}
merge-multiple: true
path: ./output/pio-deps-native
path: ./output/pio-deps-native-tft
- name: Zip linux sources
working-directory: output
run: |
zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src
zip -9 -r ./platformio-deps-native-${{ steps.version.outputs.long }}.zip ./pio-deps-native
zip -9 -r ./platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip ./pio-deps-native-tft
# For diagnostics
- name: Display structure of downloaded files
@@ -344,32 +345,10 @@ jobs:
- name: Add linux sources to release
run: |
gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip
gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-${{ steps.version.outputs.long }}.zip
gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Bump version.properties
run: >-
bin/bump_version.py
- name: Ensure debian deps are installed
shell: bash
run: |
sudo apt-get update -y --fix-missing
sudo apt-get install -y devscripts
- name: Update debian changelog
run: >-
debian/ci_changelog.sh
- name: Create version.properties pull request
uses: peter-evans/create-pull-request@v7
with:
title: Bump version.properties
add-paths: |
version.properties
debian/changelog
release-firmware:
strategy:
fail-fast: false

View File

@@ -43,3 +43,49 @@ jobs:
copr_project: |-
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
secrets: inherit
# Create a PR to bump version when a release is Published
bump-version:
if: ${{ github.event.release.published }}
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
id: version
env:
BUILD_LOCATION: local
- name: Bump version.properties
run: >-
bin/bump_version.py
- name: Ensure debian deps are installed
shell: bash
run: |
sudo apt-get update -y --fix-missing
sudo apt-get install -y devscripts
- name: Update debian changelog
run: >-
debian/ci_changelog.sh
- name: Create version.properties pull request
uses: peter-evans/create-pull-request@v7
with:
title: Bump version.properties
add-paths: |
version.properties
debian/changelog

View File

@@ -6,11 +6,14 @@ on:
schedule:
- cron: 0 1 * * 6
permissions: read-all
permissions:
actions: read
contents: read
security-events: write
jobs:
semgrep-full:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
container:
image: semgrep/semgrep

View File

@@ -18,5 +18,6 @@ jobs:
- name: Stale PR+Issues
uses: actions/stale@v9.1.0
with:
days-before-stale: 45
exempt-issue-labels: pinned,3.0
exempt-pr-labels: pinned,3.0

View File

@@ -143,7 +143,7 @@ jobs:
merge-multiple: true
- name: Test Report
uses: dorny/test-reporter@v1.9.1
uses: dorny/test-reporter@v2.0.0
with:
name: PlatformIO Tests
path: testreport.xml

View File

@@ -1,6 +1,6 @@
version: 0.1
cli:
version: 1.22.10
version: 1.22.11
plugins:
sources:
- id: trunk
@@ -8,16 +8,16 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- prettier@3.5.2
- trufflehog@3.88.12
- yamllint@1.35.1
- prettier@3.5.3
- trufflehog@3.88.17
- yamllint@1.36.0
- bandit@1.8.3
- checkov@3.2.373
- checkov@3.2.386
- terrascan@1.19.9
- trivy@0.59.1
- trivy@0.60.0
- taplo@0.9.3
- ruff@0.9.7
- isort@6.0.0
- ruff@0.10.0
- isort@6.0.1
- markdownlint@0.44.0
- oxipng@9.1.4
- svgo@3.3.2

View File

@@ -7,5 +7,8 @@
"cmake.configureOnOpen": false,
"[cpp]": {
"editor.defaultFormatter": "trunk.io"
},
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell"
}
}

View File

@@ -13,7 +13,7 @@ ENV TZ=Etc/UTC
ENV PIP_ROOT_USER_ACTION=ignore
RUN apt-get update && apt-get install --no-install-recommends -y \
wget g++ zip git ca-certificates \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev pkg-config \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir -U platformio \
@@ -38,7 +38,7 @@ ENV TZ=Etc/UTC
USER root
RUN apt-get update && apt-get --no-install-recommends -y install \
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 \
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev liborcania2.3 libulfius2.7 libssl3 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \

View File

@@ -9,7 +9,7 @@ FROM python:3.13-alpine3.21 AS builder
ENV PIP_ROOT_USER_ACTION=ignore
RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev openssl-dev pkgconf argp-standalone \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
&& rm -rf /var/cache/apk/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware
@@ -32,7 +32,7 @@ FROM alpine:3.21
USER root
RUN apk --no-cache add \
libstdc++ libgpiod yaml-cpp libusb i2c-tools \
libstdc++ libgpiod yaml-cpp libusb i2c-tools libuv \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \

View File

@@ -37,6 +37,7 @@ build_flags =
-DLIBPAX_ARDUINO
-DLIBPAX_WIFI
-DLIBPAX_BLE
-DHAS_UDP_MULTICAST=1
;-DDEBUG_HEAP
lib_deps =

View File

@@ -1,6 +1,6 @@
; The Portduino based 'native' environment. Currently supported on Linux targets with real LoRa hardware (or simulated).
[portduino_base]
platform = https://github.com/meshtastic/platform-native.git#562d189828f09fbf4c4093b3c0104bae9d8e9ff9
platform = https://github.com/Jorropo/platform-native.git#17fa89daec4402af491512f75278a7fec8a5818c
framework = arduino
build_src_filter =
@@ -34,10 +34,12 @@ build_flags =
-Isrc/platform/portduino
-DRADIOLIB_EEPROM_UNSUPPORTED
-DPORTDUINO_LINUX_HARDWARE
-DHAS_UDP_MULTICAST
-lpthread
-lstdc++fs
-lbluetooth
-lgpiod
-lyaml-cpp
-li2c
-luv
-std=c++17

View File

@@ -1,8 +1,8 @@
; Common settings for rp2040 Processor based targets
[rp2040_base]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#19e30129fb1428b823be585c787dcb4ac0d9014c ; For arduino-pico >=4.2.1
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 ; For arduino-pico >= 4.4.3
extends = arduino_base
platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#6024e9a7e82a72e38dd90f42029ba3748835eb2e ; 4.3.0 with fix MDNS
platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#4.4.3
board_build.core = earlephilhower
board_build.filesystem_size = 0.5m
@@ -18,6 +18,7 @@ build_src_filter =
lib_ignore =
BluetoothOTA
lvgl
lib_deps =
${arduino_base.lib_deps}

View File

@@ -35,11 +35,11 @@ cp $SRCBIN $OUTDIR/$basename-update.bin
echo "Building Filesystem for ESP32 targets"
pio run --environment $1 -t buildfs
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$VERSION.bin
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
# Remove webserver files from the filesystem and rebuild
ls -l data/static # Diagnostic list of files
rm -rf data/static
pio run --environment $1 -t buildfs
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$VERSION.bin
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
cp bin/device-install.* $OUTDIR
cp bin/device-update.* $OUTDIR
cp bin/device-update.* $OUTDIR

View File

@@ -24,7 +24,7 @@ mkdir -p $OUTDIR/
rm -r $OUTDIR/* || true
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
platformio pkg update --environment native || platformioFailed
pio pkg update --environment native || platformioFailed
pio run --environment native || platformioFailed
cp .pio/build/native/program "$OUTDIR/meshtasticd_linux_$(uname -m)"
cp bin/native-install.* $OUTDIR

View File

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

View File

@@ -0,0 +1,49 @@
Lora:
### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx
### https://github.com/markbirss/rock-2f
### https://github.com/markbirss/lora-starter-edition-sx1262-i2c
### https://github.com/radxa-pkg/radxa-overlays/blob/main/arch/arm64/boot/dts/rockchip/overlays/rk3528-spi0-cs1-spidev.dts
### Require install of https://github.com/radxa-pkg/radxa-overlays and rk3528-spi0-cs1-spidev.dtbo copied to /boot/dtb/rockchip/overlay and enabled
### in /boot/armbianEnv.txt - overlays=rk3528-spi0-cs1-spidev
### The Radxa Rock 2F employs multiple gpio chips.
### Each gpio pin must be unique, but can be assigned to a specific gpio chip and line.
### In case solely a no. is given, the default gpio chip and pin == line will be employed.
###
Module: sx1262 # Radxa Rock 2F + Starter Edition SX1262 HAT by Mark Birss
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: 1.8
spidev: spidev0.1
CS: # NSS PIN_24 -> chip 4, line 14
pin: 24
gpiochip: 4
line: 14
SCK: # SCK PIN_23 -> chip 4, line 12
pin: 23
gpiochip: 4
line: 12
Busy: # BUSY PIN_7 -> chip 4, line 6
pin: 7
gpiochip: 4
line: 6
MOSI: # MOSI PIN_19 -> chip 4, line 10
pin: 19
gpiochip: 4
line: 10
MISO: # MISO PIN_21 -> chip 4, line 11
pin: 21
gpiochip: 4
line: 11
Reset: # NRST PIN_12 -> chip 1, line 13
pin: 12
gpiochip: 1
line: 13
IRQ: # DIO1 PIN_15 -> chip 4, line 22
pin: 15
gpiochip: 4
line: 22
# RXen: # RXEN PIN_22 -> chip 3!, line 17
# pin: 22
# gpiochip: 3
# line: 17
# TXen: RADIOLIB_NC # TXEN no PIN, no line, fallback to default gpio chip

View File

@@ -0,0 +1,10 @@
# https://www.waveshare.com/core1262-868m.htm
# https://github.com/markbirss/lora-starter-edition-sx1262-i2c
Lora:
Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 8
IRQ: 22
Busy: 4
Reset: 18

View File

@@ -0,0 +1,10 @@
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter
Lora:
Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 21
IRQ: 16
Busy: 20
Reset: 18

View File

@@ -1,72 +1,296 @@
@ECHO OFF
SETLOCAL EnableDelayedExpansion
TITLE Meshtastic device-install
set PYTHON=python
set WEB_APP=0
SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0"
SET "PYTHON="
SET "WEB_APP=0"
SET "TFT_BUILD=0"
SET "TFT8=0"
SET "TFT16=0"
SET "ESPTOOL_BAUD=115200"
SET "ESPTOOL_CMD="
SET "LOGCOUNTER=0"
:: Determine the correct esptool command to use
where esptool >nul 2>&1
if %ERRORLEVEL% EQU 0 (
set "ESPTOOL_CMD=esptool"
) else (
set "ESPTOOL_CMD=%PYTHON% -m esptool"
)
GOTO getopts
:help
ECHO Flash image file to device, but first erasing and writing system information.
ECHO.
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web)
ECHO.
ECHO Options:
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
ECHO The file must be located in this current directory.
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
ECHO If supplied the script will use python.
ECHO If not supplied the script will try to find esptool in Path.
ECHO --web Enable WebUI. (default: false)
ECHO.
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web
GOTO eof
goto GETOPTS
:HELP
echo Usage: %~nx0 [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME^|FILENAME] [--web]
echo Flash image file to device, but first erasing and writing system information
echo.
echo -h Display this help and exit
echo -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerrous).
echo -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: %PYTHON%)
echo -f FILENAME The .bin file to flash. Custom to your device type and region.
echo --web Flash WEB APP.
goto EOF
:version
ECHO %SCRIPT_NAME% [Version 2.6.0]
ECHO Meshtastic
GOTO eof
:GETOPTS
if /I "%1"=="-h" goto HELP
if /I "%1"=="--help" goto HELP
if /I "%1"=="-F" set "FILENAME=%2" & SHIFT
if /I "%1"=="-p" set ESPTOOL_PORT=%2 & SHIFT
if /I "%1"=="-P" set PYTHON=%2 & SHIFT
if /I "%1"=="--web" set WEB_APP=1 & SHIFT
:getopts
IF "%~1"=="" GOTO endopts
IF /I "%~1"=="-?" GOTO help
IF /I "%~1"=="-h" GOTO help
IF /I "%~1"=="--help" GOTO help
IF /I "%~1"=="-v" GOTO version
IF /I "%~1"=="--version" GOTO version
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
IF /I "%~1"=="--web" SET "WEB_APP=1"
SHIFT
IF NOT "__%1__"=="____" goto GETOPTS
GOTO getopts
:endopts
IF "__%FILENAME%__" == "____" (
echo "Missing FILENAME"
goto HELP
)
IF EXIST %FILENAME% IF x%FILENAME:update=%==x%FILENAME% (
echo Trying to flash update %FILENAME%, but first erasing and writing system information"
%ESPTOOL_CMD% --baud 115200 erase_flash
%ESPTOOL_CMD% --baud 115200 write_flash 0x00 %FILENAME%
@REM Account for S3 and C3 board's different OTA partition
IF x%FILENAME:s3=%==x%FILENAME% IF x%FILENAME:v3=%==x%FILENAME% IF x%FILENAME:t-deck=%==x%FILENAME% IF x%FILENAME:wireless-paper=%==x%FILENAME% IF x%FILENAME:wireless-tracker=%==x%FILENAME% IF x%FILENAME:station-g2=%==x%FILENAME% IF x%FILENAME:unphone=%==x%FILENAME% (
IF x%FILENAME:esp32c3=%==x%FILENAME% (
%ESPTOOL_CMD% --baud 115200 write_flash 0x260000 bleota.bin
) else (
%ESPTOOL_CMD% --baud 115200 write_flash 0x260000 bleota-c3.bin
)
) else (
%ESPTOOL_CMD% --baud 115200 write_flash 0x260000 bleota-s3.bin
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
IF "__!FILENAME!__"=="____" (
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
GOTO help
) ELSE (
CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!"
IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" (
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
GOTO help
)
IF %WEB_APP%==1 (
for %%f in (littlefswebui-*.bin) do (
%ESPTOOL_CMD% --baud 115200 write_flash 0x300000 %%f
)
) else (
for %%f in (littlefs-*.bin) do (
%ESPTOOL_CMD% --baud 115200 write_flash 0x300000 %%f
)
IF "__!FILENAME:firmware-=!__"=="__!FILENAME!__" (
CALL :LOG_MESSAGE ERROR "Filename must be a firmware-* file."
GOTO help
)
) else (
echo "Invalid file: %FILENAME%"
goto HELP
) else (
echo "Invalid file: %FILENAME%"
goto HELP
@REM Remove ".\" or "./" file prefix if present.
SET "FILENAME=!FILENAME:.\=!"
SET "FILENAME=!FILENAME:./=!"
)
:EOF
CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..."
IF NOT EXIST !FILENAME! (
CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating."
GOTO eof
)
IF NOT "!FILENAME:update=!"=="!FILENAME!" (
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
CALL :LOG_MESSAGE INFO "Use script device-update.bat to flash update !FILENAME!."
GOTO eof
) ELSE (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
)
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
IF NOT "__%PYTHON%__"=="____" (
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
) ELSE (
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
WHERE esptool >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
@REM WHERE exits with code 0 if esptool is found.
SET "ESPTOOL_CMD=esptool"
) ELSE (
SET "ESPTOOL_CMD=python -m esptool"
CALL :RESET_ERROR
)
)
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
!ESPTOOL_CMD! >nul 2>&1
IF %ERRORLEVEL% GTR 2 (
@REM esptool exits with code 1 if help is displayed.
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
EXIT /B 1
GOTO eof
)
IF %DEBUG% EQU 1 (
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
)
CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!"
IF "__!ESPTOOL_PORT!__" == "____" (
CALL :LOG_MESSAGE WARN "Using esptool port: UNSET."
) ELSE (
CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!."
)
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
IF %WEB_APP% EQU 1 (
CALL :LOG_MESSAGE ERROR "Cannot enable WebUI (--web) and MUI." & GOTO eof
)
SET "TFT_BUILD=1"
GOTO tft
) ELSE (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!"
GOTO no_tft
)
:tft
SET "TFT8MB=picomputer-s3 unphone seeed-sensecap-indicator"
FOR %%a IN (%TFT8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %TFT8MB%.
SET "TFT8=1"
GOTO end_loop_tft8mb
)
)
:end_loop_tft8mb
SET "TFT16MB=t-deck"
FOR %%a IN (%TFT16MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %TFT16MB%.
SET "TFT16=1"
GOTO end_loop_tft16mb
)
)
:end_loop_tft16mb
IF %TFT8% EQU 1 CALL :LOG_MESSAGE INFO "tft and MUI 8mb selected."
IF %TFT16% EQU 1 CALL :LOG_MESSAGE INFO "tft and MUI 16mb selected."
:no_tft
@REM Extract BASENAME from %FILENAME% for later use.
SET "BASENAME=!FILENAME:firmware-=!"
CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!"
@REM Account for S3 and C3 board's different OTA partition.
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
FOR %%a IN (%S3%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %S3%.
SET "OTA_FILENAME=bleota-s3.bin"
GOTO :end_loop_s3
)
)
SET "C3=esp32c3"
FOR %%a IN (%C3%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %C3%.
SET "OTA_FILENAME=bleota-c3.bin"
GOTO :end_loop_c3
)
)
@REM Everything else
SET "OTA_FILENAME=bleota.bin"
:end_loop_s3
:end_loop_c3
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Check if (--web) is enabled and prefix BASENAME with "littlefswebui-" else "littlefs-".
IF %WEB_APP% EQU 1 (
CALL :LOG_MESSAGE INFO "WebUI selected."
SET "SPIFFS_FILENAME=littlefswebui-%BASENAME%"
) ELSE (
SET "SPIFFS_FILENAME=littlefs-%BASENAME%"
)
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
@REM Default offsets.
@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
SET "OTA_OFFSET=0x260000"
SET "SPIFFS_OFFSET=0x300000"
@REM Offsets for MUI 8mb.
IF %TFT8% EQU 1 IF %TFT_BUILD% EQU 1 (
SET "OTA_OFFSET=0x340000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for MUI 16mb.
IF %TFT16% EQU 1 IF %TFT_BUILD% EQU 1 (
SET "OTA_OFFSET=0x650000"
SET "SPIFFS_OFFSET=0xc90000"
)
CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!"
@REM Ensure target files exist before flashing operations.
IF NOT EXIST !FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
IF NOT EXIST !OTA_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!OTA_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!SPIFFS_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof
@REM Flashing operations.
CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..."
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof
CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..."
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..."
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
CALL :LOG_MESSAGE INFO "Script complete!."
:eof
ENDLOCAL
EXIT /B %ERRORLEVEL%
:RUN_ESPTOOL
@REM Subroutine used to run ESPTOOL_CMD with arguments.
@REM Also handles %ERRORLEVEL%.
@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
@REM.
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
CALL :RESET_ERROR
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
IF %ERRORLEVEL% NEQ 0 (
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
EXIT /B %ERRORLEVEL%
)
GOTO :eof
:LOG_MESSAGE
@REM Subroutine used to print log messages in four different levels.
@REM DEBUG messages only get printed if [-d] flag is passed to script.
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
@REM.
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
SET /A LOGCOUNTER=LOGCOUNTER+1
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
GOTO :eof
:GET_TIMESTAMP
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
@REM CALL :GET_TIMESTAMP
@REM.
@REM Updates: !TIMESTAMP!
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
SET "HH=%%a"
SET "MM=%%b"
SET "ss=%%c"
)
SET "TIMESTAMP=!HH!:!MM!:!ss!"
GOTO :eof
:RESET_ERROR
@REM Subroutine to reset %ERRORLEVEL% to 0.
@REM CALL :RESET_ERROR
@REM.
@REM Updates: %ERRORLEVEL%
EXIT /B 0
GOTO :eof

View File

@@ -1,18 +1,21 @@
#!/bin/sh
#!/bin/bash
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
WEB_APP=false
TFT8=false
TFT16=false
TFT_BUILD=false
# Determine the correct esptool command to use
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
ESPTOOL_CMD="$PYTHON -m esptool"
ESPTOOL_CMD="$PYTHON -m esptool"
elif command -v esptool >/dev/null 2>&1; then
ESPTOOL_CMD="esptool"
ESPTOOL_CMD="esptool"
elif command -v esptool.py >/dev/null 2>&1; then
ESPTOOL_CMD="esptool.py"
ESPTOOL_CMD="esptool.py"
else
echo "Error: esptool not found"
exit 1
echo "Error: esptool not found"
exit 1
fi
set -e
@@ -20,76 +23,139 @@ set -e
# Usage info
show_help() {
cat <<EOF
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--web]
Flash image file to device, but first erasing and writing system information"
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--web]
Flash image file to device, but first erasing and writing system information.
-h Display this help and exit
-h Display this help and exit.
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
-f FILENAME The .bin file to flash. Custom to your device type and region.
--web Flash WEB APP.
-f FILENAME The firmware .bin file to flash. Custom to your device type and region.
--web Enable WebUI. (Default: false)
EOF
}
# Preprocess long options like --web
for arg in "$@"; do
case "$arg" in
--web)
WEB_APP=true
shift # Remove this argument from the list
;;
esac
done
while getopts ":hp:P:f:" opt; do
case "${opt}" in
h)
# Parse arguments using a single while loop
while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
show_help
exit 0
;;
p)
export ESPTOOL_PORT=${OPTARG}
-p)
ESPTOOL_PORT="$2"
shift # Shift past the option argument
;;
P)
PYTHON=${OPTARG}
-P)
PYTHON="$2"
shift
;;
f)
FILENAME=${OPTARG}
-f)
FILENAME="$2"
shift
;;
--web)
WEB_APP=true
;;
--) # Stop parsing options
shift
break
;;
*)
echo "Invalid flag."
show_help >&2
echo "Unknown argument: $1" >&2
exit 1
;;
esac
shift # Move to the next argument
done
shift "$((OPTIND - 1))"
[ -z "$FILENAME" -a -n "$1" ] && {
FILENAME=$1
shift
}
if [[ $FILENAME != firmware-* ]]; then
echo "Filename must be a firmware-* file."
exit 1
fi
# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
if [[ ${FILENAME//-tft-/} != "$FILENAME" ]]; then
TFT_BUILD=true
if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then
echo "Cannot enable WebUI (--web) and MUI."
exit 1
fi
if [[ $FILENAME == *"picomputer-s3"* || $FILENAME == *"unphone"* || $FILENAME == *"seeed-sensecap-indicator"* ]]; then
TFT8=true
fi
if [[ $FILENAME == *"t-deck"* ]]; then
TFT16=true
fi
fi
# Extract BASENAME from %FILENAME% for later use.
BASENAME="${FILENAME/firmware-/}"
if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
$ESPTOOL_CMD erase_flash
$ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
# Default littlefs* offset (--web).
OFFSET=0x300000
# Default OTA Offset
OTA_OFFSET=0x260000
# littlefs* offset for MUI 8mb and OTA OFFSET.
if [ "$TFT8" = true ] && [ "$TFT_BUILD" = true ]; then
OFFSET=0x670000
OTA_OFFSET=0x340000
fi
# littlefs* offset for MUI 16mb and OTA OFFSET.
if [ "$TFT16" = true ] && [ "$TFT_BUILD" = true ]; then
OFFSET=0xc90000
OTA_OFFSET=0x650000
fi
# Account for S3 board's different OTA partition
if [ -n "${FILENAME##*"s3"*}" ] && [ -n "${FILENAME##*"-v3"*}" ] && [ -n "${FILENAME##*"t-deck"*}" ] && [ -n "${FILENAME##*"wireless-paper"*}" ] && [ -n "${FILENAME##*"wireless-tracker"*}" ] && [ -n "${FILENAME##*"station-g2"*}" ] && [ -n "${FILENAME##*"unphone"*}" ]; then
if [ -n "${FILENAME##*"esp32c3"*}" ]; then
$ESPTOOL_CMD write_flash 0x260000 bleota.bin
OTAFILE=bleota.bin
else
$ESPTOOL_CMD write_flash 0x260000 bleota-c3.bin
OTAFILE=bleota-c3.bin
fi
else
$ESPTOOL_CMD write_flash 0x260000 bleota-s3.bin
OTAFILE=bleota-s3.bin
fi
# Check if WEB_APP (--web) is enabled and add "littlefswebui-" to BASENAME else "littlefs-".
if [ "$WEB_APP" = true ]; then
$ESPTOOL_CMD write_flash 0x300000 littlefswebui-*.bin
SPIFFSFILE=littlefswebui-${BASENAME}
else
$ESPTOOL_CMD write_flash 0x300000 littlefs-*.bin
SPIFFSFILE=littlefs-${BASENAME}
fi
if [[ ! -f $FILENAME ]]; then
echo "Error: file ${FILENAME} wasn't found. Terminating."
exit 1
fi
if [[ ! -f $OTAFILE ]]; then
echo "Error: file ${OTAFILE} wasn't found. Terminating."
exit 1
fi
if [[ ! -f $SPIFFSFILE ]]; then
echo "Error: file ${SPIFFSFILE} wasn't found. Terminating."
exit 1
fi
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
$ESPTOOL_CMD erase_flash
$ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
$ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
else
show_help
echo "Invalid file: ${FILENAME}"

112
bin/device-install_test.ps1 Normal file
View File

@@ -0,0 +1,112 @@
<#
.SYNOPSIS
Unit-test for .\device-install.bat.
.DESCRIPTION
This script performs a positive unit-test on .\device-install.bat by creating the expected .bin
files for a device followed by running the .bat script without flashing the firmware (--debug).
If any errors are hit they are presented in the standard output. Investigate accordingly.
This script needs to be placed in the same directory as .\device-install.bat.
.EXAMPLE
.\device-install_test.ps1
.EXAMPLE
.\device-install_test.ps1 -Verbose
.LINK
.\device-install.bat --help
#>
[CmdletBinding()]
param()
function New-EmptyFile() {
[CmdletBinding()]
param (
[Parameter(Position = 0, Mandatory = $true)]
# Specifies the file name.
[string]$FileName,
[Parameter(Position = 1)]
# Specifies the target path. (Get-Location).Path is the default.
[string]$Directory = (Get-Location).Path
)
$filePath = Join-Path -Path $Directory -ChildPath $FileName
Write-Verbose -Message "Create empty test file if it doesn't exist: $($FileName)"
New-Item -Path "$filePath" -ItemType File -ErrorAction SilentlyContinue | Out-Null
}
function Remove-EmptyFile() {
[CmdletBinding()]
param (
[Parameter(Position = 0, Mandatory = $true)]
# Specifies the file name.
[string]$FileName,
[Parameter(Position = 1)]
# Specifies the target path. (Get-Location).Path is the default.
[string]$Directory = (Get-Location).Path
)
$filePath = Join-Path -Path $Directory -ChildPath $FileName
Write-Verbose -Message "Deleted empty test file: $($FileName)"
Remove-Item -Path "$filePath" | Out-Null
}
$TestCases = New-Object -TypeName PSObject -Property @{
# Use this PSObject to define testcases according to this syntax:
# "testname" = @("firmware-testname","bleota","littlefs-testname","args")
"t-deck" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-2.6.0.0b106d4.bin", "")
"t-deck_web" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-t-deck-2.6.0.0b106d4.bin", "--web")
"t-deck-tft" = @("firmware-t-deck-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-tft-2.6.0.0b106d4.bin", "")
"heltec-ht62-esp32c3" = @("firmware-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "bleota-c3.bin", "littlefs-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "")
"tlora-c6" = @("firmware-tlora-c6-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-tlora-c6-2.6.0.0b106d4.bin", "")
"heltec-v3_web" = @("firmware-heltec-v3-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-heltec-v3-2.6.0.0b106d4.bin", "--web")
"seeed-sensecap-indicator-tft" = @("firmware-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "")
"picomputer-s3-tft" = @("firmware-picomputer-s3-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-picomputer-s3-tft-2.6.0.0b106d4.bin", "")
}
foreach ($TestCase in $TestCases.PSObject.Properties) {
$Name = $TestCase.Name
$Files = $TestCase.Value
$Errors = $null
$Counter = 0
Write-Host -Object "Testcase: $Name`:" -ForegroundColor Green
foreach ($File in $Files) {
if ($File.EndsWith(".bin")) {
New-EmptyFile -FileName $File
}
}
Write-Host -Object "Performing test on $Name..." -ForegroundColor Blue
$Test = Invoke-Expression -Command "cmd /c .\device-install.bat --debug -f $($TestCases."$Name"[0]) $($TestCases."$Name"[3])"
foreach ($Line in $Test) {
if ($Line -match "Set OTA_OFFSET to" -or `
$Line -match "Set SPIFFS_OFFSET to") {
Write-Host -Object "$($Line -replace "^.*?Set","Set")" -ForegroundColor Blue
}
elseif ($VerbosePreference -eq "Continue") {
Write-Host -Object $Line
}
if ($Line -match "ERROR") {
$Errors += $Line
$Counter++
}
}
if ($null -ne $Errors) {
Write-Host -Object "$Counter ERROR(s) detected!" -ForegroundColor Red
if (-not ($VerbosePreference -eq "Continue")) { Write-Host -Object $Errors }
}
foreach ($File in $Files) {
if ($File.EndsWith(".bin")) {
Remove-EmptyFile -FileName $File
}
}
}

View File

@@ -1,48 +1,175 @@
@ECHO OFF
SETLOCAL EnableDelayedExpansion
TITLE Meshtastic device-update
set PYTHON=python
SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0"
SET "PYTHON="
SET "ESPTOOL_BAUD=115200"
SET "ESPTOOL_CMD="
SET "LOGCOUNTER=0"
:: Determine the correct esptool command to use
where esptool >nul 2>&1
if %ERRORLEVEL% EQU 0 (
set "ESPTOOL_CMD=esptool"
) else (
set "ESPTOOL_CMD=%PYTHON% -m esptool"
)
GOTO getopts
:help
ECHO Flash image file to device, but leave existing system intact.
ECHO.
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python]
ECHO.
ECHO Options:
ECHO -f filename The .bin file to flash. Custom to your device type and region. (required)
ECHO The file must be located in this current directory.
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
ECHO If supplied the script will use python.
ECHO If not supplied the script will try to find esptool in Path.
ECHO.
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11
GOTO eof
goto GETOPTS
:HELP
echo Usage: %~nx0 [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME^|FILENAME]
echo Flash image file to device, leave existing system intact.
echo.
echo -h Display this help and exit
echo -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerrous).
echo -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: %PYTHON%)
echo -f FILENAME The *update.bin file to flash. Custom to your device type.
goto EOF
:version
ECHO %SCRIPT_NAME% [Version 2.6.0]
ECHO Meshtastic
GOTO eof
:GETOPTS
if /I "%1"=="-h" goto HELP
if /I "%1"=="--help" goto HELP
if /I "%1"=="-F" set "FILENAME=%2" & SHIFT
if /I "%1"=="-p" set ESPTOOL_PORT=%2 & SHIFT
if /I "%1"=="-P" set PYTHON=%2 & SHIFT
:getopts
IF "%~1"=="" GOTO endopts
IF /I "%~1"=="-?" GOTO help
IF /I "%~1"=="-h" GOTO help
IF /I "%~1"=="--help" GOTO help
IF /I "%~1"=="-v" GOTO version
IF /I "%~1"=="--version" GOTO version
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
SHIFT
IF NOT "__%1__"=="____" goto GETOPTS
GOTO getopts
:endopts
IF "__%FILENAME%__" == "____" (
echo "Missing FILENAME"
goto HELP
)
IF EXIST %FILENAME% IF NOT x%FILENAME:update=%==x%FILENAME% (
echo Trying to flash update %FILENAME%
%ESPTOOL_CMD% --baud 115200 write_flash 0x10000 %FILENAME%
) else (
echo "Invalid file: %FILENAME%"
goto HELP
) else (
echo "Invalid file: %FILENAME%"
goto HELP
CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..."
IF "__!FILENAME!__"=="____" (
CALL :LOG_MESSAGE DEBUG "Missing -f filename input."
GOTO help
) ELSE (
IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" (
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
GOTO help
)
@REM Remove ".\" or "./" file prefix if present.
SET "FILENAME=!FILENAME:.\=!"
SET "FILENAME=!FILENAME:./=!"
)
:EOF
CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!"
CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..."
IF NOT EXIST !FILENAME! (
CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating."
GOTO eof
)
IF "!FILENAME:update=!"=="!FILENAME!" (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash update !FILENAME!."
GOTO eof
) ELSE (
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
)
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
IF NOT "__%PYTHON%__"=="____" (
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
) ELSE (
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
WHERE esptool >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
@REM WHERE exits with code 0 if esptool is found.
SET "ESPTOOL_CMD=esptool"
) ELSE (
SET "ESPTOOL_CMD=python -m esptool"
CALL :RESET_ERROR
)
)
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
!ESPTOOL_CMD! >nul 2>&1
IF %ERRORLEVEL% GTR 2 (
@REM esptool exits with code 1 if help is displayed.
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
EXIT /B 1
GOTO eof
)
IF %DEBUG% EQU 1 (
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
)
CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!"
IF "__!ESPTOOL_PORT!__" == "____" (
CALL :LOG_MESSAGE WARN "Using esptool port: UNSET."
) ELSE (
CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!."
)
CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
@REM Flashing operations.
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
CALL :LOG_MESSAGE INFO "Script complete!."
:eof
ENDLOCAL
EXIT /B %ERRORLEVEL%
:RUN_ESPTOOL
@REM Subroutine used to run ESPTOOL_CMD with arguments.
@REM Also handles %ERRORLEVEL%.
@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
@REM.
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
CALL :RESET_ERROR
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
IF %ERRORLEVEL% NEQ 0 (
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
EXIT /B %ERRORLEVEL%
)
GOTO :eof
:LOG_MESSAGE
@REM Subroutine used to print log messages in four different levels.
@REM DEBUG messages only get printed if [-d] flag is passed to script.
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
@REM.
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
SET /A LOGCOUNTER=LOGCOUNTER+1
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
GOTO :eof
:GET_TIMESTAMP
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
@REM CALL :GET_TIMESTAMP
@REM.
@REM Updates: !TIMESTAMP!
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
SET "HH=%%a"
SET "MM=%%b"
SET "ss=%%c"
)
SET "TIMESTAMP=!HH!:!MM!:!ss!"
GOTO :eof
:RESET_ERROR
@REM Subroutine to reset %ERRORLEVEL% to 0.
@REM CALL :RESET_ERROR
@REM.
@REM Updates: %ERRORLEVEL%
EXIT /B 0
GOTO :eof

View File

@@ -35,6 +35,11 @@ for subdir, dirs, files in os.walk(rootdir):
outlist.append(section)
else:
outlist.append(section)
# Add the TFT variants if the base variant is selected
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
outlist.append(section)
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
outlist.append(section)
if "board_check" in config[config[c].name]:
if (config[config[c].name]["board_check"] == "true") & (
"check" in options
@@ -43,4 +48,4 @@ for subdir, dirs, files in os.walk(rootdir):
if ("quick" in options) & (len(outlist) > 3):
print(json.dumps(random.sample(outlist, 3)))
else:
print(json.dumps(outlist))
print(json.dumps(outlist))

View File

@@ -125,4 +125,9 @@ for flag in flags:
projenv.Append(
CCFLAGS=flags,
)
)
for lb in env.GetLibBuilders():
if lb.name == "meshtastic-device-ui":
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
break

View File

@@ -1 +1,10 @@
cd protobufs && ..\nanopb-0.4.9\generator-bin\protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:..\src\mesh\generated" -I=..\protobufs\ ..\protobufs\meshtastic\*.proto
@ECHO OFF
SETLOCAL
cd protobufs
..\nanopb-0.4.9\generator-bin\protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:..\src\mesh\generated" -I=..\protobufs\ ..\protobufs\meshtastic\*.proto
GOTO eof
:eof
ENDLOCAL
EXIT /B %ERRORLEVEL%

View File

@@ -1,2 +1,124 @@
@echo off
if [%1]==[] (echo "Please specify a platformio NRF target (i.e. rak4631) as the first argument.") else (python3 .\bin\uf2conv.py .\.pio\build\%1\firmware.hex -c -o .\.pio\build\%1\firmware.uf2 -f 0xADA52840)
@ECHO OFF
SETLOCAL EnableDelayedExpansion
TITLE Meshtastic uf2-convert
SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0"
SET "NRF=0"
SET "UF2CONV_CMD=python3 .\bin\uf2conv.py"
GOTO getopts
:help
ECHO.
ECHO Usage: %SCRIPT_NAME% -t [t-echo^|rak4631^|nano-g2-ultra^|wio-tracker-wm1110^|canaryone^|
ECHO heltec-mesh-node-t114^|tracker-t1000-e^|rak_wismeshtap^|rak2560^|
ECHO nrf52_promicro_diy_tcxo]
ECHO.
ECHO Options:
ECHO -t target Specify a platformio NRF target to build for. (required)
ECHO.
ECHO Example: %SCRIPT_NAME% -t rak4631
GOTO eof
:version
ECHO %SCRIPT_NAME% [Version 2.6.0]
ECHO Meshtastic
GOTO eof
:getopts
IF "%~1"=="" GOTO endopts
IF /I "%~1"=="-?" GOTO help
IF /I "%~1"=="-h" GOTO help
IF /I "%~1"=="--help" GOTO help
IF /I "%~1"=="-v" GOTO version
IF /I "%~1"=="--version" GOTO version
IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled."
IF /I "%~1"=="-t" SET "TARGETNAME=%~2" & SHIFT
IF /I "%~1"=="--target" SET "TARGETNAME=%~2" & SHIFT
SHIFT
GOTO getopts
:endopts
CALL :LOG_MESSAGE DEBUG "Checking TARGETNAME parameter..."
IF "__!TARGETNAME!__"=="____" (
CALL :LOG_MESSAGE DEBUG "Missing -t target input."
GOTO help
)
IF %DEBUG% EQU 1 SET "UF2CONV_CMD=REM python3 .\bin\uf2conv.py"
SET "NRFTARGETS=t-echo rak4631 nano-g2-ultra wio-tracker-wm1110 canaryone heltec-mesh-node-t114 tracker-t1000-e rak_wismeshtap rak2560 nrf52_promicro_diy_tcxo"
FOR %%a IN (%NRFTARGETS%) DO (
IF /I "%%a"=="!TARGETNAME!" (
@REM We are working with any of %NRFTARGETS%.
SET "NRF=1"
GOTO end_loop_nrf
)
)
:end_loop_nrf
@REM Building operations.
IF !NRF! EQU 1 (
CALL :LOG_MESSAGE INFO "Trying to build for !TARGETNAME!..."
CALL :RUN_UF2CONV !TARGETNAME! || GOTO eof
) ELSE (
CALL :LOG_MESSAGE WARN "!TARGETNAME! is not supported..."
GOTO eof
)
CALL :LOG_MESSAGE INFO "Script complete!."
:eof
ENDLOCAL
EXIT /B %ERRORLEVEL%
:RUN_UF2CONV
@REM Subroutine used to run .\bin\uf2conv.py with arguments.
@REM Also handles %ERRORLEVEL%.
@REM CALL :RUN_UF2CONV [target]
@REM.
@REM Example:: CALL :RUN_UF2CONV rak4631
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840"
CALL :RESET_ERROR
!UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840
IF %ERRORLEVEL% NEQ 0 (
CALL :LOG_MESSAGE ERROR "Error running command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840"
EXIT /B %ERRORLEVEL%
)
GOTO :eof
:LOG_MESSAGE
@REM Subroutine used to print log messages in four different levels.
@REM DEBUG messages only get printed if [-d] flag is passed to script.
@REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message"
@REM.
@REM Example:: CALL :LOG_MESSAGE INFO "Message."
SET /A LOGCOUNTER=LOGCOUNTER+1
IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2
GOTO :eof
:GET_TIMESTAMP
@REM Subroutine used to set !TIMESTAMP! to HH:MM:ss.
@REM CALL :GET_TIMESTAMP
@REM.
@REM Updates: !TIMESTAMP!
FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO (
SET "HH=%%a"
SET "MM=%%b"
SET "ss=%%c"
)
SET "TIMESTAMP=!HH!:!MM!:!ss!"
GOTO :eof
:RESET_ERROR
@REM Subroutine to reset %ERRORLEVEL% to 0.
@REM CALL :RESET_ERROR
@REM.
@REM Updates: %ERRORLEVEL%
EXIT /B 0
GOTO :eof

View File

@@ -7,13 +7,15 @@
"core": "esp32",
"extra_flags": [
"-DARDUINO_ESP32S3_DEV",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DBOARD_HAS_PSRAM"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"

View File

@@ -0,0 +1,56 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x2886", "0x0166"]
],
"usb_product": "XIAO-BOOT",
"mcu": "nrf52840",
"variant": "seeed_xiao_nrf52840_kit",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "seeed_xiao_nrf52840_kit",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.seeedstudio.com/XIAO-nRF52840-Wio-SX1262-Kit-for-Meshtastic-p-6400.html",
"vendor": "seeed"
}

View File

@@ -1,40 +0,0 @@
{
"build": {
"arduino": {
"earlephilhower": {
"boot2_source": "boot2_w25q080_2_padded_checksum.S",
"usb_vid": "0x2E8A",
"usb_pid": "0x000A"
}
},
"core": "earlephilhower",
"cpu": "cortex-m0plus",
"extra_flags": "-DARDUINO_GENERIC_RP2040 -DRASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040 -DUSBD_MAX_POWER_MA=250",
"f_cpu": "133000000L",
"hwids": [
["0x2E8A", "0x00C0"],
["0x2E8A", "0x000A"]
],
"mcu": "rp2040",
"variant": "WisBlock_RAK11300_Board"
},
"debug": {
"jlink_device": "RP2040_M0_0",
"openocd_target": "rp2040.cfg",
"svd_path": "rp2040.svd"
},
"frameworks": ["arduino"],
"name": "WisBlock RAK11300",
"upload": {
"maximum_ram_size": 270336,
"maximum_size": 2097152,
"require_upload_port": true,
"native_usb": true,
"use_1200bps_touch": true,
"wait_for_upload_port": false,
"protocol": "picotool",
"protocols": ["cmsis-dap", "raspberrypi-swd", "picotool", "picoprobe"]
},
"url": "https://docs.rakwireless.com/",
"vendor": "RAKwireless"
}

1
debian/control vendored
View File

@@ -17,6 +17,7 @@ Build-Depends: debhelper-compat (= 13),
libbluetooth-dev,
libusb-1.0-0-dev,
libi2c-dev,
libuv1-dev,
openssl,
libssl-dev,
libulfius-dev,

View File

@@ -36,6 +36,7 @@ BuildRequires: pkgconfig(libgpiod)
BuildRequires: pkgconfig(bluez)
BuildRequires: pkgconfig(libusb-1.0)
BuildRequires: libi2c-devel
BuildRequires: pkgconfig(libuv)
# Web components:
BuildRequires: pkgconfig(openssl)
BuildRequires: pkgconfig(liborcania)

View File

@@ -7,6 +7,8 @@ default_envs = tbeam
extra_configs =
arch/*/*.ini
variants/*/platformio.ini
src/graphics/niche/InkHUD/PlatformioConfig.ini
description = Meshtastic
[env]
@@ -58,7 +60,7 @@ lib_deps =
mathertel/OneButton@2.6.1
https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159
https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4
https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0
https://github.com/meshtastic/ArduinoThread.git#7c3ee9e1951551b949763b1f5280f8db1fa4068d
nanopb/Nanopb@0.4.91
erriez/ErriezCRC32@1.0.1
@@ -77,7 +79,7 @@ lib_deps =
${env.lib_deps}
end2endzone/NonBlockingRTTTL@1.3.0
build_flags = ${env.build_flags} -Os
build_src_filter = ${env.build_src_filter} -<platform/portduino/>
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
; Common libs for communicating over TCP/IP networks such as MQTT
[networking_base]
@@ -90,6 +92,10 @@ lib_deps =
lib_deps =
jgromes/RadioLib@7.1.2
[device-ui_base]
lib_deps =
https://github.com/meshtastic/device-ui.git#74e739ed4532ca10393df9fc89ae5a22f0bab2b1
; Common libs for environmental measurements in telemetry module
; (not included in native / portduino)
[environmental_base]
@@ -100,6 +106,7 @@ lib_deps =
adafruit/Adafruit BMP085 Library@1.2.4
adafruit/Adafruit BME280 Library@2.2.4
adafruit/Adafruit BMP3XX Library@2.1.5
adafruit/Adafruit DPS310@1.1.5
adafruit/Adafruit MCP9808 Library@2.0.2
adafruit/Adafruit INA260 Library@1.5.2
adafruit/Adafruit INA219@1.2.3

105
src/BluetoothStatus.h Normal file
View File

@@ -0,0 +1,105 @@
#pragma once
#include "Status.h"
#include "assert.h"
#include "configuration.h"
#include "meshUtils.h"
#include <Arduino.h>
namespace meshtastic
{
// Describes the state of the Bluetooth connection
// Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code
class BluetoothStatus : public Status
{
public:
enum class ConnectionState {
DISCONNECTED,
PAIRING,
CONNECTED,
};
private:
CallbackObserver<BluetoothStatus, const BluetoothStatus *> statusObserver =
CallbackObserver<BluetoothStatus, const BluetoothStatus *>(this, &BluetoothStatus::updateStatus);
ConnectionState state = ConnectionState::DISCONNECTED;
std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero
public:
BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; }
// New BluetoothStatus: connected or disconnected
explicit BluetoothStatus(ConnectionState state)
{
assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey
statusType = STATUS_TYPE_BLUETOOTH;
this->state = state;
}
// New BluetoothStatus: pairing, with passkey
explicit BluetoothStatus(const std::string &passkey) : Status()
{
statusType = STATUS_TYPE_BLUETOOTH;
this->state = ConnectionState::PAIRING;
this->passkey = passkey;
}
ConnectionState getConnectionState() const { return this->state; }
std::string getPasskey() const
{
assert(state == ConnectionState::PAIRING);
return this->passkey;
}
void observe(Observable<const BluetoothStatus *> *source) { statusObserver.observe(source); }
bool matches(const BluetoothStatus *newStatus) const
{
if (this->state == newStatus->getConnectionState()) {
// Same state: CONNECTED / DISCONNECTED
if (this->state != ConnectionState::PAIRING)
return true;
// Same state: PAIRING, and passkey matches
else if (this->getPasskey() == newStatus->getPasskey())
return true;
}
return false;
}
int updateStatus(const BluetoothStatus *newStatus)
{
// Has the status changed?
if (!matches(newStatus)) {
// Copy the members
state = newStatus->getConnectionState();
if (state == ConnectionState::PAIRING)
passkey = newStatus->getPasskey();
// Tell anyone interested that we have an update
onNewStatus.notifyObservers(this);
// Debug only:
switch (state) {
case ConnectionState::PAIRING:
LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str());
break;
case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED");
break;
case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED");
break;
}
}
return 0;
}
};
} // namespace meshtastic
extern meshtastic::BluetoothStatus *bluetoothStatus;

View File

@@ -11,6 +11,7 @@
#include "main.h"
#include "modules/ExternalNotificationModule.h"
#include "power.h"
#include "sleep.h"
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
#endif
@@ -99,6 +100,13 @@ ButtonThread::ButtonThread() : OSThread("Button")
userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click?
#endif
#ifdef ARCH_ESP32
// Register callbacks for before and after lightsleep
// Used to detach and reattach interrupts
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
#endif
attachButtonInterrupts();
#endif
}
@@ -320,6 +328,26 @@ void ButtonThread::detachButtonInterrupts()
#endif
}
#ifdef ARCH_ESP32
// Detach our class' interrupts before lightsleep
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
int ButtonThread::beforeLightSleep(void *unused)
{
detachButtonInterrupts();
return 0; // Indicates success
}
// Reconfigure our interrupts
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause)
{
attachButtonInterrupts();
return 0; // Indicates success
}
#endif
/**
* Watch a GPIO and if we get an IRQ, wake the main thread.
* Use to add wake on button press

View File

@@ -37,6 +37,12 @@ class ButtonThread : public concurrency::OSThread
void detachButtonInterrupts();
void storeClickCount();
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
#endif
private:
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
static OneButton userButton; // Static - accessed from an interrupt
@@ -48,6 +54,14 @@ class ButtonThread : public concurrency::OSThread
OneButton userButtonTouch;
#endif
#ifdef ARCH_ESP32
// Get notified when lightsleep begins and ends
CallbackObserver<ButtonThread, void *> lsObserver =
CallbackObserver<ButtonThread, void *>(this, &ButtonThread::beforeLightSleep);
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t> lsEndObserver =
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t>(this, &ButtonThread::afterLightSleep);
#endif
// set during IRQ
static volatile ButtonEventType btnEvent;

View File

@@ -121,10 +121,15 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
// Default Bluetooth PIN
#define defaultBLEPin 123456
#if HAS_ETHERNET
#if HAS_ETHERNET && !defined(USE_WS5500)
#include <RAK13800_W5100S.h>
#endif // HAS_ETHERNET
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#endif // HAS_ETHERNET
#if HAS_WIFI
#include <WiFi.h>
#endif // HAS_WIFI
@@ -164,4 +169,4 @@ class Syslog
bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0)));
};
#endif // HAS_ETHERNET || HAS_WIFI
#endif // HAS_NETWORKING

View File

@@ -23,6 +23,10 @@ SPIClass SPI1(HSPI);
#define SDHandler SPI
#endif
#ifndef SD_SPI_FREQUENCY
#define SD_SPI_FREQUENCY 4000000U
#endif
#endif // HAS_SDCARD
#if defined(ARCH_STM32WL)
@@ -361,8 +365,7 @@ void setupSDCard()
#ifdef HAS_SDCARD
concurrency::LockGuard g(spiLock);
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
if (!SD.begin(SDCARD_CS, SDHandler)) {
if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) {
LOG_DEBUG("No SD_MMC card detected");
return;
}

View File

@@ -32,6 +32,11 @@
#include <WiFi.h>
#endif
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#endif // HAS_ETHERNET
#endif
#ifndef DELAY_FOREVER

View File

@@ -11,12 +11,18 @@ static File openFile(const char *filename, bool fullAtomic)
FSCom.remove(filename);
return FSCom.open(filename, FILE_O_WRITE);
#endif
if (!fullAtomic)
if (!fullAtomic) {
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
}
String filenameTmp = filename;
filenameTmp += ".tmp";
// FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now
// if (fullAtomic) {
// FSCom.remove(filename);
// }
// clear any previous LFS errors
return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE);
}

View File

@@ -7,6 +7,7 @@
#define STATUS_TYPE_POWER 1
#define STATUS_TYPE_GPS 2
#define STATUS_TYPE_NODE 3
#define STATUS_TYPE_BLUETOOTH 4
namespace meshtastic
{

View File

@@ -135,6 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define LPS22HB_ADDR 0x5C
#define LPS22HB_ADDR_ALT 0x5D
#define SHT31_4x_ADDR 0x44
#define SHT31_4x_ADDR_ALT 0x45
#define PMSA0031_ADDR 0x12
#define QMA6100P_ADDR 0x12
#define AHT10_ADDR 0x38
@@ -150,6 +151,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MAX30102_ADDR 0x57
#define MLX90614_ADDR_DEF 0x5A
#define CGRADSENS_ADDR 0x66
#define LTR390UV_ADDR 0x53
// -----------------------------------------------------------------------------
// ACCELEROMETER

View File

@@ -67,6 +67,8 @@ class ScanI2C
INA226,
NXP_SE050,
DFROBOT_RAIN,
DPS310,
LTR390UV,
} DeviceType;
// typedef uint8_t DeviceAddress;

View File

@@ -237,6 +237,16 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
logFoundDevice("BMP085/BMP180", (uint8_t)addr.address);
type = BMP_085;
break;
case 0x00:
// do we have a DPS310 instead?
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1);
switch (registerValue) {
case 0x10:
logFoundDevice("DPS310", (uint8_t)addr.address);
type = DPS310;
break;
}
break;
default:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID
switch (registerValue) {
@@ -339,7 +349,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
}
case SHT31_4x_ADDR:
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c) {
type = SHT4X;
@@ -412,11 +423,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(OPT3001_ADDR, OPT3001, "OPT3001", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
#ifdef HAS_TPS65233
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
#endif

View File

@@ -1,3 +1,7 @@
#include <cstring> // Include for strstr
#include <string>
#include <vector>
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_GPS
#include "Default.h"
@@ -1100,12 +1104,16 @@ int32_t GPS::runOnce()
return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000;
}
// clear the GPS rx buffer as quickly as possible
// clear the GPS rx/tx buffer as quickly as possible
void GPS::clearBuffer()
{
#ifdef ARCH_ESP32
_serial_gps->flush(false);
#else
int x = _serial_gps->available();
while (x--)
_serial_gps->read();
#endif
}
/// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs
@@ -1117,7 +1125,7 @@ int GPS::prepareDeepSleep(void *unused)
}
static const char *PROBE_MESSAGE = "Trying %s (%s)...";
static const char *DETECTED_MESSAGE = "%s detected, using %s Module";
static const char *DETECTED_MESSAGE = "%s detected";
#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \
do { \
@@ -1125,11 +1133,22 @@ static const char *DETECTED_MESSAGE = "%s detected, using %s Module";
clearBuffer(); \
_serial_gps->write(TOWRITE "\r\n"); \
if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \
LOG_INFO(DETECTED_MESSAGE, CHIP, #DRIVER); \
LOG_INFO(DETECTED_MESSAGE, CHIP); \
return DRIVER; \
} \
} while (0)
#define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \
do { \
LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \
clearBuffer(); \
_serial_gps->write(COMMAND "\r\n"); \
GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \
if (detectedDriver != GNSS_MODEL_UNKNOWN) { \
return detectedDriver; \
} \
} while (0)
GnssModel_t GPS::probe(int serialSpeed)
{
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
@@ -1160,31 +1179,34 @@ GnssModel_t GPS::probe(int serialSpeed)
delay(20);
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
PROBE_SIMPLE("UC6580", "$PDTINFO", "UC6580", GNSS_MODEL_UC6580, 500);
PROBE_SIMPLE("UM600", "$PDTINFO", "UM600", GNSS_MODEL_UC6580, 500);
PROBE_SIMPLE("ATGM336H", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H, 500);
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS))
based on AT6558 */
PROBE_SIMPLE("ATGM332D", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H, 500);
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
std::vector<ChipInfo> atgm = {
{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
{"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
PROBE_SIMPLE("AG3335", "$PAIR021*39", "$PAIR021,AG3335", GNSS_MODEL_AG3335, 500);
PROBE_SIMPLE("AG3352", "$PAIR021*39", "$PAIR021,AG3352", GNSS_MODEL_AG3352, 500);
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
std::vector<ChipInfo> airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
// Close all NMEA sentences, valid for L76B MTK platform (Waveshare Pico GPS)
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n");
delay(20);
PROBE_SIMPLE("L76B", "$PMTK605*31", "Quectel-L76B", GNSS_MODEL_MTK_L76B, 500);
PROBE_SIMPLE("PA1616S", "$PMTK605*31", "1616S", GNSS_MODEL_MTK_PA1616S, 500);
PROBE_SIMPLE("LS20031", "$PMTK605*31", "MC-1513", GNSS_MODEL_LS20031, 500);
std::vector<ChipInfo> mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B},
{"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S},
{"LS20031", "MC-1513", GNSS_MODEL_LS20031}};
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500);
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
UBXChecksum(cfg_rate, sizeof(cfg_rate));
@@ -1281,6 +1303,38 @@ GnssModel_t GPS::probe(int serialSpeed)
return GNSS_MODEL_UNKNOWN;
}
GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap)
{
String response = "";
unsigned long start = millis();
while (millis() - start < timeout) {
if (_serial_gps->available()) {
response += (char)_serial_gps->read();
if (response.endsWith(",") || response.endsWith("\r\n")) {
#ifdef GPS_DEBUG
LOG_DEBUG(response.c_str());
#endif
// check if we can see our chips
for (const auto &chipInfo : responseMap) {
if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) {
LOG_INFO("%s detected", chipInfo.chipName.c_str());
return chipInfo.driver;
}
}
}
if (response.endsWith("\r\n")) {
response.trim();
response = ""; // Reset the response string for the next potential message
}
}
}
#ifdef GPS_DEBUG
LOG_DEBUG(response.c_str());
#endif
return GNSS_MODEL_UNKNOWN; // Return empty string on timeout
}
GPS *GPS::createGps()
{
int8_t _rx_gpio = config.position.rx_gpio;

View File

@@ -48,6 +48,11 @@ enum GPSPowerState : uint8_t {
GPS_OFF // Powered off indefinitely
};
struct ChipInfo {
String chipName; // The name of the chip (for logging)
String detectionString; // The string to match in the response
GnssModel_t driver; // The driver to use
};
/**
* A gps class that only reads from the GPS periodically and keeps the gps powered down except when reading
*
@@ -230,6 +235,8 @@ class GPS : private concurrency::OSThread
virtual int32_t runOnce() override;
GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap);
// Get GNSS model
GnssModel_t probe(int serialSpeed);

View File

@@ -166,7 +166,7 @@ bool EInkDisplay::connect()
}
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER)
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER)
{
// Start HSPI
hspi = new SPIClass(HSPI);
@@ -182,6 +182,9 @@ bool EInkDisplay::connect()
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
#if defined(CROWPANEL_ESP32S3_5_EPAPER)
adafruitDisplay->setRotation(0);
#endif
}
#elif defined(PCA10059) || defined(ME25LS01)
{

View File

@@ -68,7 +68,7 @@ class EInkDisplay : public OLEDDisplay
// If display uses HSPI
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER)
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER)
SPIClass *hspi = NULL;
#endif
@@ -77,4 +77,4 @@ class EInkDisplay : public OLEDDisplay
uint32_t lastDrawMsec = 0;
};
#endif
#endif

View File

@@ -324,6 +324,14 @@ void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
if (refresh != UNSPECIFIED)
return;
// Bypass limit if UNLIMITED_FAST mode is active
if (frameFlags & UNLIMITED_FAST) {
refresh = FAST;
reason = NO_OBJECTIONS;
LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags);
return;
}
// If too many FAST refreshes consecutively - force a FULL refresh
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
refresh = FULL;

View File

@@ -23,6 +23,10 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
~EInkDynamicDisplay();
// Methods to enable or disable unlimited fast refresh mode
void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); }
void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); }
// What kind of frame is this
enum frameFlagTypes : uint8_t {
BACKGROUND = (1 << 0), // For frames via display()
@@ -30,6 +34,7 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
COSMETIC = (1 << 2), // For splashes
DEMAND_FAST = (1 << 3), // Special case only
BLOCKING = (1 << 4), // Modifier - block while refresh runs
UNLIMITED_FAST = (1 << 5)
};
void addFrameFlag(frameFlagTypes flag);

View File

@@ -73,6 +73,16 @@
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#endif
#if defined(CROWPANEL_ESP32S3_5_EPAPER)
#include "graphics/fonts/EinkDisplayFonts.h"
#undef FONT_SMALL
#undef FONT_MEDIUM
#undef FONT_LARGE
#define FONT_SMALL FONT_LARGE_LOCAL // Height: 30
#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 30
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 30
#endif
#define _fontHeight(font) ((font)[1] + 1) // height is position 1
#define FONT_HEIGHT_SMALL _fontHeight(FONT_SMALL)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
#ifndef EINKDISPLAYFONTS_h
#define EINKDISPLAYFONTS_h
#ifdef ARDUINO
#include <Arduino.h>
#elif __MBED__
#define PROGMEM
#endif
/**
* Monospaced Plain 30
*/
extern const uint8_t Monospaced_plain_30[] PROGMEM;
#endif

View File

@@ -0,0 +1,108 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./LatchingBacklight.h"
#include "assert.h"
#include "sleep.h"
using namespace NicheGraphics::Drivers;
// Private constructor
// Called by getInstance
LatchingBacklight::LatchingBacklight()
{
// Attach the deep sleep callback
deepSleepObserver.observe(&notifyDeepSleep);
}
// Get access to (or create) the singleton instance of this class
LatchingBacklight *LatchingBacklight::getInstance()
{
// Instantiate the class the first time this method is called
static LatchingBacklight *const singletonInstance = new LatchingBacklight;
return singletonInstance;
}
// Which pin controls the backlight?
// Is the light active HIGH (default) or active LOW?
void LatchingBacklight::setPin(uint8_t pin, bool activeWhen)
{
this->pin = pin;
this->logicActive = activeWhen;
pinMode(pin, OUTPUT);
off(); // Explicit off seem required by T-Echo?
}
// Called when device is shutting down
// Ensures the backlight is off
int LatchingBacklight::beforeDeepSleep(void *unused)
{
// Contingency only
// - pin wasn't set
if (pin != (uint8_t)-1) {
off();
pinMode(pin, INPUT); // High impedance - unnecessary?
} else
LOG_WARN("LatchingBacklight instantiated, but pin not set");
return 0; // Continue with deep sleep
}
// Turn the backlight on *temporarily*
// This should be used for momentary illumination, such as while a button is held
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
void LatchingBacklight::peek()
{
assert(pin != (uint8_t)-1);
digitalWrite(pin, logicActive); // On
on = true;
latched = false;
}
// Turn the backlight on, and keep it on
// This should be used when the backlight should remain active, even after user input ends
// e.g. when enabled via the menu
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
void LatchingBacklight::latch()
{
assert(pin != (uint8_t)-1);
// Blink if moving from peek to latch
// Indicates to user that the transition has taken place
if (on && !latched) {
digitalWrite(pin, !logicActive); // Off
delay(25);
digitalWrite(pin, logicActive); // On
delay(25);
digitalWrite(pin, !logicActive); // Off
delay(25);
}
digitalWrite(pin, logicActive); // On
on = true;
latched = true;
}
// Turn the backlight off
// Suitable for ending both peek and latch
void LatchingBacklight::off()
{
assert(pin != (uint8_t)-1);
digitalWrite(pin, !logicActive); // Off
on = false;
latched = false;
}
bool LatchingBacklight::isOn()
{
return on;
}
bool LatchingBacklight::isLatched()
{
return latched;
}
#endif

View File

@@ -0,0 +1,50 @@
/*
Singleton class
On-demand control of a display's backlight, connected to a GPIO
Initial use case is control of T-Echo's frontlight, via the capacitive touch button
- momentary on
- latched on
*/
#pragma once
#include "configuration.h"
#include "Observer.h"
namespace NicheGraphics::Drivers
{
class LatchingBacklight
{
public:
static LatchingBacklight *getInstance(); // Create or get the singleton instance
void setPin(uint8_t pin, bool activeWhen = HIGH);
int beforeDeepSleep(void *unused); // Callback for auto-shutoff
void peek(); // Backlight on temporarily, e.g. while button held
void latch(); // Backlight on permanently, e.g. toggled via menu
void off(); // Backlight off. Suitable for both peek and latch
bool isOn(); // Either peek or latch
bool isLatched();
private:
LatchingBacklight(); // Constructor made private: force use of getInstance
// Get notified when the system is shutting down
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
uint8_t pin = (uint8_t)-1;
bool logicActive = HIGH; // Is light active HIGH or active LOW
bool on = false; // Is light on (either peek or latched)
bool latched = false; // Is light latched on
};
} // namespace NicheGraphics::Drivers

View File

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

View File

@@ -0,0 +1,34 @@
/*
E-Ink display driver
- DEPG0154BNS800
- Manufacturer: DKE
- Size: 1.54 inch
- Resolution: 152px x 152px
- Flex connector marking: FPC7525
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class DEPG0154BNS800 : public SSD16XX
{
// Display properties
private:
static constexpr uint32_t width = 152;
static constexpr uint32_t height = 152;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL);
public:
DEPG0154BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,120 @@
#include "./DEPG0290BNS800.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
// Describes the operation performed when a "fast refresh" is performed
// Source: custom, with DEPG0150BNS810 as a reference
static const uint8_t LUT_FAST[] = {
// 1 2 3 4
0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels)
0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels)
0x00, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels)
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1. Tap existing black pixels back into place
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. Move new pixels
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. New pixels, and also existing black pixels
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // 4. All pixels, then cooldown
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00,
};
// How strongly the pixels are pulled and pushed
void DEPG0290BNS800::configVoltages()
{
switch (updateType) {
case FAST:
// Listed as "typical" in datasheet
sendCommand(0x04);
sendData(0x41); // VSH1 15V
sendData(0x00); // VSH2 NA
sendData(0x32); // VSL -15V
break;
case FULL:
default:
// From OTP memory
break;
}
}
// Load settings about how the pixels are moved from old state to new state during a refresh
// - manually specified,
// - or with stored values from displays OTP memory
void DEPG0290BNS800::configWaveform()
{
switch (updateType) {
case FAST:
sendCommand(0x3C); // Border waveform:
sendData(0x60); // Actively hold screen border during update
sendCommand(0x32); // Write LUT register from MCU:
sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh)
break;
case FULL:
default:
// From OTP memory
break;
}
}
// Describes the sequence of events performed by the displays controller IC during a refresh
// Includes "power up", "load settings from memory", "update the pixels", etc
void DEPG0290BNS800::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x22); // Set "update sequence"
sendData(0xCF); // Differential, use manually loaded waveform
break;
case FULL:
default:
sendCommand(0x22); // Set "update sequence"
sendData(0xF7); // Non-differential, load waveform from OTP
break;
}
}
// Once the refresh operation has been started,
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
// Only used when refresh is "async"
void DEPG0290BNS800::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 450); // At least 450ms for fast refresh
case FULL:
default:
return beginPolling(100, 3000); // At least 3 seconds for full refresh
}
}
// For this display, we do not need to re-write the new image.
// We're overriding SSD16XX::finalizeUpdate to make this small optimization.
// The display does also work just fine with the generic SSD16XX method, though.
void DEPG0290BNS800::finalizeUpdate()
{
// Put a copy of the image into the "old memory".
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
if (updateType != FULL) {
// writeNewImage(); // Not required for this display
writeOldImage();
sendCommand(0x7F); // Terminate image write without update
wait();
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,42 @@
/*
E-Ink display driver
- DEPG0290BNS800
- Manufacturer: DKE
- Size: 2.9 inch
- Resolution: 128px x 296px
- Flex connector marking: FPC-7519 rev.b
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class DEPG0290BNS800 : public SSD16XX
{
// Display properties
private:
static constexpr uint32_t width = 128;
static constexpr uint32_t height = 296;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
protected:
void configVoltages() override;
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
void finalizeUpdate() override; // Only overriden for a slight optimization
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,70 @@
#include "./EInk.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
// Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants
EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported)
: concurrency::OSThread("E-Ink Driver"), width(width), height(height), supportedUpdateTypes(supported)
{
OSThread::disable();
}
// Used by NicheGraphics implementations to check if a display supports a specific refresh operation.
// Whether or not the update type is supported is specified in the constructor
bool EInk::supports(UpdateTypes type)
{
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.
if (supportedUpdateTypes & type)
return true;
else
return false;
}
// Begins using the OSThread to detect when a display update is complete
// This allows the refresh operation to run "asynchronously".
// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin
// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes.
// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration",
// provided its isUpdateDone() override always returns true.
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
{
updateRunning = true;
updateBegunAt = millis();
pollingInterval = interval;
// To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take
// By default, expectedDuration is 0, and we'll start polling immediately
OSThread::setIntervalFromNow(expectedDuration);
OSThread::enabled = true;
}
// Meshtastic's pseudo-threading layer
// We're using this as a timer, to periodically check if an update is complete
// This is what allows us to update the display asynchronously
int32_t EInk::runOnce()
{
if (!isUpdateDone())
return pollingInterval; // Poll again in a few ms
// If update done:
finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc
updateRunning = false; // Change what we report via EInk::busy()
return disable(); // Stop polling
}
// Wait for an in progress update to complete before continuing
// Run a normal (async) update first, *then* call await
void EInk::await()
{
// Stop our concurrency thread
OSThread::disable();
// Sit and block until the update is complete
while (updateRunning) {
runOnce();
yield();
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,56 @@
/*
Base class for E-Ink display drivers
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "concurrency/OSThread.h"
#include <SPI.h>
namespace NicheGraphics::Drivers
{
class EInk : private concurrency::OSThread
{
public:
// Different possible operations used to update an E-Ink display
// Some displays will not support all operations
// Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType)
enum UpdateTypes : uint8_t {
UNSPECIFIED = 0,
FULL = 1 << 0,
FAST = 1 << 1,
};
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
void await(); // Wait for an in-progress update to complete before proceeding
bool supports(UpdateTypes type); // Can display perform a certain update type
bool busy() { return updateRunning; } // Display able to update right now?
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
const uint16_t height;
protected:
void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished
virtual bool isUpdateDone() = 0; // Check once if update finished
virtual void finalizeUpdate() {} // Run any post-update code
private:
int32_t runOnce() override; // Repeated checking if update finished
const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class
bool updateRunning = false; // see EInk::busy()
uint32_t updateBegunAt = 0; // For initial pause before polling for update completion
uint32_t pollingInterval = 0; // How often to check if update complete (ms)
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,61 @@
#include "./GDEY0154D67.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
// Map the display controller IC's output to the connected panel
void GDEY0154D67::configScanning()
{
// "Driver output control"
sendCommand(0x01);
sendData(0xC7);
sendData(0x00);
sendData(0x00);
// To-do: delete this method?
// Values set here might be redundant: C7, 00, 00 seems to be default
}
// Specify which information is used to control the sequence of voltages applied to move the pixels
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
// the controller IC's OTP memory, when the update procedure begins.
void GDEY0154D67::configWaveform()
{
sendCommand(0x3C); // Border waveform:
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
sendCommand(0x18); // Temperature sensor:
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
}
void GDEY0154D67::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x22); // Set "update sequence"
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
break;
case FULL:
default:
sendCommand(0x22); // Set "update sequence"
sendData(0xF7); // Will load LUT from OTP memory
break;
}
}
// Once the refresh operation has been started,
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
// Only used when refresh is "async"
void GDEY0154D67::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 500); // At least 500ms for fast refresh
case FULL:
default:
return beginPolling(100, 2000); // At least 2 seconds for full refresh
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,42 @@
/*
E-Ink display driver
- GDEY0154D67
- Manufacturer: Goodisplay
- Size: 1.54 inch
- Resolution: 200px x 200px
- Flex connector marking: FPC-B001
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class GDEY0154D67 : public SSD16XX
{
// Display properties
private:
static constexpr uint32_t width = 200;
static constexpr uint32_t height = 200;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
GDEY0154D67() : SSD16XX(width, height, supported) {}
protected:
virtual void configScanning() override;
virtual void configWaveform() override;
virtual void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,295 @@
#include "./LCMEN2R13EFC1.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include <assert.h>
using namespace NicheGraphics::Drivers;
// Look up table: fast refresh, common electrode
static const uint8_t LUT_FAST_VCOMDC[] = {
0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01, //
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
// Look up table: fast refresh, pixels which remain white
static const uint8_t LUT_FAST_WW[] = {
0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01, //
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
// Look up table: fast refresh, pixel which change from black to white
static const uint8_t LUT_FAST_BW[] = {
0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01, //
0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
// Look up table: fash refresh, pixels which change from white to black
static const uint8_t LUT_FAST_WB[] = {
0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, //
0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, //
0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
// Look up table: fash refresh, pixels which remain black
static const uint8_t LUT_FAST_BB[] = {
0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01, //
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported)
{
// Pre-calculate size of the image buffer, for convenience
// Determine the X dimension of the image buffer, in bytes.
// Along rows, pixels are stored 8 per byte.
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
bufferRowSize = ((width - 1) / 8) + 1;
// Total size of image buffer, in bytes.
bufferSize = bufferRowSize * height;
}
void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
{
this->spi = spi;
this->pin_dc = pin_dc;
this->pin_cs = pin_cs;
this->pin_busy = pin_busy;
this->pin_rst = pin_rst;
pinMode(pin_dc, OUTPUT);
pinMode(pin_cs, OUTPUT);
pinMode(pin_busy, INPUT);
// Reset is active low, hold high
pinMode(pin_rst, INPUT_PULLUP);
reset();
}
// Display an image on the display
void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type)
{
this->updateType = type;
this->buffer = imageData;
reset();
// Config
if (updateType == FULL)
configFull();
else
configFast();
// Transfer image data
if (updateType == FULL) {
writeNewImage();
writeOldImage();
} else {
writeNewImage();
}
sendCommand(0x04); // Power on the panel voltage
wait();
sendCommand(0x12); // Begin executing the update
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
// For a blocking update, call await after update
detachFromUpdate();
}
void LCMEN213EFC1::wait()
{
// Busy when LOW
while (digitalRead(pin_busy) == LOW)
yield();
}
void LCMEN213EFC1::reset()
{
pinMode(pin_rst, OUTPUT);
digitalWrite(pin_rst, LOW);
delay(10);
pinMode(pin_rst, INPUT_PULLUP);
wait();
sendCommand(0x12);
wait();
}
void LCMEN213EFC1::sendCommand(const uint8_t command)
{
spi->beginTransaction(spiSettings);
digitalWrite(pin_dc, LOW); // DC pin low indicates command
digitalWrite(pin_cs, LOW);
spi->transfer(command);
digitalWrite(pin_cs, HIGH);
digitalWrite(pin_dc, HIGH);
spi->endTransaction();
}
void LCMEN213EFC1::sendData(uint8_t data)
{
sendData(&data, 1);
}
void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size)
{
spi->beginTransaction(spiSettings);
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
digitalWrite(pin_cs, LOW);
// Platform-specific SPI command
// Mothballing. This display model is only used by Heltec Wireless Paper (ESP32)
#if defined(ARCH_ESP32)
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
#elif defined(ARCH_NRF52)
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
#else
#error Not implemented yet? Feel free to add other platforms here.
#endif
digitalWrite(pin_cs, HIGH);
digitalWrite(pin_dc, HIGH);
spi->endTransaction();
}
void LCMEN213EFC1::configFull()
{
sendCommand(0x00); // Panel setting register
sendData(0b11 << 6 // Display resolution
| 1 << 4 // B&W only
| 1 << 3 // Vertical scan direction
| 1 << 2 // Horizontal scan direction
| 1 << 1 // Shutdown: no
| 1 << 0 // Reset: no
);
sendCommand(0x50); // VCOM and data interval setting register
sendData(0b10 << 6 // Border driven white
| 0b11 << 4 // Invert image colors: no
| 0b0111 << 0 // Interval between VCOM on and image data (default)
);
}
void LCMEN213EFC1::configFast()
{
sendCommand(0x00); // Panel setting register
sendData(0b11 << 6 // Display resolution
| 1 << 5 // LUT from registers (set below)
| 1 << 4 // B&W only
| 1 << 3 // Vertical scan direction
| 1 << 2 // Horizontal scan direction
| 1 << 1 // Shutdown: no
| 1 << 0 // Reset: no
);
sendCommand(0x50); // VCOM and data interval setting register
sendData(0b11 << 6 // Border floating
| 0b01 << 4 // Invert image colors: no
| 0b0111 << 0 // Interval between VCOM on and image data (default)
);
// Load the various LUTs
sendCommand(0x20); // VCOM
sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
sendCommand(0x21); // White -> White
sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
sendCommand(0x22); // Black -> White
sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
sendCommand(0x23); // White -> Black
sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
sendCommand(0x24); // Black -> Black
sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
}
void LCMEN213EFC1::writeNewImage()
{
sendCommand(0x13);
sendData(buffer, bufferSize);
}
void LCMEN213EFC1::writeOldImage()
{
sendCommand(0x10);
sendData(buffer, bufferSize);
}
void LCMEN213EFC1::detachFromUpdate()
{
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
// If not implemented, we'll just poll right from the get-go
switch (updateType) {
case FULL:
EInk::beginPolling(10, 3650);
break;
case FAST:
EInk::beginPolling(10, 720);
break;
default:
assert(false);
}
}
bool LCMEN213EFC1::isUpdateDone()
{
// Busy when LOW
if (digitalRead(pin_busy) == LOW)
return false;
else
return true;
}
void LCMEN213EFC1::finalizeUpdate()
{
// Power off the panel voltages
sendCommand(0x02);
wait();
// Put a copy of the image into the "old memory".
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
if (updateType != FULL) {
writeOldImage();
wait();
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,71 @@
/*
E-Ink display driver
- LCMEN213EFC1
- Manufacturer: Wisevast
- Size: 2.13 inch
- Resolution: 122px x 250px
- Flex connector marking: HINK-E0213A162-FPC-A0 (Hidden, printed on back-side)
Note: this display uses an uncommon controller IC, Fitipower JD79656.
It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./EInk.h"
namespace NicheGraphics::Drivers
{
class LCMEN213EFC1 : public EInk
{
// Display properties
private:
static constexpr uint32_t width = 122;
static constexpr uint32_t height = 250;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
LCMEN213EFC1();
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
void update(uint8_t *imageData, UpdateTypes type) override;
protected:
void wait();
void reset();
void sendCommand(const uint8_t command);
void sendData(const uint8_t data);
void sendData(const uint8_t *data, uint32_t size);
void configFull(); // Configure display for FULL refresh
void configFast(); // Configure display for FAST refresh
void writeNewImage();
void writeOldImage(); // Used for "differential update", aka FAST refresh
void detachFromUpdate();
bool isUpdateDone();
void finalizeUpdate();
protected:
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
uint32_t bufferSize = 0; // In bytes. Rows * Columns
uint8_t *buffer = nullptr;
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
uint8_t pin_dc = -1;
uint8_t pin_cs = -1;
uint8_t pin_busy = -1;
uint8_t pin_rst = -1;
SPIClass *spi = nullptr;
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,85 @@
# NicheGraphics - E-Ink Driver
A driver for E-Ink SPI displays. Suitable for re-use by various NicheGraphics UIs.
Your UI should use the class `NicheGraphics::Drivers::EInk` .
When you set up a hardware variant, you will use one of the specific display model classes, which extend the EInk class.
An example setup might look like this:
```cpp
void setupNicheGraphics()
{
using namespace NicheGraphics;
// An imaginary UI
YourCustomUI *yourUI = new YourCustomUI();
// Setup SPI
SPIClass *hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
// Setup Enk driver
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
// Pass the driver to your UI
YourUI::driver = driver;
}
```
## Methods
### `update(uint8_t *imageData, UpdateTypes type)`
Update the image on the display
- _`imageData`_ to draw to the display.
- _`type`_ which type of update to perform.
- `FULL`
- `FAST`
- (Other custom types may be possible)
The imageData is a 1-bit image. X-Pixels are 8-per byte, with the MSB being the leftmost pixel. This was not an InkHUD design decision; it is the raw format accepted by the E-Ink display controllers ICs.
_To-do: add a helper method to `InkHUD::Drivers::EInk` to do this arithmetic for you._
```cpp
uint16_t w = driver::width();
uint16_t h = driver::height();
uint8_t image[ (w/8) * h ]; // X pixels are 8-per-byte
image[0] |= (1 << 7); // Set pixel x=0, y=0
image[0] |= (1 << 0); // Set pixel x=7, y=0
image[1] |= (1 << 7); // Set pixel x=8, y=0
uint8_t x = 12;
uint8_t y = 2;
uint8_t yBytes = y * (w/8);
uint8_t xBytes = x / 8;
uint8_t xBits = (7-x) % 8;
image[yByte + xByte] |= (1 << xBits); // Set pixel x=12, y=2
```
### `await()`
Wait for an in-progress update to complete before continuing
### `supports(UpdateTypes type)`
Check if display supports a specific update type. `true` if supported.
- _`type`_ type to check
### `busy()`
Check if display is already performing an `update()`. `true` if already updating.
### `width()`
Width of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software.
### `height()`
Height of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software.

View File

@@ -0,0 +1,220 @@
#include "./SSD16XX.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX)
: EInk(width, height, supported), bufferOffsetX(bufferOffsetX)
{
// Pre-calculate size of the image buffer, for convenience
// Determine the X dimension of the image buffer, in bytes.
// Along rows, pixels are stored 8 per byte.
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
bufferRowSize = ((width - 1) / 8) + 1;
// Total size of image buffer, in bytes.
bufferSize = bufferRowSize * height;
}
void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
{
this->spi = spi;
this->pin_dc = pin_dc;
this->pin_cs = pin_cs;
this->pin_busy = pin_busy;
this->pin_rst = pin_rst;
pinMode(pin_dc, OUTPUT);
pinMode(pin_cs, OUTPUT);
pinMode(pin_busy, INPUT);
// If using a reset pin, hold high
// Reset is active low for Solomon Systech ICs
if (pin_rst != 0xFF)
pinMode(pin_rst, INPUT_PULLUP);
reset();
}
void SSD16XX::wait()
{
// Busy when HIGH
while (digitalRead(pin_busy) == HIGH)
yield();
}
void SSD16XX::reset()
{
// Check if reset pin is defined
if (pin_rst != 0xFF) {
pinMode(pin_rst, OUTPUT);
digitalWrite(pin_rst, LOW);
delay(50);
pinMode(pin_rst, INPUT_PULLUP);
wait();
}
sendCommand(0x12);
wait();
}
void SSD16XX::sendCommand(const uint8_t command)
{
spi->beginTransaction(spiSettings);
digitalWrite(pin_dc, LOW); // DC pin low indicates command
digitalWrite(pin_cs, LOW);
spi->transfer(command);
digitalWrite(pin_cs, HIGH);
digitalWrite(pin_dc, HIGH);
spi->endTransaction();
}
void SSD16XX::sendData(uint8_t data)
{
sendData(&data, 1);
}
void SSD16XX::sendData(const uint8_t *data, uint32_t size)
{
spi->beginTransaction(spiSettings);
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
digitalWrite(pin_cs, LOW);
// Platform-specific SPI command
#if defined(ARCH_ESP32)
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
#elif defined(ARCH_NRF52)
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
#else
#error Not implemented yet? Feel free to add other platforms here.
#endif
digitalWrite(pin_cs, HIGH);
digitalWrite(pin_dc, HIGH);
spi->endTransaction();
}
void SSD16XX::configFullscreen()
{
// Placing this code in a separate method because it's probably pretty consistent between displays
// Should make it tidier to override SSD16XX::configure
// Define the boundaries of the "fullscreen" region, for the controller IC
static const uint16_t sx = bufferOffsetX; // Notice the offset
static const uint16_t sy = 0;
static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
static const uint16_t ey = height;
// Split into bytes
static const uint8_t sy1 = sy & 0xFF;
static const uint8_t sy2 = (sy >> 8) & 0xFF;
static const uint8_t ey1 = ey & 0xFF;
static const uint8_t ey2 = (ey >> 8) & 0xFF;
// Data entry mode - Left to Right, Top to Bottom
sendCommand(0x11);
sendData(0x03);
// Select controller IC memory region to display a fullscreen image
sendCommand(0x44); // Memory X start - end
sendData(sx);
sendData(ex);
sendCommand(0x45); // Memory Y start - end
sendData(sy1);
sendData(sy2);
sendData(ey1);
sendData(ey2);
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
sendCommand(0x4E); // Memory cursor X
sendData(sx);
sendCommand(0x4F); // Memory cursor y
sendData(sy1);
sendData(sy2);
}
void SSD16XX::update(uint8_t *imageData, UpdateTypes type)
{
this->updateType = type;
this->buffer = imageData;
reset();
configFullscreen();
configScanning(); // Virtual, unused by base class
configVoltages(); // Virtual, unused by base class
configWaveform(); // Virtual, unused by base class
wait();
if (updateType == FULL) {
writeNewImage();
writeOldImage();
} else {
writeNewImage();
}
configUpdateSequence();
sendCommand(0x20); // Begin executing the update
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
// For a blocking update, call await after update
detachFromUpdate();
}
// Send SPI commands for controller IC to begin executing the refresh operation
void SSD16XX::configUpdateSequence()
{
switch (updateType) {
default:
sendCommand(0x22); // Set "update sequence"
sendData(0xF7); // Non-differential, load waveform from OTP
break;
}
}
void SSD16XX::writeNewImage()
{
sendCommand(0x24);
sendData(buffer, bufferSize);
}
void SSD16XX::writeOldImage()
{
sendCommand(0x26);
sendData(buffer, bufferSize);
}
void SSD16XX::detachFromUpdate()
{
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
// If not implemented, we'll just poll right from the get-go
switch (updateType) {
default:
EInk::beginPolling(100, 0);
}
}
bool SSD16XX::isUpdateDone()
{
// Busy when HIGH
if (digitalRead(pin_busy) == HIGH)
return false;
else
return true;
}
void SSD16XX::finalizeUpdate()
{
// Put a copy of the image into the "old memory".
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
if (updateType != FULL) {
writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678?
writeOldImage();
sendCommand(0x7F); // Terminate image write without update
wait();
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -0,0 +1,65 @@
/*
E-Ink base class for displays based on SSD16XX
Most (but not all) SPI E-Ink displays use this family of controller IC.
Implementing new SSD16XX displays should be fairly painless.
See DEPG0154BNS800 and DEPG0290BNS800 for examples.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./EInk.h"
namespace NicheGraphics::Drivers
{
class SSD16XX : public EInk
{
public:
SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0);
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1);
virtual void update(uint8_t *imageData, UpdateTypes type) override;
protected:
virtual void wait();
virtual void reset();
virtual void sendCommand(const uint8_t command);
virtual void sendData(const uint8_t data);
virtual void sendData(const uint8_t *data, uint32_t size);
virtual void configFullscreen(); // Select memory region on controller IC
virtual void configScanning() {} // Optional. First & last gates, scan direction, etc
virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc
virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc
virtual void configUpdateSequence(); // Tell controller IC which operations to run
virtual void writeNewImage();
virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh"
virtual void detachFromUpdate();
virtual bool isUpdateDone() override;
virtual void finalizeUpdate() override;
protected:
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
uint32_t bufferSize = 0; // In bytes. Rows * Columns
uint8_t *buffer = nullptr;
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
uint8_t pin_dc = -1;
uint8_t pin_cs = -1;
uint8_t pin_busy = -1;
uint8_t pin_rst = -1;
SPIClass *spi = nullptr;
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

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

View File

@@ -0,0 +1,140 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
/*
Re-usable NicheGraphics tool
Save settings / data to flash, without use of the Meshtastic Protobufs
Avoid bloating everyone's protobuf code for our one-off UI implementations
*/
#pragma once
#include "configuration.h"
#include "SafeFile.h"
namespace NicheGraphics
{
template <typename T> class FlashData
{
private:
static std::string getFilename(const char *label)
{
std::string filename;
filename += "/NicheGraphics";
filename += "/";
filename += label;
filename += ".data";
return filename;
}
static uint32_t getHash(T *data)
{
uint32_t hash = 0;
// Sum all bytes of the image buffer together
for (uint32_t i = 0; i < sizeof(T); i++)
hash ^= ((uint8_t *)data)[i] + 1;
return hash;
}
public:
static bool load(T *data, const char *label)
{
// Set false if we run into issues
bool okay = true;
// Get a filename based on the label
std::string filename = getFilename(label);
#ifdef FSCom
// Check that the file *does* actually exist
if (!FSCom.exists(filename.c_str())) {
LOG_WARN("'%s' not found. Using default values", filename.c_str());
okay = false;
return okay;
}
// Open the file
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
// If opened, start reading
if (f) {
LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str());
// Create an object which will received data from flash
// We read here first, so we can verify the checksum, without committing to overwriting the *data object
// Allows us to retain any defaults that might be set after we declared *data, but before loading settings,
// in case the flash values are corrupt
T flashData;
// Read the actual data
f.readBytes((char *)&flashData, sizeof(T));
// Read the hash
uint32_t savedHash = 0;
f.readBytes((char *)&savedHash, sizeof(savedHash));
// Calculate hash of the loaded data, then compare with the saved hash
// If hash looks good, copy the values to the main data object
uint32_t calculatedHash = getHash(&flashData);
if (savedHash != calculatedHash) {
LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str());
okay = false;
} else
*data = flashData;
f.close();
} else {
LOG_ERROR("Could not open / read %s", filename.c_str());
okay = false;
}
#else
LOG_ERROR("Filesystem not implemented");
state = LoadFileState::NO_FILESYSTEM;
okay = false;
#endif
return okay;
}
// Save module's custom data (settings?) to flash. Does use protobufs
static void save(T *data, const char *label)
{
// Get a filename based on the label
std::string filename = getFilename(label);
#ifdef FSCom
FSCom.mkdir("/NicheGraphics");
auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename.
LOG_INFO("Saving %s", filename.c_str());
// Calculate a hash of the data
uint32_t hash = getHash(data);
f.write((uint8_t *)data, sizeof(T)); // Write the actual data
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
// f.flush();
bool writeSucceeded = f.close();
if (!writeSucceeded) {
LOG_ERROR("Can't write data!");
}
#else
LOG_ERROR("ERROR: Filesystem not implemented\n");
#endif
}
};
} // namespace NicheGraphics
#endif

View File

@@ -0,0 +1,129 @@
#pragma once
const uint8_t FreeSans6pt7bBitmaps[] PROGMEM = {
0xAA, 0xA8, 0xC0, 0xF6, 0xA0, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00,
0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, 0x70, 0x91, 0x23, 0x86, 0x12, 0xA2, 0x4E, 0xF4, 0xE0,
0x5A, 0xAA, 0x94, 0x89, 0x12, 0x49, 0x29, 0x00, 0x27, 0x50, 0x21, 0x3E, 0x42, 0x00, 0xE0, 0xC0, 0x80, 0x24, 0xA4, 0xA4, 0x80,
0x74, 0xE3, 0x18, 0xC6, 0x33, 0x70, 0x27, 0x92, 0x49, 0x20, 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, 0x79, 0x30, 0x43, 0x18,
0x10, 0x71, 0x78, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0x7D, 0x04, 0x1E, 0x44, 0x10, 0x51, 0x78, 0x74, 0x61, 0xE8, 0xC6,
0x31, 0x70, 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53,
0x78, 0x82, 0x87, 0x01, 0xF1, 0x83, 0x04, 0xF8, 0x3E, 0x07, 0x06, 0x36, 0x40, 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, 0x0F, 0x86,
0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42,
0x42, 0xC3, 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, 0xF9, 0x0A, 0x1C,
0x18, 0x30, 0x61, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x61,
0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x87,
0x29, 0x70, 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
0xA5, 0x99, 0x99, 0x99, 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6,
0x1E, 0x00, 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1F, 0x00, 0x00,
0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04,
0x08, 0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, 0xC2, 0x42, 0x42, 0x64, 0x24, 0x24, 0x38, 0x18, 0x18, 0xC4, 0x28,
0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, 0x42, 0x66, 0x24, 0x18, 0x18, 0x18, 0x24, 0x46, 0x42, 0xC3,
0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24,
0x89, 0x20, 0xE9, 0x24, 0x92, 0x49, 0x70, 0x46, 0xA9, 0x10, 0xFE, 0x40, 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, 0x84, 0x3D, 0x18,
0xC6, 0x31, 0xF0, 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, 0x39, 0x38, 0x7F, 0x81, 0x13,
0x80, 0x6B, 0xA4, 0x92, 0x40, 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, 0xBF, 0x80,
0x45, 0x55, 0x57, 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, 0xFF, 0x80, 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, 0xF4, 0x63, 0x18,
0xC6, 0x20, 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04,
0xF2, 0x49, 0x20, 0x79, 0x24, 0x1C, 0x0B, 0x27, 0x80, 0x5D, 0x24, 0x93, 0x8C, 0x63, 0x18, 0xCF, 0xA0, 0x85, 0x24, 0x92, 0x30,
0xC3, 0x00, 0x89, 0x2C, 0x96, 0x4A, 0xA5, 0x61, 0x30, 0x98, 0x49, 0x23, 0x08, 0x31, 0x2C, 0x80, 0x89, 0x24, 0x94, 0x50, 0xC2,
0x08, 0x21, 0x00, 0x78, 0x44, 0x46, 0x23, 0xE0, 0x6A, 0xAA, 0xA9, 0xFF, 0xE0, 0x95, 0x55, 0x56, 0x66, 0x60};
const GFXglyph FreeSans6pt7bGlyphs[] PROGMEM = {{0, 0, 0, 3, 0, 1}, // 0x20 ' '
{0, 2, 9, 4, 1, -8}, // 0x21 '!'
{3, 4, 3, 4, 0, -8}, // 0x22 '"'
{5, 7, 8, 7, 0, -7}, // 0x23 '#'
{12, 6, 11, 7, 0, -9}, // 0x24 '$'
{21, 10, 9, 11, 0, -8}, // 0x25 '%'
{33, 7, 9, 8, 1, -8}, // 0x26 '&'
{41, 1, 3, 2, 1, -8}, // 0x27 '''
{42, 2, 11, 4, 1, -8}, // 0x28 '('
{45, 3, 11, 4, 0, -8}, // 0x29 ')'
{50, 4, 3, 5, 0, -8}, // 0x2A '*'
{52, 5, 5, 7, 1, -4}, // 0x2B '+'
{56, 1, 3, 3, 1, 0}, // 0x2C ','
{57, 2, 1, 4, 1, -3}, // 0x2D '-'
{58, 1, 1, 3, 1, 0}, // 0x2E '.'
{59, 3, 9, 3, 0, -8}, // 0x2F '/'
{63, 5, 9, 7, 1, -8}, // 0x30 '0'
{69, 3, 9, 7, 1, -8}, // 0x31 '1'
{73, 6, 9, 7, 0, -8}, // 0x32 '2'
{80, 6, 9, 7, 0, -8}, // 0x33 '3'
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
{94, 6, 9, 7, 0, -8}, // 0x35 '5'
{101, 5, 9, 7, 1, -8}, // 0x36 '6'
{107, 5, 9, 7, 1, -8}, // 0x37 '7'
{113, 6, 9, 7, 0, -8}, // 0x38 '8'
{120, 6, 9, 7, 0, -8}, // 0x39 '9'
{127, 1, 7, 3, 1, -6}, // 0x3A ':'
{128, 1, 8, 3, 1, -5}, // 0x3B ';'
{129, 5, 6, 7, 1, -5}, // 0x3C '<'
{133, 5, 3, 7, 1, -3}, // 0x3D '='
{135, 5, 6, 7, 1, -5}, // 0x3E '>'
{139, 5, 9, 7, 1, -8}, // 0x3F '?'
{145, 11, 11, 12, 0, -8}, // 0x40 '@'
{161, 8, 9, 8, 0, -8}, // 0x41 'A'
{170, 6, 9, 8, 1, -8}, // 0x42 'B'
{177, 8, 9, 9, 0, -8}, // 0x43 'C'
{186, 7, 9, 8, 1, -8}, // 0x44 'D'
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
{208, 8, 9, 9, 0, -8}, // 0x47 'G'
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
{282, 9, 10, 9, 0, -8}, // 0x51 'Q'
{294, 7, 9, 9, 1, -8}, // 0x52 'R'
{302, 6, 9, 8, 1, -8}, // 0x53 'S'
{309, 7, 9, 8, 0, -8}, // 0x54 'T'
{317, 7, 9, 9, 1, -8}, // 0x55 'U'
{325, 8, 9, 8, 0, -8}, // 0x56 'V'
{334, 11, 9, 11, 0, -8}, // 0x57 'W'
{347, 8, 9, 8, 0, -8}, // 0x58 'X'
{356, 8, 9, 8, 0, -8}, // 0x59 'Y'
{365, 7, 9, 7, 0, -8}, // 0x5A 'Z'
{373, 2, 12, 3, 1, -8}, // 0x5B '['
{376, 3, 9, 3, 0, -8}, // 0x5C '\'
{380, 3, 12, 3, 0, -8}, // 0x5D ']'
{385, 4, 5, 6, 1, -8}, // 0x5E '^'
{388, 7, 1, 7, 0, 2}, // 0x5F '_'
{389, 3, 1, 3, 0, -8}, // 0x60 '`'
{390, 6, 7, 7, 0, -6}, // 0x61 'a'
{396, 5, 9, 7, 1, -8}, // 0x62 'b'
{402, 6, 7, 6, 0, -6}, // 0x63 'c'
{408, 6, 9, 7, 0, -8}, // 0x64 'd'
{415, 6, 7, 6, 0, -6}, // 0x65 'e'
{421, 3, 9, 3, 0, -8}, // 0x66 'f'
{425, 6, 10, 7, 0, -6}, // 0x67 'g'
{433, 5, 9, 6, 1, -8}, // 0x68 'h'
{439, 1, 9, 3, 1, -8}, // 0x69 'i'
{441, 2, 12, 3, 0, -8}, // 0x6A 'j'
{444, 5, 9, 6, 1, -8}, // 0x6B 'k'
{450, 1, 9, 3, 1, -8}, // 0x6C 'l'
{452, 8, 7, 10, 1, -6}, // 0x6D 'm'
{459, 5, 7, 6, 1, -6}, // 0x6E 'n'
{464, 6, 7, 6, 0, -6}, // 0x6F 'o'
{470, 5, 9, 7, 1, -6}, // 0x70 'p'
{476, 6, 9, 7, 0, -6}, // 0x71 'q'
{483, 3, 7, 4, 1, -6}, // 0x72 'r'
{486, 6, 7, 6, 0, -6}, // 0x73 's'
{492, 3, 8, 3, 0, -7}, // 0x74 't'
{495, 5, 7, 6, 1, -6}, // 0x75 'u'
{500, 6, 7, 6, 0, -6}, // 0x76 'v'
{506, 9, 7, 9, 0, -6}, // 0x77 'w'
{514, 6, 7, 6, 0, -6}, // 0x78 'x'
{520, 6, 10, 6, 0, -6}, // 0x79 'y'
{528, 5, 7, 6, 0, -6}, // 0x7A 'z'
{533, 2, 12, 4, 1, -8}, // 0x7B '{'
{536, 1, 11, 3, 1, -8}, // 0x7C '|'
{538, 2, 12, 4, 1, -8}, // 0x7D '}'
{541, 6, 2, 6, 0, -4}}; // 0x7E '~'
const GFXfont FreeSans6pt7b PROGMEM = {(uint8_t *)FreeSans6pt7bBitmaps, (GFXglyph *)FreeSans6pt7bGlyphs, 0x20, 0x7E, 14};
// Approx. 1215 bytes

View File

@@ -0,0 +1,302 @@
/*
Uses Windows-1251 encoding to map translingual Cyrillic characters to range between (uint8_t)127 and (uint8_t)255
https://en.wikipedia.org/wiki/Windows-1251
Cyrillic characters present to the firmware as UTF8.
A NicheGraphics implementation needs to identify these, and substitute the appropriate Windows-1251 char value.
*/
#pragma once
const uint8_t FreeSans6pt8bCyrillicBitmaps[] PROGMEM = {
0xFF, 0xA0, 0xC0, 0xFF, 0xA0, 0xC0, 0xB6, 0x80, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x31, 0x75, 0x54, 0x78, 0x79, 0x75,
0x7C, 0x41, 0x00, 0x01, 0x1C, 0x49, 0x22, 0x50, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0xC0, 0x21, 0x44, 0x94, 0x62, 0x59, 0xE2,
0xF4, 0xE0, 0x6A, 0xAA, 0x90, 0x48, 0x92, 0x49, 0x4A, 0x00, 0x5D, 0x40, 0x21, 0x09, 0xF2, 0x10, 0xE0, 0xC0, 0x80, 0x25, 0x25,
0x24, 0x26, 0xA3, 0x18, 0xC6, 0x31, 0xF0, 0x27, 0x92, 0x49, 0x20, 0x11, 0xB4, 0x41, 0x0C, 0xC6, 0x10, 0xFC, 0x26, 0xA2, 0x13,
0x04, 0x31, 0xF0, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0xFF, 0xE1, 0x4D, 0x84, 0x31, 0xF0, 0x26, 0xE3, 0x0F, 0x46, 0x31,
0xF0, 0xFF, 0xC4, 0x22, 0x11, 0x08, 0x40, 0x11, 0xA4, 0x51, 0x39, 0x1C, 0x51, 0x78, 0x11, 0xA4, 0x71, 0x45, 0xF0, 0x51, 0x78,
0xC0, 0x30, 0xC0, 0x36, 0x1F, 0x20, 0xE0, 0x80, 0xF8, 0x3E, 0xC1, 0xC2, 0xE8, 0x00, 0x74, 0x62, 0x11, 0x10, 0x80, 0x20, 0x0F,
0x06, 0x18, 0x81, 0xA7, 0xD4, 0x93, 0x22, 0x64, 0x4A, 0x7E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x1C, 0x24, 0x24, 0x7E,
0x42, 0x42, 0xC3, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xF9, 0x1A, 0x1C,
0x18, 0x30, 0x60, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x3C, 0x46,
0x82, 0x80, 0x8F, 0x81, 0x83, 0xC3, 0x7D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x86,
0x31, 0x78, 0x87, 0x1A, 0x65, 0x8F, 0x1A, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
0xA5, 0x99, 0x99, 0x99, 0x83, 0x87, 0x8D, 0x19, 0x32, 0x62, 0xC3, 0x86, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC2,
0x3E, 0x00, 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x3F, 0x00, 0xFD,
0x0E, 0x0C, 0x1F, 0xD0, 0xA0, 0xC1, 0x82, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08,
0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0x7C, 0xC3, 0x42, 0x42, 0x26, 0x24, 0x24, 0x14, 0x18, 0x18, 0xC4, 0x28, 0xC5,
0x39, 0xA5, 0x24, 0xA4, 0x52, 0x8C, 0x71, 0x8C, 0x30, 0x80, 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, 0xC3, 0x42, 0x26, 0x24,
0x18, 0x18, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x10, 0x41, 0x06, 0x08, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24, 0x89, 0x20, 0xED,
0xB6, 0xDB, 0x6D, 0xF0, 0x46, 0xAA, 0x90, 0xFC, 0x90, 0xFC, 0x4F, 0x98, 0xFC, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0xF0, 0x79, 0x18,
0x20, 0x45, 0xE0, 0x04, 0x10, 0x5F, 0xC6, 0x18, 0x51, 0x7C, 0xFC, 0x7F, 0x08, 0xF8, 0x29, 0x74, 0x92, 0x40, 0x7D, 0x18, 0x61,
0x45, 0xF0, 0x52, 0x30, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0x88, 0xDF, 0x80, 0x51, 0x55, 0x56, 0x84, 0x21, 0x2A, 0x72, 0x92, 0x98,
0xFF, 0x80, 0xFF, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFC, 0x63, 0x18, 0xC4, 0x79, 0x18, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xFA,
0x10, 0x80, 0x7D, 0x18, 0x61, 0x45, 0xF0, 0x41, 0x04, 0xF2, 0x49, 0x00, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0x4B, 0xA4, 0x93, 0x8C,
0x63, 0x18, 0xFC, 0xCD, 0x24, 0x94, 0x30, 0xC0, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, 0x96, 0x66, 0x99, 0xCA, 0x52, 0x63, 0x18,
0x84, 0x40, 0x78, 0xC4, 0x44, 0x7C, 0x6A, 0xAA, 0xA9, 0xFF, 0xF0, 0xC9, 0x24, 0x4A, 0x49, 0x40, 0xE8, 0xC0, 0xFE, 0x18, 0x61,
0x86, 0x18, 0x61, 0xFC, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, 0x10, 0x8F, 0xE0, 0x82,
0x08, 0x20, 0x82, 0x08, 0x00, 0x64, 0x0F, 0x88, 0x88, 0x80, 0x3D, 0x0C, 0x2E, 0xF9, 0x04, 0x0F, 0x7C, 0x08, 0x81, 0x10, 0x22,
0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0,
0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0x83,
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, 0x78, 0x24, 0x13,
0xC9, 0x14, 0x8E, 0x7C, 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, 0x60, 0x9A, 0xCC, 0xA9, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51,
0x44, 0x8C, 0x63, 0x18, 0xFC, 0x80, 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, 0x70, 0x22, 0x95, 0xA8, 0xC4,
0x23, 0x10, 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, 0x28, 0x0F, 0xE0, 0x82, 0x0F,
0xE0, 0x82, 0x0F, 0xC0, 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, 0x51, 0x55, 0x56, 0xA1, 0x24, 0x92, 0x49, 0x00, 0xFF,
0x80, 0xDF, 0x80, 0x27, 0xC9, 0x24, 0x8A, 0x28, 0xA2, 0x8B, 0xF8, 0x20, 0x80, 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, 0x88,
0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x89, 0x80, 0x79, 0x1F, 0x30, 0x45, 0xE0, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61,
0x7C, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0xB4, 0x24, 0x92, 0x40, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, 0xFE, 0x08,
0x20, 0xFE, 0x18, 0x61, 0xFC, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, 0x1F, 0x08,
0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0x88, 0xA4, 0x9A,
0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68,
0xE1, 0xC2, 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, 0x3E, 0x44,
0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60,
0xC1, 0x82, 0x3C, 0x46, 0x83, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, 0xFA, 0x18,
0x61, 0xFE, 0x08, 0x20, 0x80, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10,
0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, 0x08, 0x1E, 0x32, 0xD1, 0x38, 0x8C, 0x4F, 0x2C, 0xFC, 0x08, 0x00, 0x87, 0x34,
0x8C, 0x30, 0xC4, 0xB3, 0x84, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, 0x8E, 0x38, 0xE3, 0x8D, 0xF0,
0xC3, 0x0C, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF,
0x80, 0x40, 0x20, 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC,
0x2E, 0x17, 0x0B, 0xF9, 0x80, 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, 0x87,
0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, 0x79, 0x11, 0xD9,
0xCD, 0xD0, 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, 0xF4, 0xBD, 0x29, 0xF8, 0xF8, 0x88, 0x88, 0x3C, 0x48, 0x91, 0x22, 0x5F,
0xE0, 0x80, 0x79, 0x1F, 0xF0, 0x45, 0xE0, 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, 0x78, 0x23, 0x82, 0xCD, 0xE0, 0x9C, 0xEB, 0x5C,
0xC4, 0x70, 0x27, 0x3A, 0xD7, 0x31, 0x9A, 0xCC, 0xA9, 0x7A, 0x52, 0x94, 0xE4, 0x8F, 0x3D, 0x6D, 0xA6, 0x90, 0x8C, 0x7F, 0x18,
0xC4, 0x79, 0x1C, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xC4, 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, 0x79, 0x1C, 0x30, 0x45, 0xE0,
0xF9, 0x08, 0x42, 0x10, 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04,
0x01, 0x00, 0x40, 0x4B, 0x8C, 0x65, 0xE4, 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, 0x99, 0x97, 0x11, 0x96, 0x59, 0x65, 0x97, 0xF0,
0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, 0x86, 0x1F, 0x63, 0x8F, 0xD0, 0x84, 0x3D, 0x18,
0xF8, 0xF4, 0xDE, 0x19, 0xF8, 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, 0xFC, 0x7E, 0xD4, 0xC4,
};
const GFXglyph FreeSans6pt8bCyrillicGlyphs[] PROGMEM = {
{0, 0, 0, 3, 0, 0}, // 0x20 ' '
{3, 2, 9, 3, 1, -8}, // 0x21 '!'
{6, 3, 3, 4, 1, -8}, // 0x22 '"'
{8, 7, 8, 7, 0, -7}, // 0x23 '#'
{15, 6, 11, 7, 0, -8}, // 0x24 '$'
{24, 10, 9, 11, 0, -8}, // 0x25 '%'
{36, 6, 9, 8, 1, -8}, // 0x26 '&'
{43, 1, 3, 2, 1, -8}, // 0x27 '''
{44, 2, 10, 4, 1, -7}, // 0x28 '('
{47, 3, 11, 4, 0, -7}, // 0x29 ')'
{52, 3, 4, 5, 1, -8}, // 0x2A '*'
{54, 5, 6, 7, 1, -5}, // 0x2B '+'
{58, 1, 3, 3, 1, 0}, // 0x2C ','
{59, 2, 1, 4, 1, -3}, // 0x2D '-'
{60, 1, 1, 3, 1, 0}, // 0x2E '.'
{61, 3, 8, 3, 0, -7}, // 0x2F '/'
{64, 5, 9, 7, 1, -8}, // 0x30 '0'
{70, 3, 9, 7, 1, -8}, // 0x31 '1'
{74, 6, 9, 7, 0, -8}, // 0x32 '2'
{81, 5, 9, 7, 1, -8}, // 0x33 '3'
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
{94, 5, 9, 7, 1, -8}, // 0x35 '5'
{100, 5, 9, 7, 1, -8}, // 0x36 '6'
{106, 5, 9, 7, 1, -8}, // 0x37 '7'
{112, 6, 9, 7, 0, -8}, // 0x38 '8'
{119, 6, 9, 7, 0, -8}, // 0x39 '9'
{126, 2, 6, 3, 1, -5}, // 0x3A ':'
{128, 2, 8, 3, 1, -5}, // 0x3B ';'
{130, 5, 5, 7, 1, -4}, // 0x3C '<'
{134, 5, 3, 7, 1, -3}, // 0x3D '='
{136, 5, 5, 7, 1, -4}, // 0x3E '>'
{140, 5, 9, 7, 1, -8}, // 0x3F '?'
{146, 11, 11, 12, 0, -8}, // 0x40 '@'
{162, 8, 9, 8, 0, -8}, // 0x41 'A'
{171, 6, 9, 8, 1, -8}, // 0x42 'B'
{178, 7, 9, 9, 1, -8}, // 0x43 'C'
{186, 7, 9, 9, 1, -8}, // 0x44 'D'
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
{208, 8, 9, 9, 1, -8}, // 0x47 'G'
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
{282, 9, 9, 9, 0, -8}, // 0x51 'Q'
{293, 7, 9, 9, 1, -8}, // 0x52 'R'
{301, 6, 9, 8, 1, -8}, // 0x53 'S'
{308, 7, 9, 7, 0, -8}, // 0x54 'T'
{316, 7, 9, 9, 1, -8}, // 0x55 'U'
{324, 8, 9, 8, 0, -8}, // 0x56 'V'
{333, 11, 9, 11, 0, -8}, // 0x57 'W'
{346, 6, 9, 8, 1, -8}, // 0x58 'X'
{353, 8, 9, 8, 0, -8}, // 0x59 'Y'
{362, 7, 9, 7, 0, -8}, // 0x5A 'Z'
{370, 2, 12, 3, 1, -8}, // 0x5B '['
{373, 3, 9, 3, 0, -8}, // 0x5C '\'
{377, 3, 12, 3, 0, -8}, // 0x5D ']'
{382, 4, 5, 6, 1, -8}, // 0x5E '^'
{385, 6, 1, 7, 0, 2}, // 0x5F '_'
{386, 2, 2, 4, 1, -8}, // 0x60 '`'
{387, 5, 6, 7, 1, -5}, // 0x61 'a'
{391, 5, 9, 7, 1, -8}, // 0x62 'b'
{397, 6, 6, 6, 0, -5}, // 0x63 'c'
{402, 6, 9, 7, 0, -8}, // 0x64 'd'
{409, 5, 6, 7, 1, -5}, // 0x65 'e'
{413, 3, 9, 3, 0, -8}, // 0x66 'f'
{417, 6, 9, 7, 0, -5}, // 0x67 'g'
{424, 5, 9, 7, 1, -8}, // 0x68 'h'
{430, 1, 9, 3, 1, -8}, // 0x69 'i'
{432, 2, 12, 3, 0, -8}, // 0x6A 'j'
{435, 5, 9, 6, 1, -8}, // 0x6B 'k'
{441, 1, 9, 3, 1, -8}, // 0x6C 'l'
{443, 8, 6, 10, 1, -5}, // 0x6D 'm'
{449, 5, 6, 7, 1, -5}, // 0x6E 'n'
{453, 6, 6, 7, 0, -5}, // 0x6F 'o'
{458, 5, 9, 7, 1, -5}, // 0x70 'p'
{464, 6, 9, 7, 0, -5}, // 0x71 'q'
{471, 3, 6, 4, 1, -5}, // 0x72 'r'
{474, 6, 6, 6, 0, -5}, // 0x73 's'
{479, 3, 8, 3, 0, -7}, // 0x74 't'
{482, 5, 6, 7, 1, -5}, // 0x75 'u'
{486, 6, 6, 6, 0, -5}, // 0x76 'v'
{491, 8, 6, 9, 0, -5}, // 0x77 'w'
{497, 4, 6, 6, 1, -5}, // 0x78 'x'
{500, 5, 9, 6, 0, -5}, // 0x79 'y'
{506, 5, 6, 6, 0, -5}, // 0x7A 'z'
{510, 2, 12, 4, 1, -8}, // 0x7B '{'
{513, 1, 12, 3, 1, -8}, // 0x7C '|'
{515, 3, 12, 4, 0, -8}, // 0x7D '}'
{520, 5, 2, 7, 1, -4}, // 0x7E '~'
{522, 6, 9, 8, 1, -8}, //
{529, 9, 11, 9, 0, -8}, //
{542, 6, 11, 7, 1, -10}, //
{551, 0, 0, 8, 0, 0}, //
{551, 4, 9, 5, 1, -8}, //
{556, 0, 0, 8, 0, 0}, //
{556, 0, 0, 8, 0, 0}, //
{556, 0, 0, 8, 0, 0}, //
{556, 0, 0, 8, 0, 0}, //
{556, 6, 8, 8, 1, -7}, //
{562, 0, 0, 8, 0, 0}, //
{562, 11, 9, 13, 1, -8}, //
{575, 0, 0, 8, 0, 0}, //
{575, 11, 9, 12, 1, -8}, //
{588, 6, 11, 8, 1, -10}, //
{597, 9, 9, 9, 0, -8}, //
{608, 7, 11, 9, 1, -8}, //
{618, 6, 11, 7, 0, -8}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 0, 0, 8, 0, 0}, //
{627, 9, 6, 10, 0, -5}, //
{634, 0, 0, 8, 0, 0}, //
{634, 9, 6, 10, 1, -5}, //
{641, 4, 8, 6, 1, -7}, //
{645, 6, 9, 7, 0, -8}, //
{652, 5, 7, 7, 1, -5}, //
{657, 0, 0, 8, 0, 0}, //
{657, 7, 11, 7, 0, -10}, //
{667, 5, 11, 6, 0, -7}, //
{674, 5, 9, 6, 0, -8}, //
{680, 0, 0, 8, 0, 0}, //
{680, 6, 10, 7, 1, -9}, //
{688, 0, 0, 8, 0, 0}, //
{688, 0, 0, 8, 0, 0}, //
{688, 6, 11, 8, 1, -10}, //
{697, 7, 9, 9, 1, -8}, //
{705, 0, 0, 8, 0, 0}, //
{705, 0, 0, 8, 0, 0}, //
{705, 2, 12, 3, 0, -8}, //
{708, 0, 0, 8, 0, 0}, //
{708, 0, 0, 8, 0, 0}, //
{708, 3, 11, 3, 0, -10}, //
{713, 0, 0, 8, 0, 0}, //
{713, 0, 0, 8, 0, 0}, //
{713, 1, 9, 3, 1, -8}, //
{715, 1, 9, 3, 1, -8}, //
{717, 3, 8, 5, 1, -7}, //
{720, 6, 9, 7, 1, -5}, //
{727, 0, 0, 8, 0, 0}, //
{727, 0, 0, 8, 0, 0}, //
{727, 6, 9, 7, 0, -8}, //
{734, 9, 9, 11, 1, -8}, //
{745, 6, 6, 6, 0, -5}, //
{750, 0, 0, 8, 0, 0}, //
{750, 0, 0, 8, 0, 0}, //
{750, 6, 9, 8, 1, -8}, //
{757, 6, 6, 6, 0, -5}, //
{762, 3, 9, 3, 0, -8}, //
{766, 8, 9, 8, 0, -8}, //
{775, 6, 9, 8, 1, -8}, //
{782, 6, 9, 8, 1, -8}, //
{789, 6, 9, 7, 1, -8}, //
{796, 9, 11, 10, 0, -8}, //
{809, 6, 9, 8, 1, -8}, //
{816, 9, 9, 11, 1, -8}, //
{827, 6, 9, 8, 1, -8}, //
{834, 7, 9, 9, 1, -8}, //
{842, 7, 11, 9, 1, -10}, //
{852, 6, 9, 8, 1, -8}, //
{859, 7, 9, 8, 0, -8}, //
{867, 8, 9, 10, 1, -8}, //
{876, 7, 9, 9, 1, -8}, //
{884, 8, 9, 10, 1, -8}, //
{893, 7, 9, 9, 1, -8}, //
{901, 6, 9, 8, 1, -8}, //
{908, 7, 9, 9, 1, -8}, //
{916, 7, 9, 7, 0, -8}, //
{924, 7, 9, 7, 0, -8}, //
{932, 9, 9, 10, 1, -8}, //
{943, 6, 9, 8, 1, -8}, //
{950, 8, 11, 9, 1, -8}, //
{961, 6, 9, 8, 1, -8}, //
{968, 8, 9, 10, 1, -8}, //
{977, 9, 11, 10, 1, -8}, //
{990, 10, 9, 10, 0, -8}, //
{1002, 9, 9, 10, 1, -8}, //
{1013, 6, 9, 8, 1, -8}, //
{1020, 7, 9, 9, 1, -8}, //
{1028, 10, 9, 12, 1, -8}, //
{1040, 6, 9, 8, 1, -8}, //
{1047, 6, 6, 7, 0, -5}, //
{1052, 6, 9, 7, 0, -8}, //
{1059, 5, 6, 6, 1, -5}, //
{1063, 4, 6, 5, 1, -5}, //
{1066, 7, 7, 7, 0, -5}, //
{1073, 6, 6, 7, 0, -5}, //
{1078, 8, 6, 9, 1, -5}, //
{1084, 6, 6, 6, 0, -5}, //
{1089, 5, 6, 7, 1, -5}, //
{1093, 5, 8, 7, 1, -7}, //
{1098, 4, 6, 6, 1, -5}, //
{1101, 5, 6, 6, 0, -5}, //
{1105, 6, 6, 7, 1, -5}, //
{1110, 5, 6, 7, 1, -5}, //
{1114, 6, 6, 7, 0, -5}, //
{1119, 5, 6, 7, 1, -5}, //
{1123, 5, 9, 7, 1, -5}, //
{1129, 6, 6, 6, 0, -5}, //
{1134, 5, 6, 5, 0, -5}, //
{1138, 5, 9, 6, 0, -5}, //
{1144, 10, 11, 10, 0, -7}, //
{1158, 5, 6, 6, 0, -5}, //
{1162, 6, 7, 7, 1, -5}, //
{1168, 4, 6, 6, 1, -5}, //
{1171, 6, 6, 8, 1, -5}, //
{1176, 7, 7, 9, 1, -5}, //
{1183, 7, 6, 8, 0, -5}, //
{1189, 6, 6, 8, 1, -5}, //
{1194, 5, 6, 6, 1, -5}, //
{1198, 5, 6, 6, 1, -5}, //
{1202, 8, 6, 9, 1, -5}, //
{1208, 5, 6, 7, 1, -5} //
};
const GFXfont FreeSans6pt8bCyrillic PROGMEM = {(uint8_t *)FreeSans6pt8bCyrillicBitmaps, (GFXglyph *)FreeSans6pt8bCyrillicGlyphs,
0x20, 0xFF, 16};

View File

@@ -0,0 +1,4 @@
# NicheGraphics - Fonts
A common area to store fonts which might be reused by different Niche Graphics UIs
In future, we may want to separate these by library (AdafruitGFX, u8g2, etc)

View File

@@ -0,0 +1,948 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Applet.h"
#include "main.h"
#include "RTC.h"
using namespace NicheGraphics;
InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose font. Set by setDefaultFonts
InkHUD::AppletFont InkHUD::Applet::fontSmall; // General purpose font. Set by setDefaultFonts
constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo
InkHUD::Applet::Applet() : GFX(0, 0)
{
// GFX is given initial dimensions of 0
// The width and height will change dynamically, depending on Applet tiling
// If you're getting a "divide by zero error", consider it an assert:
// WindowManager should be the only one controlling the rendering
inkhud = InkHUD::getInstance();
settings = &inkhud->persistence->settings;
latestMessage = &inkhud->persistence->latestMessage;
}
// Draw a single pixel
// The raw pixel output generated by AdafruitGFX drawing all passes through here
// Hand off to the applet's tile, which will in-turn pass to the renderer
void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
{
// Only render pixels if they fall within user's cropped region
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
assignedTile->handleAppletPixel(x, y, (Color)color);
}
// Link our applet to a tile
// This can only be called by Tile::assignApplet
// The tile determines the applets dimensions
// Pixel output is passed to tile during render()
void InkHUD::Applet::setTile(Tile *t)
{
// If we're setting (not clearing), make sure the link is "reciprocal"
if (t)
assert(t->getAssignedApplet() == this);
assignedTile = t;
}
// The tile to which our applet is assigned
InkHUD::Tile *InkHUD::Applet::getTile()
{
return assignedTile;
}
// Draw the applet
void InkHUD::Applet::render()
{
assert(assignedTile); // Ensure that we have a tile
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
// WindowManager::update has now consumed the info about our update request
// Clear everything for future requests
wantRender = false; // Flag set by requestUpdate
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
updateDimensions();
resetDrawingSpace();
onRender(); // Derived applet's drawing takes place here
// Handle "Tile Highlighting"
// Some devices may use an auxiliary button to switch between tiles
// When this happens, we temporarily highlight the newly focused tile with a border
// If our tile is (or was) highlighted, to indicate a change in focus
if (Tile::highlightTarget == assignedTile) {
// Draw the highlight
if (!Tile::highlightShown) {
drawRect(0, 0, width(), height(), BLACK);
Tile::startHighlightTimeout();
Tile::highlightShown = true;
}
// Clear the highlight
else {
Tile::cancelHighlightTimeout();
Tile::highlightShown = false;
Tile::highlightTarget = nullptr;
}
}
}
// Does the applet want to render now?
// Checks whether the applet called requestUpdate recently, in response to an event
// Used by WindowManager::update
bool InkHUD::Applet::wantsToRender()
{
return wantRender;
}
// Does the applet want to be moved to foreground before next render, to show new data?
// User specifies whether an applet has permission for this, using the on-screen menu
// Used by WindowManager::update
bool InkHUD::Applet::wantsToAutoshow()
{
return wantAutoshow;
}
// Which technique would this applet prefer that the display use to change the image?
// Used by WindowManager::update
Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
{
return wantUpdateType;
}
// Get size of the applet's drawing space from its tile
// Performed immediately before derived applet's drawing code runs
void InkHUD::Applet::updateDimensions()
{
assert(assignedTile);
WIDTH = assignedTile->getWidth();
HEIGHT = assignedTile->getHeight();
_width = WIDTH;
_height = HEIGHT;
}
// Ensure that render() always starts with the same initial drawing config
void InkHUD::Applet::resetDrawingSpace()
{
resetCrop(); // Allow pixel from any region of the applet to draw
setTextColor(BLACK); // Reset text params
setCursor(0, 0);
setTextWrap(false);
setFont(fontSmall);
}
// Tell InkHUD::Renderer that we want to render now
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
// When an applet decides it has heard something important, and wants to redraw, it calls this method
// Once the renderer has given other applets a chance to process whatever event we just detected,
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
{
wantRender = true;
wantUpdateType = type;
inkhud->requestUpdate();
}
// Ask window manager to move this applet to foreground at start of next render
// Users select which applets have permission for this using the on-screen menu
void InkHUD::Applet::requestAutoshow()
{
wantAutoshow = true;
}
// Called when an Applet begins running
// Active applets are considered "enabled"
// They should now listen for events, and request their own updates
// They may also be unexpectedly renderer at any time by other InkHUD components
// Applets can be activated at run-time through the on-screen menu
void InkHUD::Applet::activate()
{
onActivate(); // Call derived class' handler
active = true;
}
// Called when an Applet stops running
// Inactive applets are considered "disabled"
// They should not listen for events, process data
// They will not be rendered
// Applets can be deactivated at run-time through the on-screen menu
void InkHUD::Applet::deactivate()
{
// If applet is still in foreground, run its onBackground code first
if (isForeground())
sendToBackground();
// If applet is active, run its onDeactivate code first
if (isActive())
onDeactivate(); // Derived class' handler
active = false;
}
// Is the Applet running?
// Note: active / inactive is not related to background / foreground
// An inactive applet is *fully* disabled
bool InkHUD::Applet::isActive()
{
return active;
}
// Begin showing the Applet
// It will be rendered immediately to whichever tile it is assigned
// The Renderer will also now honor requestUpdate() calls from this applet
void InkHUD::Applet::bringToForeground()
{
if (!foreground) {
foreground = true;
onForeground(); // Run derived applet class' handler
}
requestUpdate();
}
// Stop showing the Applet
// Calls to requestUpdate() will no longer be honored
// When one applet moves to background, another should move to foreground (exception: some system applets)
void InkHUD::Applet::sendToBackground()
{
if (foreground) {
foreground = false;
onBackground(); // Run derived applet class' handler
}
}
// Is the applet currently displayed on a tile
// Note: in some uncommon situations, an applet may be "foreground", and still not visible.
// This can occur when a system applet is covering the screen (e.g. during BLE pairing)
// This is not our applets responsibility to handle,
// as in those situations, the system applet will have "locked" rendering
bool InkHUD::Applet::isForeground()
{
return foreground;
}
// Limit drawing to a certain region of the applet
// Pixels outside this region will be discarded
void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height)
{
cropLeft = left;
cropTop = top;
cropWidth = width;
cropHeight = height;
}
// Allow drawing to any region of the Applet
// Reverses Applet::setCrop
void InkHUD::Applet::resetCrop()
{
setCrop(0, 0, width(), height());
}
// Convert relative width to absolute width, in px
// X(0) is 0
// X(0.5) is width() / 2
// X(1) is width()
uint16_t InkHUD::Applet::X(float f)
{
return width() * f;
}
// Convert relative hight to absolute height, in px
// Y(0) is 0
// Y(0.5) is height() / 2
// Y(1) is height()
uint16_t InkHUD::Applet::Y(float f)
{
return height() * f;
}
// Print text, specifying the position of any edge / corner of the textbox
void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va)
{
printAt(x, y, std::string(text), ha, va);
}
// Print text, specifying the position of any edge / corner of the textbox
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
{
// Custom font
// - set with AppletFont::addSubstitution
// - find certain UTF8 chars
// - replace with glyph from custom font (or suitable ASCII addSubstitution?)
getFont().applySubstitutions(&text);
// We do still have to run getTextBounds to find the width
int16_t textOffsetX, textOffsetY;
uint16_t textWidth, textHeight;
getTextBounds(text.c_str(), 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight);
int16_t cursorX = 0;
int16_t cursorY = 0;
switch (ha) {
case LEFT:
cursorX = x - textOffsetX;
break;
case CENTER:
cursorX = (x - textOffsetX) - (textWidth / 2);
break;
case RIGHT:
cursorX = (x - textOffsetX) - textWidth;
break;
}
// We're using a fixed line height, rather than sizing to text (getTextBounds)
switch (va) {
case TOP:
cursorY = y + currentFont.heightAboveCursor();
break;
case MIDDLE:
cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2);
break;
case BOTTOM:
cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight();
break;
}
setCursor(cursorX, cursorY);
print(text.c_str());
}
// Set which font should be used for subsequent drawing
// This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data
void InkHUD::Applet::setFont(AppletFont f)
{
GFX::setFont(f.gfxFont);
currentFont = f;
}
// Get which font is currently being used for drawing
// This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data
InkHUD::AppletFont InkHUD::Applet::getFont()
{
return currentFont;
}
// Gets rendered width of a string
// Wrapper for getTextBounds
uint16_t InkHUD::Applet::getTextWidth(const char *text)
{
// We do still have to run getTextBounds to find the width
int16_t textOffsetX, textOffsetY;
uint16_t textWidth, textHeight;
getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight);
return textWidth;
}
// Gets rendered width of a string
// Wrapper for getTextBounds
uint16_t InkHUD::Applet::getTextWidth(std::string text)
{
getFont().applySubstitutions(&text);
return getTextWidth(text.c_str());
}
// Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels
// Roughly comparable to values used by the iOS app;
// I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator
InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi)
{
uint8_t score = 0;
// Give a score for the SNR
if (snr > -17.5)
score += 2;
else if (snr > -26.0)
score += 1;
// Give a score for the RSSI
if (rssi > -115.0)
score += 3;
else if (rssi > -120.0)
score += 2;
else if (rssi > -126.0)
score += 1;
// Combine scores, then give a result
if (score >= 5)
return SIGNAL_GOOD;
else if (score >= 4)
return SIGNAL_FAIR;
else if (score > 0)
return SIGNAL_BAD;
else
return SIGNAL_NONE;
}
// Apply the standard "node id" formatting to a nodenum int: !0123abdc
std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
{
// Not found in nodeDB, show a hex nodeid instead
char nodeIdHex[10];
sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format
return std::string(nodeIdHex);
}
// Print text, with word wrapping
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
{
// Custom font glyphs
// - set with AppletFont::addSubstitution
// - find certain UTF8 chars
// - replace with glyph from custom font (or suitable ASCII addSubstitution?)
getFont().applySubstitutions(&text);
// Place the AdafruitGFX cursor to suit our "top" coord
setCursor(left, top + getFont().heightAboveCursor());
// How wide a space character is
// Used when simulating print, for dimensioning
// Works around issues where getTextDimensions() doesn't account for whitespace
const uint8_t wSp = getFont().widthBetweenWords();
// Move through our text, character by character
uint16_t wordStart = 0;
for (uint16_t i = 0; i < text.length(); i++) {
// Found: end of word (split by spaces or newline)
// Also handles end of string
if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) {
// Isolate this word
uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1
std::string word = text.substr(wordStart, wordLength);
wordStart = i + 1; // Next word starts *after* the space
// If word is terminated by a newline char, don't actually print it.
// We'll manually add a new line later
if (word.back() == '\n')
word.pop_back();
// Measure the word, in px
int16_t l, t;
uint16_t w, h;
getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h);
// Word is short
if (w < width) {
// Word fits on current line
if ((l + w + wSp) < left + width)
print(word.c_str());
// Word doesn't fit on current line
else {
setCursor(left, getCursorY() + getFont().lineHeight()); // Newline
print(word.c_str());
}
}
// Word is really long
// (wider than applet)
else {
// Horribly inefficient:
// Rather than working directly with the glyph sizes,
// we're going to run everything through getTextBounds as a c-string of length 1
// This is because AdafruitGFX has special internal handling for their legacy 6x8 font,
// which would be a pain to add manually here.
// These super-long strings probably don't come up often so we can maybe tolerate this.
// Todo: rewrite making use of AdafruitGFX native text wrapping
char cstr[] = {0, 0};
int16_t l, t;
uint16_t w, h;
for (uint16_t c = 0; c < word.length(); c++) {
// Shove next char into a c string
cstr[0] = word[c];
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
// Manual newline, if next character will spill beyond screen edge
if ((l + w) > left + width)
setCursor(left, getCursorY() + getFont().lineHeight());
// Print next character
print(word[c]);
}
}
}
// If word was terminated by a newline char, manually add the new line now
if (text[i] == '\n') {
setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline
wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line
}
}
}
// Simulate running printWrapped, to determine how tall the block of text will be.
// This is a wasteful way of handling things. Maybe some way to optimize in future?
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
{
// Cache the current crop region
int16_t cL = cropLeft;
int16_t cT = cropTop;
uint16_t cW = cropWidth;
uint16_t cH = cropHeight;
setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels
printWrapped(left, 0, width, text); // Simulate only - no pixels drawn
// Restore previous crop region
cropLeft = cL;
cropTop = cT;
cropWidth = cW;
cropHeight = cH;
// Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val,
// so we need to account for that when determining the height
return (getCursorY() + getFont().heightBelowCursor());
}
// Fill a region with sparse diagonal lines, to create a pseudo-translucent fill
void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color)
{
// Cache the currently cropped region
int16_t oldCropL = cropLeft;
int16_t oldCropT = cropTop;
uint16_t oldCropW = cropWidth;
uint16_t oldCropH = cropHeight;
setCrop(x, y, w, h);
// Draw lines starting along the top edge, every few px
for (int16_t ix = x; ix < x + w; ix += spacing) {
for (int16_t i = 0; i < w || i < h; i++) {
drawPixel(ix + i, y + i, color);
}
}
// Draw lines starting along the left edge, every few px
for (int16_t iy = y; iy < y + h; iy += spacing) {
for (int16_t i = 0; i < w || i < h; i++) {
drawPixel(x + i, iy + i, color);
}
}
// Restore any previous crop
// If none was set, this will clear
cropLeft = oldCropL;
cropTop = oldCropT;
cropWidth = oldCropW;
cropHeight = oldCropH;
}
// Get a human readable time representation of an epoch time (seconds since 1970)
// If time is invalid, this will be an empty string
std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds)
{
#ifdef BUILD_EPOCH
constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build
#else
constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT
#endif
uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true);
int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY;
int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR;
// Times are invalid: rtc is much older than when code was built
// Don't give any human readable string
if (epochNow <= validAfterEpoch)
return "";
// Times are invalid: argument time is significantly ahead of RTC
// Don't give any human readable string
if (daysAgo < -2)
return "";
// Times are probably invalid: more than 6 months ago
if (daysAgo > 6 * 30)
return "";
if (daysAgo > 1)
return to_string(daysAgo) + " days ago";
else if (hoursAgo > 18)
return "Yesterday";
else {
uint32_t hms = epochSeconds % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m
uint32_t hour = hms / SEC_PER_HOUR;
uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
// Format the clock string
char clockStr[11];
sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM");
return clockStr;
}
}
// If no argument specified, get time string for the current RTC time
std::string InkHUD::Applet::getTimeString()
{
return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true));
}
// Calculate how many nodes have been seen within our preferred window of activity
// This period is set by user, via the menu
// Todo: optimize to calculate once only per WindowManager::render
uint16_t InkHUD::Applet::getActiveNodeCount()
{
// Don't even try to count nodes if RTC isn't set
// The last heard values in nodedb will be incomprehensible
if (getRTCQuality() == RTCQualityNone)
return 0;
uint16_t count = 0;
// For each node in db
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Check if heard recently, and not our own node
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
count++;
}
return count;
}
// Get an abbreviated, human readable, distance string
// Honors config.display.units, to offer both metric and imperial
std::string InkHUD::Applet::localizeDistance(uint32_t meters)
{
constexpr float FEET_PER_METER = 3.28084;
constexpr uint16_t FEET_PER_MILE = 5280;
// Resulting string
std::string localized;
// Imperial
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
uint32_t feet = meters * FEET_PER_METER;
// Distant (miles, rounded)
if (feet > FEET_PER_MILE / 2) {
localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE));
localized += "mi";
}
// Nearby (feet)
else {
localized += to_string(feet);
localized += "ft";
}
}
// Metric
else {
// Distant (kilometers, rounded)
if (meters >= 500) {
localized += to_string((uint32_t)roundf(meters / 1000.0));
localized += "km";
}
// Nearby (meters)
else {
localized += to_string(meters);
localized += "m";
}
}
return localized;
}
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
{
// How many times to draw along x axis
int16_t xStart;
int16_t xEnd;
switch (thicknessX) {
case 0:
assert(false);
case 1:
xStart = xCenter;
xEnd = xCenter;
break;
case 2:
xStart = xCenter;
xEnd = xCenter + 1;
break;
default:
xStart = xCenter - (thicknessX / 2);
xEnd = xCenter + (thicknessX / 2);
}
// How many times to draw along Y axis
int16_t yStart;
int16_t yEnd;
switch (thicknessY) {
case 0:
assert(false);
case 1:
yStart = yCenter;
yEnd = yCenter;
break;
case 2:
yStart = yCenter;
yEnd = yCenter + 1;
break;
default:
yStart = yCenter - (thicknessY / 2);
yEnd = yCenter + (thicknessY / 2);
}
// Print multiple times, overlapping
for (int16_t x = xStart; x <= xEnd; x++) {
for (int16_t y = yStart; y <= yEnd; y++) {
printAt(x, y, text, CENTER, MIDDLE);
}
}
}
// Allow this applet to suppress notifications
// Asked before a notification is shown via the NotificationApplet
// An applet might want to suppress a notification if the applet itself already displays this info
// Example: AllMessageApplet should not approve notifications for messages, if it is in foreground
bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n)
{
// By default, no objection
return true;
}
// Draw the standard header, used by most Applets
/*
┌───────────────────────────────┐
│ Applet::name here │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ │
│ │
└───────────────────────────────┘
*/
void InkHUD::Applet::drawHeader(std::string text)
{
// Y position for divider
// - between header text and messages
constexpr int16_t padDivH = 2;
const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1;
// Print header
printAt(0, padDivH, text);
// Divider
// - below header text: separates message
// - above header text: separates other applets
for (int16_t x = 0; x < width(); x += 2) {
drawPixel(x, 0, BLACK);
drawPixel(x, headerDivY, BLACK); // Dotted 50%
}
}
// Get the height of the standard applet header
// This will vary, depending on font
// Applets use this value to avoid drawing overtop the header
uint16_t InkHUD::Applet::getHeaderHeight()
{
// Y position for divider
// - between header text and messages
constexpr int16_t padDivH = 2;
const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1;
return headerDivY + 1; // "Plus one": height is always one more than Y position
}
// "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio
uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight)
{
// Determine whether we're limited by width or height
// Makes sure we draw the logo as large as possible, within the specified region,
// while still maintaining correct aspect ratio
if (limitWidth > limitHeight * LOGO_ASPECT_RATIO)
return limitHeight * LOGO_ASPECT_RATIO;
else
return limitWidth;
}
// "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio
uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight)
{
// Determine whether we're limited by width or height
// Makes sure we draw the logo as large as possible, within the specified region,
// while still maintaining correct aspect ratio
if (limitHeight > limitWidth / LOGO_ASPECT_RATIO)
return limitWidth / LOGO_ASPECT_RATIO;
else
return limitHeight;
}
// Draw a scalable Meshtastic logo
// Make sure to provide dimensions which have the correct aspect ratio (~2)
// Three paths, drawn thick using quads, with one corner "radiused"
/*
- ^
/- /-\
// // \\
// // \\
// // \\
// // \\
*/
void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height)
{
struct Point {
int x;
int y;
};
typedef Point Distance;
int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org.
int16_t logoL = centerX - (width / 2) + (logoTh / 2);
int16_t logoT = centerY - (height / 2) + (logoTh / 2);
int16_t logoW = width - logoTh;
int16_t logoH = height - logoTh;
int16_t logoR = logoL + logoW - 1;
int16_t logoB = logoT + logoH - 1;
// Points for paths (a, b, and c)
/*
+-----------------------------+
--| a2 b2/c1 |
| |
| |
| |
--| a1 b1 c2 |
+-----------------------------+
| | | |
*/
Point a1 = {map(0, 0, 3, logoL, logoR), logoB};
Point a2 = {map(1, 0, 3, logoL, logoR), logoT};
Point b1 = {map(1, 0, 3, logoL, logoR), logoB};
Point b2 = {map(2, 0, 3, logoL, logoR), logoT};
Point c1 = {map(2, 0, 3, logoL, logoR), logoT};
Point c2 = {map(3, 0, 3, logoL, logoR), logoB};
// Find angle of the path(s)
// Used to thicken the single pixel paths
/*
+-------------------------------+
| a2 |
| -| |
| -/ | |
| -/ | |
| -/# | |
| -/ # | |
| / # | |
| a1---------- |
+-------------------------------+
*/
Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)};
float angle = tanh((float)deltaA.y / deltaA.x);
// Distance (at right angle to the paths), which will give corners for our "quads"
// The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner
/*
| a2
| .
| ..
| aq1 ..
| # ..
| | # ..
|fromPath.y | # ..
| +----a1
|
| fromPath.x
+--------------------------------
*/
Distance fromPath;
fromPath.x = cos(radians(90) - angle) * logoTh * 0.5;
fromPath.y = sin(radians(90) - angle) * logoTh * 0.5;
// Make the paths thick
// Corner points for the rectangles (quads):
/*
aq2
a2
/ aq3
/
/
aq1 /
a1
aq3
*/
// Filled as two triangles per quad:
/*
aq2 #
# ###
## # aq3
## ### -
## #### -/
## ### -/
## #### -/
aq1 ## -/
--- -/
\---aq4
*/
// Make the path thick: path a becomes quad a
Point aq1{a1.x - fromPath.x, a1.y - fromPath.y};
Point aq2{a2.x - fromPath.x, a2.y - fromPath.y};
Point aq3{a2.x + fromPath.x, a2.y + fromPath.y};
Point aq4{a1.x + fromPath.x, a1.y + fromPath.y};
fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, BLACK);
fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, BLACK);
// Make the path thick: path b becomes quad b
Point bq1{b1.x - fromPath.x, b1.y - fromPath.y};
Point bq2{b2.x - fromPath.x, b2.y - fromPath.y};
Point bq3{b2.x + fromPath.x, b2.y + fromPath.y};
Point bq4{b1.x + fromPath.x, b1.y + fromPath.y};
fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, BLACK);
fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, BLACK);
// Make the path thick: path c becomes quad c
Point cq1{c1.x - fromPath.x, c1.y + fromPath.y};
Point cq2{c2.x - fromPath.x, c2.y + fromPath.y};
Point cq3{c2.x + fromPath.x, c2.y - fromPath.y};
Point cq4{c1.x + fromPath.x, c1.y - fromPath.y};
fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, BLACK);
fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, BLACK);
// Radius the intersection of quad b and quad c
/*
b2 / c1
####
## ##
/ \
/ \/ \
/ /\ \
/ / \ \
*/
// Don't attempt if logo is tiny
if (logoTh > 3) {
// The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding
// We get better results just re-deriving it
int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2));
fillCircle(b2.x, b2.y, capRad, BLACK);
}
}
#endif

View File

@@ -0,0 +1,172 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Base class for InkHUD applets
Must be overriden
An applet is one "program" which may show info on the display.
*/
#pragma once
#include "configuration.h"
#include <GFX.h> // GFXRoot drawing lib
#include "mesh/MeshTypes.h"
#include "./AppletFont.h"
#include "./Applets/System/Notification/Notification.h" // The notification object, not the applet
#include "./InkHUD.h"
#include "./Persistence.h"
#include "./Tile.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
using NicheGraphics::Drivers::EInk;
using std::to_string;
class Applet : public GFX
{
public:
// Which edge Applet::printAt will place on the Y parameter
enum VerticalAlignment : uint8_t {
TOP,
MIDDLE,
BOTTOM,
};
// Which edge Applet::printAt will place on the X parameter
enum HorizontalAlignment : uint8_t {
LEFT,
RIGHT,
CENTER,
};
// An easy-to-understand interpretation of SNR and RSSI
// Calculate with Applet::getSignalStrength
enum SignalStrength : int8_t {
SIGNAL_UNKNOWN = -1,
SIGNAL_NONE,
SIGNAL_BAD,
SIGNAL_FAIR,
SIGNAL_GOOD,
};
Applet();
void setTile(Tile *t); // Should only be called via Tile::setApplet
Tile *getTile(); // Tile with which this applet is linked
// Rendering
void render(); // Draw the applet
bool wantsToRender(); // Check whether applet wants to render
bool wantsToAutoshow(); // Check whether applet wants to become foreground
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
void updateDimensions(); // Get current size from tile
void resetDrawingSpace(); // Makes sure every render starts with same parameters
// State of the applet
void activate(); // Begin running
void deactivate(); // Stop running
void bringToForeground(); // Show
void sendToBackground(); // Hide
bool isActive();
bool isForeground();
// Event handlers
virtual void onRender() = 0; // All drawing happens here
virtual void onActivate() {}
virtual void onDeactivate() {}
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
virtual void onButtonShortPress() {} // (System Applets only)
virtual void onButtonLongPress() {} // (System Applets only)
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
static uint16_t getHeaderHeight(); // How tall the "standard" applet header is
static AppletFont fontSmall, fontLarge; // The general purpose fonts, used by all applets
const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet
protected:
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
void requestAutoshow(); // Ask for applet to be moved to foreground
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
void resetCrop(); // Removes setCrop()
// Text
void setFont(AppletFont f);
AppletFont getFont();
uint16_t getTextWidth(std::string text);
uint16_t getTextWidth(const char *text);
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
void drawHeader(std::string text); // Draw the standard applet header
// Meshtastic Logo
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height); // Draw the meshtastic logo
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
std::string getTimeString(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
// Convenient references
InkHUD *inkhud = nullptr;
Persistence::Settings *settings = nullptr;
Persistence::LatestMessage *latestMessage = nullptr;
private:
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
bool active = false; // Has the user enabled this applet (at run-time)?
bool foreground = false; // Is the applet currently drawn on a tile?
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
AppletFont currentFont; // As passed to setFont
// As set by setCrop
int16_t cropLeft = 0;
int16_t cropTop = 0;
uint16_t cropWidth = 0;
uint16_t cropHeight = 0;
};
}; // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,221 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./AppletFont.h"
using namespace NicheGraphics;
InkHUD::AppletFont::AppletFont()
{
// Default constructor uses the in-built AdafruitGFX font
}
InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafruitGFXFont)
{
// AdafruitGFX fonts are drawn relative to a "cursor line";
// they print as if the glyphs are resting on the line of piece of ruled paper.
// The glyphs also each have a different height.
// To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text
// We also need to know where that "cursor line" sits inside this "line height";
// we need this additional info in order to align text by top-left, bottom-right, etc
// AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding,
// which we'd rather not deal with. If we want padding, we'll add it manually.
// Scan each glyph in the AdafruitGFX font
for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) {
uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph
this->height = max(this->height, glyphHeight); // Store if it's a new max
// Calculate how far the glyph rises the cursor line
// Store if new max value
// Caution: signed and unsigned types
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
if (glyphAscender > 0)
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
}
// Determine how far characters may hang "below the line"
descenderHeight = height - ascenderHeight;
// Find how far the cursor advances when we "print" a space character
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
}
/*
▲ ##### # ▲
│ # # │
lineHeight │ ### # │
│ # # # # │ heightAboveCursor
│ # # # # │
│ # # #### │
│ -----------------#----
│ # │ heightBelowCursor
▼ ### ▼
*/
uint8_t InkHUD::AppletFont::lineHeight()
{
return this->height;
}
// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper).
// This value is the height of the font, above that imaginary line.
// Used to calculate the true height of the font
uint8_t InkHUD::AppletFont::heightAboveCursor()
{
return this->ascenderHeight;
}
// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper).
// This value is the height of the font, below that imaginary line.
// Used to calculate the true height of the font
uint8_t InkHUD::AppletFont::heightBelowCursor()
{
return this->descenderHeight;
}
// Width of the space character
// Used with Applet::printWrapped
uint8_t InkHUD::AppletFont::widthBetweenWords()
{
return this->spaceCharWidth;
}
// Add to the list of substituted glyphs
// This "find and replace" operation will be run before text is printed
// Used to swap out UTF8 special characters, either with a custom font, or with a suitable ASCII approximation
void InkHUD::AppletFont::addSubstitution(const char *from, const char *to)
{
substitutions.push_back({.from = from, .to = to});
}
// Run all registered substitutions on a string
// Used to swap out UTF8 special chars
void InkHUD::AppletFont::applySubstitutions(std::string *text)
{
// For each substitution
for (Substitution s : substitutions) {
// Find and replace
// - search for Substitution::from
// - replace with Substitution::to
size_t i = text->find(s.from);
while (i != std::string::npos) {
text->replace(i, strlen(s.from), s.to);
i = text->find(s.from, i); // Continue looking from last position
}
}
}
// Apply a set of substitutions which remap UTF8 for a Windows-1251 font
// Windows-1251 is an 8-bit character encoding, suitable for several languages which use the Cyrillic script
void InkHUD::AppletFont::addSubstitutionsWin1251()
{
addSubstitution("Ђ", "\x80");
addSubstitution("Ѓ", "\x81");
addSubstitution("ѓ", "\x83");
addSubstitution("", "\x88");
addSubstitution("Љ", "\x8A");
addSubstitution("Њ", "\x8C");
addSubstitution("Ќ", "\x8D");
addSubstitution("Ћ", "\x8E");
addSubstitution("Џ", "\x8F");
addSubstitution("ђ", "\x90");
addSubstitution("љ", "\x9A");
addSubstitution("њ", "\x9C");
addSubstitution("ќ", "\x9D");
addSubstitution("ћ", "\x9E");
addSubstitution("џ", "\x9F");
addSubstitution("Ў", "\xA1");
addSubstitution("ў", "\xA2");
addSubstitution("Ј", "\xA3");
addSubstitution("Ґ", "\xA5");
addSubstitution("Ё", "\xA8");
addSubstitution("Є", "\xAA");
addSubstitution("Ї", "\xAF");
addSubstitution("І", "\xB2");
addSubstitution("і", "\xB3");
addSubstitution("ґ", "\xB4");
addSubstitution("ё", "\xB8");
addSubstitution("", "\xB9");
addSubstitution("є", "\xBA");
addSubstitution("ј", "\xBC");
addSubstitution("Ѕ", "\xBD");
addSubstitution("ѕ", "\xBE");
addSubstitution("ї", "\xBF");
addSubstitution("А", "\xC0");
addSubstitution("Б", "\xC1");
addSubstitution("В", "\xC2");
addSubstitution("Г", "\xC3");
addSubstitution("Д", "\xC4");
addSubstitution("Е", "\xC5");
addSubstitution("Ж", "\xC6");
addSubstitution("З", "\xC7");
addSubstitution("И", "\xC8");
addSubstitution("Й", "\xC9");
addSubstitution("К", "\xCA");
addSubstitution("Л", "\xCB");
addSubstitution("М", "\xCC");
addSubstitution("Н", "\xCD");
addSubstitution("О", "\xCE");
addSubstitution("П", "\xCF");
addSubstitution("Р", "\xD0");
addSubstitution("С", "\xD1");
addSubstitution("Т", "\xD2");
addSubstitution("У", "\xD3");
addSubstitution("Ф", "\xD4");
addSubstitution("Х", "\xD5");
addSubstitution("Ц", "\xD6");
addSubstitution("Ч", "\xD7");
addSubstitution("Ш", "\xD8");
addSubstitution("Щ", "\xD9");
addSubstitution("Ъ", "\xDA");
addSubstitution("Ы", "\xDB");
addSubstitution("Ь", "\xDC");
addSubstitution("Э", "\xDD");
addSubstitution("Ю", "\xDE");
addSubstitution("Я", "\xDF");
addSubstitution("а", "\xE0");
addSubstitution("б", "\xE1");
addSubstitution("в", "\xE2");
addSubstitution("г", "\xE3");
addSubstitution("д", "\xE4");
addSubstitution("е", "\xE5");
addSubstitution("ж", "\xE6");
addSubstitution("з", "\xE7");
addSubstitution("и", "\xE8");
addSubstitution("й", "\xE9");
addSubstitution("к", "\xEA");
addSubstitution("л", "\xEB");
addSubstitution("м", "\xEC");
addSubstitution("н", "\xED");
addSubstitution("о", "\xEE");
addSubstitution("п", "\xEF");
addSubstitution("р", "\xF0");
addSubstitution("с", "\xF1");
addSubstitution("т", "\xF2");
addSubstitution("у", "\xF3");
addSubstitution("ф", "\xF4");
addSubstitution("х", "\xF5");
addSubstitution("ц", "\xF6");
addSubstitution("ч", "\xF7");
addSubstitution("ш", "\xF8");
addSubstitution("щ", "\xF9");
addSubstitution("ъ", "\xFA");
addSubstitution("ы", "\xFB");
addSubstitution("ь", "\xFC");
addSubstitution("э", "\xFD");
addSubstitution("ю", "\xFE");
addSubstitution("я", "\xFF");
}
#endif

View File

@@ -0,0 +1,59 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Wrapper class for an AdafruitGFX font
Pre-calculates some font dimension info which InkHUD uses repeatedly
Also contains an optional set of "substitutions".
These can be used to detect special UTF8 chars, and replace occurrences with a remapped char val to suit a custom font
These can also be used to swap UTF8 chars for a suitable ASCII substitution (e.g. German ö -> oe, etc)
*/
#pragma once
#include "configuration.h"
#include <GFX.h> // GFXRoot drawing lib
namespace NicheGraphics::InkHUD
{
// An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD
class AppletFont
{
public:
AppletFont();
explicit AppletFont(const GFXfont &adafruitGFXFont);
uint8_t lineHeight();
uint8_t heightAboveCursor();
uint8_t heightBelowCursor();
uint8_t widthBetweenWords(); // Width of the space character
void applySubstitutions(std::string *text); // Run all char-substitution operations, prior to printing
void addSubstitution(const char *from, const char *to); // Register a find-replace action, for remapping UTF8 chars
void addSubstitutionsWin1251(); // Cyrillic fonts: remap UTF8 values to their Win-1251 equivalent
// Todo: Polish font
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
private:
uint8_t height = 8; // Default value: in-built AdafruitGFX font
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font
uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font
// One pair of find-replace values, for substituting or remapping UTF8 chars
struct Substitution {
const char *from;
const char *to;
};
std::vector<Substitution> substitutions; // List of all character substitutions to run, prior to printing a string
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,428 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./MapApplet.h"
using namespace NicheGraphics;
void InkHUD::MapApplet::onRender()
{
// Abort if no markers to render
if (!enoughMarkers()) {
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE);
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
return;
}
// Find center of map
// - latitude and longitude
// - will be placed at X(0.5), Y(0.5)
getMapCenter(&latCenter, &lngCenter);
// Calculate North+East distance of each node to map center
// - which nodes to use controlled by virtual shouldDrawNode method
calculateAllMarkers();
// Set the region shown on the map
// - default: fit all nodes, plus padding
// - maybe overriden by derived applet
// - getMapSize *sets* passed parameters (C-style)
getMapSize(&widthMeters, &heightMeters);
// Set the metersToPx conversion value
calculateMapScale();
// Special marker for own node
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode))
drawLabeledMarker(ourNode);
// Draw all markers
for (Marker m : markers) {
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
// Cross Size
constexpr uint16_t csMin = 5;
constexpr uint16_t csMax = 12;
// Too many hops away
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops
printAt(x, y, "!", CENTER, MIDDLE);
else if (!m.hasHopsAway) // Unknown hops
drawCross(x, y, csMin);
else // The fewer hops, the larger the cross
drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin));
}
}
// Find the center point, in the middle of all node positions
// Calculated values are written to the *lat and *long pointer args
// - Finds the "mean lat long"
// - Calculates furthest nodes from "mean lat long"
// - Place map center directly between these furthest nodes
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
{
// Find mean lat long coords
// ============================
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
// - averages the x, y and z coords
// - uses tan to find angles for lat / long degrees
// - longitude: triangle formed by x and y (on plane of the equator)
// - latitude: triangle formed by z (north south),
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's surface
// Working totals, averaged after nodeDB processed
uint32_t positionCount = 0;
float xAvg = 0;
float yAvg = 0;
float zAvg = 0;
// For each node in db
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
if (!nodeDB->hasValidPosition(node))
continue;
// Skip if derived applet doesn't want to show this node on the map
if (!shouldDrawNode(node))
continue;
// Latitude and Longitude of node, in radians
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
// Convert to cartesian points, with center of earth at 0, 0, 0
// Exact distance from center is irrelevant, as we're only interested in the vector
float x = cos(latRad) * cos(lngRad);
float y = cos(latRad) * sin(lngRad);
float z = sin(latRad);
// To find mean values shortly
xAvg += x;
yAvg += y;
zAvg += z;
positionCount++;
}
// All NodeDB processed, find mean values
xAvg /= positionCount;
yAvg /= positionCount;
zAvg /= positionCount;
// Longitude from cartesian coords
// (Angle from 3D coords describing a point of globe's surface)
/*
UK
/-------\
(Top View) /- -\
/- (You) -\
/- . -\
/- . X -\
Asia - ... - USA
\- Y -/
\- -/
\- -/
\- -/
\- -----/
Pacific
*/
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
// Latitude from cartesian coords
// (Angle from 3D coords describing a point on the globe's surface)
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
/*
UK North
/-------\ (Front View) /-------\
(Top View) /- -\ /- -\
/- (You) -\ /-(You) -\
/- /. -\ /- . -\
/- √X²+Y²/ . X -\ /- Z . -\
Asia - /... - USA - ..... -
\- Y -/ \- √X²+Y² -/
\- -/ \- -/
\- -/ \- -/
\- -/ \- -/
\- -----/ \- -----/
Pacific South
*/
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
// ----------------------------------------------
// This has given us the "mean position"
// This will be a position *somewhere* near the center of our nodes.
// What we actually want is to place our center so that our outermost nodes end up on the border of our map.
// The only real use of our "mean position" is to give us a reference frame:
// which direction is east, and which is west.
//------------------------------------------------
// Find furthest nodes from "mean lat long"
// ========================================
float northernmost = latCenter;
float southernmost = latCenter;
float easternmost = lngCenter;
float westernmost = lngCenter;
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
if (!nodeDB->hasValidPosition(node))
continue;
// Skip if derived applet doesn't want to show this node on the map
if (!shouldDrawNode(node))
continue;
// Check for a new top or bottom latitude
float lat = node->position.latitude_i * 1e-7;
northernmost = max(northernmost, lat);
southernmost = min(southernmost, lat);
// Longitude is trickier
float lng = node->position.longitude_i * 1e-7;
float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
if (degEastward < degWestward)
easternmost = max(easternmost, lngCenter + degEastward);
else
westernmost = min(westernmost, lngCenter - degWestward);
}
// Todo: check for issues with map spans >180 deg. MQTT only..
latCenter = (northernmost + southernmost) / 2;
lngCenter = (westernmost + easternmost) / 2;
// In case our new center is west of -180, or east of +180, for some reason
lngCenter = fmod(lngCenter, 180);
}
// Size of map in meters
// Grown to fit the nodes furthest from map center
// Overridable if derived applet wants a custom map size (fixed size?)
void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters)
{
// Reset the value
*widthMeters = 0;
*heightMeters = 0;
// Find the greatest distance horizontally and vertically from map center
for (Marker m : markers) {
*widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2);
*heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2);
}
// Add padding
*widthMeters *= 1.1;
*heightMeters *= 1.1;
}
// Convert and store info we need for drawing a marker
// Lat / long to "meters relative to map center", for position on screen
// Info about hopsAway, for marker size
InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway)
{
assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling.
// Bearing and distance from map center to node
float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng);
float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians
// Split into meters north and meters east components (signed)
// - signedness of cos / sin automatically sets negative if south or west
float northMeters = cos(bearingFromCenter) * distanceFromCenter;
float eastMeters = sin(bearingFromCenter) * distanceFromCenter;
// Store this as a new marker
Marker m;
m.eastMeters = eastMeters;
m.northMeters = northMeters;
m.hasHopsAway = hasHopsAway;
m.hopsAway = hopsAway;
return m;
}
// Draw a marker on the map for a node, with a shortname label, and backing box
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
{
// Find x and y position based on node's position in nodeDB
assert(nodeDB->hasValidPosition(node));
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
);
// Convert to pixel coords
int16_t markerX = X(0.5) + (m.eastMeters * metersToPx);
int16_t markerY = Y(0.5) - (m.northMeters * metersToPx);
constexpr uint16_t paddingH = 2;
constexpr uint16_t paddingW = 4;
uint16_t paddingInnerW = 2; // Zero'd out if no text
constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross)
constexpr uint16_t markerSizeMin = 5;
int16_t textX;
int16_t textY;
uint16_t textW;
uint16_t textH;
int16_t labelX;
int16_t labelY;
uint16_t labelW;
uint16_t labelH;
uint8_t markerSize;
bool tooManyHops = node->hops_away > config.lora.hop_limit;
bool isOurNode = node->num == nodeDB->getNodeNum();
bool unknownHops = !node->has_hops_away && !isOurNode;
// We will draw a left or right hand variant, to place text towards screen center
// Hopefully avoid text spilling off screen
// Most values are the same, regardless of left-right handedness
// Pick emblem style
if (tooManyHops)
markerSize = getTextWidth("!");
else if (unknownHops)
markerSize = markerSizeMin;
else
markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin);
// Common dimensions (left or right variant)
textW = getTextWidth(node->user.short_name);
if (textW == 0)
paddingInnerW = 0; // If no text, no padding for text
textH = fontSmall.lineHeight();
labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH;
labelY = markerY - (labelH / 2);
textY = markerY;
labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant
// Left-side variant
if (markerX < width() / 2) {
labelX = markerX - (markerSize / 2) - paddingW;
textX = labelX + paddingW + markerSize + paddingInnerW;
}
// Right-side variant
else {
labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW;
textX = labelX + paddingW;
}
// Backing box
fillRect(labelX, labelY, labelW, labelH, WHITE);
drawRect(labelX, labelY, labelW, labelH, BLACK);
// Short name
printAt(textX, textY, node->user.short_name, LEFT, MIDDLE);
// If the label is for our own node,
// fade it by overdrawing partially with white
if (node == nodeDB->getMeshNode(nodeDB->getNodeNum()))
hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE);
// Draw the marker emblem
// - after the fading, because hatching (own node) can align with cross and make it look weird
if (tooManyHops)
printAt(markerX, markerY, "!", CENTER, MIDDLE);
else
drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops
}
// Check if we actually have enough nodes which would be shown on the map
// Need at least two, to draw a sensible map
bool InkHUD::MapApplet::enoughMarkers()
{
uint8_t count = 0;
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Count nodes
if (nodeDB->hasValidPosition(node) && shouldDrawNode(node))
count++;
// We need to find two
if (count == 2)
return true; // Two nodes is enough for a sensible map
}
return false; // No nodes would be drawn (or just the one, uselessly at 0,0)
}
// Calculate how far north and east of map center each node is
// Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode
void InkHUD::MapApplet::calculateAllMarkers()
{
// Clear old markers
markers.clear();
// For each node in db
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
if (!nodeDB->hasValidPosition(node))
continue;
// Skip if derived applet doesn't want to show this node on the map
if (!shouldDrawNode(node))
continue;
// Skip if our own node
// - special handling in render()
if (node->num == nodeDB->getNodeNum())
continue;
// Calculate marker and store it
markers.push_back(
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
));
}
}
// Determine the conversion factor between metres, and pixels on screen
// May be overriden by derived applet, if custom scale required (fixed map size?)
void InkHUD::MapApplet::calculateMapScale()
{
// Aspect ratio of map and screen
// - larger = wide, smaller = tall
// - used to set scale, so that widest map dimension fits in applet
float mapAspectRatio = (float)widthMeters / heightMeters;
float appletAspectRatio = (float)width() / height();
// "Shrink to fit"
// Scale the map so that the largest dimension is fully displayed
// Because aspect ratio will be maintained, the other dimension will appear "padded"
if (mapAspectRatio > appletAspectRatio)
metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width.
else
metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height.
}
// Draw an x, centered on a specific point
// Most markers will draw with this method
void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size)
{
int16_t x0 = x - (size / 2);
int16_t y0 = y - (size / 2);
int16_t x1 = x0 + size - 1;
int16_t y1 = y0 + size - 1;
drawLine(x0, y0, x1, y1, BLACK);
drawLine(x0, y1, x1, y0, BLACK);
}
#endif

View File

@@ -0,0 +1,65 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Base class for Applets which show nodes on a map
Plots position of for a selection of nodes, with north facing up.
Size of cross represents hops away.
Our own node is identified with a faded label.
The base applet doesn't handle any events; this is left to the derived applets.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "MeshModule.h"
#include "gps/GeoCoord.h"
namespace NicheGraphics::InkHUD
{
class MapApplet : public Applet
{
public:
void onRender() override;
protected:
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
virtual void getMapCenter(float *lat, float *lng);
virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters);
bool enoughMarkers(); // Anything to draw?
void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker
private:
// Position and size of a marker to be drawn
struct Marker {
float eastMeters = 0; // Meters east of map center. Negative if west.
float northMeters = 0; // Meters north of map center. Negative if south.
bool hasHopsAway = false;
uint8_t hopsAway = 0; // Determines marker size
};
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);
void calculateAllMarkers();
void calculateMapScale(); // Conversion factor for meters to pixels
void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers
float metersToPx = 0; // Conversion factor for meters to pixels
float latCenter = 0; // Map center: latitude
float lngCenter = 0; // Map center: longitude
std::list<Marker> markers;
uint32_t widthMeters = 0; // Map width: meters
uint32_t heightMeters = 0; // Map height: meters
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,279 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "RTC.h"
#include "GeoCoord.h"
#include "NodeDB.h"
#include "./NodeListApplet.h"
using namespace NicheGraphics;
InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name)
{
// We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule
// For all other packets, we manually act as if isPromiscuous=false, in wantPacket
MeshModule::isPromiscuous = true;
}
// Do we want to process this packet with handleReceived()?
bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p)
{
// Only interested if:
return isActive() // Applet is active
&& !isFromUs(p) // Packet is incoming (not outgoing)
&& (isToUs(p) || isBroadcast(p->to) || // Either: intended for us,
p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo
// To match the behavior seen in the client apps:
// - NodeInfoModule's ProtoBufModule base is "promiscuous"
// - All other activity is *not* promiscuous
// To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here,
// to match the code in MeshModule::callModules
}
// MeshModule packets arrive here
// Extract the info and pass it to the derived applet
// Derived applet will store the CardInfo, and perform any required sorting of the CardInfo collection
// Derived applet might also need to keep other tallies (active nodes count?)
ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
// Abort if applet fully deactivated
// Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return
if (!isActive())
return ProcessMessage::CONTINUE;
// Assemble info: from this event
CardInfo c;
c.nodeNum = mp.from;
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
// Assemble info: from nodeDB (needed to detect changes)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node) {
if (node->has_hops_away)
c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float
// Meshtastic stores these as integers internally
float ourLat = ourNode->position.latitude_i * 1e-7;
float ourLong = ourNode->position.longitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7;
float theirLong = node->position.longitude_i * 1e-7;
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
}
}
// Pass to the derived applet
// Derived applet is responsible for requesting update, if justified
// That request will eventually trigger our class' onRender method
handleParsed(c);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
// Calculate maximum number of cards we may ever need to render, in our tallest layout config
// Number might be slightly in excess of the true value: applet header text not accounted for
uint8_t InkHUD::NodeListApplet::maxCards()
{
// Cache result. Shouldn't change during execution
static uint8_t cards = 0;
if (!cards) {
const uint16_t height = Tile::maxDisplayDimension();
// Use a loop instead of arithmetic, because it's easier for my brain to follow
// Add cards one by one, until the latest card extends below screen
uint16_t y = cardH; // First card: no margin above
cards = 1;
while (y < height) {
y += cardMarginH;
y += cardH;
cards++;
}
}
return cards;
}
// Draw, using info which derived applet placed into NodeListApplet::cards for us
void InkHUD::NodeListApplet::onRender()
{
// ================================
// Draw the standard applet header
// ================================
drawHeader(getHeaderText()); // Ask derived applet for the title
// Dimensions of the header
int16_t headerDivY = getHeaderHeight() - 1;
constexpr uint16_t padDivH = 2;
// ========================
// Draw the main node list
// ========================
// Imaginary vertical line dividing left-side and right-side info
// Long-name will crop here
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
// Y value (top) of the current card. Increases as we draw.
uint16_t cardTopY = headerDivY + padDivH;
// -- Each node in list --
for (auto card = cards.begin(); card != cards.end(); ++card) {
// Gather info
// ========================================
NodeNum &nodeNum = card->nodeNum;
SignalStrength &signal = card->signal;
std::string longName; // handled below
std::string shortName; // handled below
std::string distance; // handled below;
uint8_t &hopsAway = card->hopsAway;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
// -- Shortname --
// use "?" if unknown
if (node && node->has_user)
shortName = node->user.short_name;
else
shortName = "?";
// -- Longname --
// use node id if unknown
if (node && node->has_user)
longName = node->user.long_name; // Found in nodeDB
else {
// Not found in nodeDB, show a hex nodeid instead
longName = hexifyNodeNum(nodeNum);
}
// -- Distance --
if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN)
distance = localizeDistance(card->distanceMeters);
// Draw the info
// ====================================
// Define two lines of text for the card
// We will center our text on these lines
uint16_t lineAY = cardTopY + (fontLarge.lineHeight() / 2);
uint16_t lineBY = cardTopY + fontLarge.lineHeight() + (fontSmall.lineHeight() / 2);
// Print the short name
setFont(fontLarge);
printAt(0, lineAY, shortName, LEFT, MIDDLE);
// Print the distance
setFont(fontSmall);
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
// If we have a direct connection to the node, draw the signal indicator
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
uint16_t signalH = fontLarge.lineHeight() * 0.75;
int16_t signalY = lineAY + (fontLarge.lineHeight() / 2) - (fontLarge.lineHeight() * 0.75);
int16_t signalX = width() - signalW;
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
}
// Otherwise, print "hops away" info, if available
else if (hopsAway != CardInfo::HOPS_UNKNOWN) {
std::string hopString = to_string(node->hops_away);
hopString += " Hop";
if (node->hops_away != 1)
hopString += "s"; // Append s for "Hops", rather than "Hop"
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
}
// Print the long name, cropping to prevent overflow onto the right-side info
setCrop(0, 0, dividerX - 1, height());
printAt(0, lineBY, longName, LEFT, MIDDLE);
// GFX effect: "hatch" the right edge of longName area
// If a longName has been cropped, it will appear to fade out,
// creating a soft barrier with the right-side info
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
const int16_t hatchWidth = fontSmall.lineHeight();
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
// Prepare to draw the next card
resetCrop();
cardTopY += cardH;
// Once we've run out of screen, stop drawing cards
// Depending on tiles / rotation, this may be before we hit maxCards
if (cardTopY > height())
break;
}
}
// Draw element: a "mobile phone" style signal indicator
// We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc
// This prevents issues with premature rounding when rendering tiny elements
void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength)
{
/*
+-------------------------------------------+
| |
| |
| barHeightRelative=1.0
| +--+ ^ |
| gutterW +--+ | | | |
| <--> +--+ | | | | | |
| +--+ | | | | | | | |
| | | | | | | | | | |
| <-> +--+ +--+ +--+ +--+ v |
| paddingW ^ |
| paddingH | |
| v |
+-------------------------------------------+
*/
constexpr float paddingW = 0.1; // Either side
constexpr float paddingH = 0.1; // Above and below
constexpr float gutterW = 0.1; // Between bars
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest
constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.
// Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount;
float barHMax = 1.0 - (paddingH + paddingH);
// Draw signal bar rectangles, then placeholder lines once strength reached
for (uint8_t i = 0; i < barCount; i++) {
// Coords for this specific bar
float barH = barHMax * barHRel[i];
float barX = paddingW + (i * (gutterW + barW));
float barY = paddingH + (barHMax - barH);
// Rasterize to px coords at the last moment
int16_t rX = (x + (w * barX)) + 0.5;
int16_t rY = (y + (h * barY)) + 0.5;
uint16_t rW = (w * barW) + 0.5;
uint16_t rH = (h * barH) + 0.5;
// Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
if (i <= strength)
drawRect(rX, rY, rW, rH, BLACK);
else {
// Just draw a placeholder line
float lineY = barY + barH;
uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
}
}
}
#endif

View File

@@ -0,0 +1,74 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Base class for Applets which display a list of nodes
Used by the "Recents" and "Heard" applets. Possibly more in future?
+-------------------------------+
| | |
| SHRT . | | |
| Long name 50km |
| |
| ABCD 2 Hops |
| abcdedfghijk 30km |
| |
+-------------------------------+
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "main.h"
namespace NicheGraphics::InkHUD
{
class NodeListApplet : public Applet, public MeshModule
{
protected:
// Info needed to draw a node card to the list
// - generated each time we hear a node
struct CardInfo {
static constexpr uint8_t HOPS_UNKNOWN = -1;
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
NodeNum nodeNum = 0;
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
uint32_t distanceMeters = DISTANCE_UNKNOWN;
uint8_t hopsAway = HOPS_UNKNOWN;
};
public:
NodeListApplet(const char *name);
void onRender() override;
bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
protected:
virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node
virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be
uint8_t maxCards(); // Max number of cards which could ever fit on screen
std::deque<CardInfo> cards; // Cards to be rendered. Derived applet fills this.
private:
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h,
SignalStrength signal); // Draw a "mobile phone" style signal indicator
// Card Dimensions
// - for rendering and for maxCards calc
const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,14 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./BasicExampleApplet.h"
using namespace NicheGraphics;
// All drawing happens here
// Our basic example doesn't do anything useful. It just passively prints some text.
void InkHUD::BasicExampleApplet::onRender()
{
print("Hello, World!");
}
#endif

View File

@@ -0,0 +1,36 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
A bare-minimum example of an InkHUD applet.
Only prints Hello World.
In variants/<your device>/nicheGraphics.h:
- include this .h file
- add the following line of code:
windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class BasicExampleApplet : public Applet
{
public:
// You must have an onRender() method
// All drawing happens here
void onRender() override;
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,52 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./NewMsgExampleApplet.h"
using namespace NicheGraphics;
// We configured MeshModule API to call this method when we receive a new text message
ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
// Abort if applet fully deactivated
if (!isActive())
return ProcessMessage::CONTINUE;
// Check that this is an incoming message
// Outgoing messages (sent by us) will also call handleReceived
if (!isFromUs(&mp)) {
// Store the sender's nodenum
// We need to keep this information, so we can re-use it anytime render() is called
haveMessage = true;
fromWho = mp.from;
// Tell InkHUD that we have something new to show on the screen
requestUpdate();
}
// Tell MeshModule API to continue informing other firmware components about this message
// We're not the only component which is interested in new text messages
return ProcessMessage::CONTINUE;
}
// All drawing happens here
// We can trigger a render by calling requestUpdate()
// Render might be called by some external source
// We should always be ready to draw
void InkHUD::NewMsgExampleApplet::onRender()
{
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
int16_t centerX = X(0.5); // Same as width() / 2
int16_t centerY = Y(0.5); // Same as height() / 2
if (haveMessage) {
printAt(centerX, centerY, "New Message", CENTER, BOTTOM);
printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP);
} else {
printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY)
}
}
#endif

View File

@@ -0,0 +1,61 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
An example of an InkHUD applet.
Tells us when a new text message arrives.
This applet makes use of the MeshModule API to detect new messages,
which is a general part of the Meshtastic firmware, and not part of InkHUD.
In variants/<your device>/nicheGraphics.h:
- include this .h file
- add the following line of code:
windowManager->addApplet("New Msg", new InkHUD::NewMsgExampleApplet);
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "mesh/SinglePortModule.h"
namespace NicheGraphics::InkHUD
{
class NewMsgExampleApplet : public Applet, public SinglePortModule
{
public:
// The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages.
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
// All drawing happens here
void onRender() override;
// Your applet might also want to use some of these
// Useful for setting up or tidying up
/*
void onActivate(); // When started
void onDeactivate(); // When stopped
void onForeground(); // When shown by short-press
void onBackground(); // When hidden by short-press
*/
private:
// Called when we receive new text messages
// Part of the MeshModule API
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
// Store info from handleReceived
bool haveMessage = false;
NodeNum fromWho = 0;
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,101 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./BatteryIconApplet.h"
using namespace NicheGraphics;
InkHUD::BatteryIconApplet::BatteryIconApplet()
{
// Show at boot, if user has previously enabled the feature
if (settings->optionalFeatures.batteryIcon)
bringToForeground();
// Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available
// This happens whether or not the battery icon feature is enabled
powerStatusObserver.observe(&powerStatus->onNewStatus);
}
// We handle power status' even when the feature is disabled,
// so that we have up to date data ready if the feature is enabled later.
// Otherwise could be 30s before new status update, with weird battery value displayed
int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status)
{
// System applets are always active
assert(isActive());
// This method should only receive power statuses
// If we get a different type of status, something has gone weird elsewhere
assert(status->getStatusType() == STATUS_TYPE_POWER);
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
// Get the new state of charge %, and round to the nearest 10%
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
// If rounded value has changed, trigger a display update
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
// Don't trigger an update if the feature is disabled
if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon)
requestUpdate();
// Store the new value
this->socRounded = newSocRounded;
return 0; // Tell Observable to continue informing other observers
}
void InkHUD::BatteryIconApplet::onRender()
{
// Fill entire tile
// - size of icon controlled by size of tile
int16_t l = 0;
int16_t t = 0;
uint16_t w = width();
int16_t h = height();
// Clear the region beneath the tile
// Most applets are drawing onto an empty frame buffer and don't need to do this
// We do need to do this with the battery though, as it is an "overlay"
fillRect(l, t, w, h, WHITE);
// Vertical centerline
const int16_t m = t + (h / 2);
// =====================
// Draw battery outline
// =====================
// Positive terminal "bump"
const int16_t &bumpL = l;
const uint16_t bumpH = h / 2;
const int16_t bumpT = m - (bumpH / 2);
constexpr uint16_t bumpW = 2;
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
// Main body of battery
const int16_t bodyL = bumpL + bumpW;
const int16_t &bodyT = t;
const int16_t &bodyH = h;
const int16_t bodyW = w - bumpW;
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
// Erase join between bump and body
drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE);
// ===================
// Draw battery level
// ===================
constexpr int16_t slicePad = 2;
const int16_t sliceL = bodyL + slicePad;
const int16_t sliceT = bodyT + slicePad;
const uint16_t sliceH = bodyH - (slicePad * 2);
uint16_t sliceW = bodyW - (slicePad * 2);
sliceW = (sliceW * socRounded) / 100; // Apply percentage
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
}
#endif

View File

@@ -0,0 +1,39 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
This applet floats top-left, giving a graphical representation of battery remaining
It should be optional, enabled by the on-screen menu
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include "PowerStatus.h"
namespace NicheGraphics::InkHUD
{
class BatteryIconApplet : public SystemApplet
{
public:
BatteryIconApplet();
void onRender() override;
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
private:
// Get informed when new information about the battery is available (via onPowerStatusUpdate method)
CallbackObserver<BatteryIconApplet, const meshtastic::Status *> powerStatusObserver =
CallbackObserver<BatteryIconApplet, const meshtastic::Status *>(this, &BatteryIconApplet::onPowerStatusUpdate);
uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10%
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,92 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./LogoApplet.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
{
OSThread::setIntervalFromNow(8 * 1000UL);
OSThread::enabled = true;
textLeft = "";
textRight = "";
textTitle = xstr(APP_VERSION_SHORT);
fontTitle = fontSmall;
bringToForeground();
// This is then drawn with a FULL refresh by Renderer::begin
}
void InkHUD::LogoApplet::onRender()
{
// Size of the region which the logo should "scale to fit"
uint16_t logoWLimit = X(0.8);
uint16_t logoHLimit = Y(0.5);
// Get the max width and height we can manage within the region, while still maintaining aspect ratio
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
// Where to place the center of the logo
int16_t logoCX = X(0.5);
int16_t logoCY = Y(0.5 - 0.05);
drawLogo(logoCX, logoCY, logoW, logoH);
if (!textLeft.empty()) {
setFont(fontSmall);
printAt(0, 0, textLeft, LEFT, TOP);
}
if (!textRight.empty()) {
setFont(fontSmall);
printAt(X(1), 0, textRight, RIGHT, TOP);
}
if (!textTitle.empty()) {
int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo
setFont(fontTitle);
printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP);
}
}
void InkHUD::LogoApplet::onForeground()
{
SystemApplet::lockRendering = true;
SystemApplet::lockRequests = true;
SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it.
}
void InkHUD::LogoApplet::onBackground()
{
SystemApplet::lockRendering = false;
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
// Begin displaying the screen which is shown at shutdown
void InkHUD::LogoApplet::onShutdown()
{
textLeft = "";
textRight = "";
textTitle = owner.short_name;
fontTitle = fontLarge;
bringToForeground();
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update
}
int32_t InkHUD::LogoApplet::runOnce()
{
sendToBackground();
return OSThread::disable();
}
#endif

View File

@@ -0,0 +1,40 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows the Meshtastic logo fullscreen, with accompanying text
Used for boot and shutdown
*/
#pragma once
#include "configuration.h"
#include "concurrency/OSThread.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
namespace NicheGraphics::InkHUD
{
class LogoApplet : public SystemApplet, public concurrency::OSThread
{
public:
LogoApplet();
void onRender() override;
void onForeground() override;
void onBackground() override;
void onShutdown() override;
protected:
int32_t runOnce() override;
std::string textLeft;
std::string textRight;
std::string textTitle;
AppletFont fontTitle;
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,38 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Set of end-point actions for the Menu Applet
Added as menu entries in MenuApplet::showPage
Behaviors assigned in MenuApplet::execute
*/
#pragma once
#include "configuration.h"
namespace NicheGraphics::InkHUD
{
enum MenuAction {
NO_ACTION,
SEND_NODEINFO,
SEND_POSITION,
SHUTDOWN,
NEXT_TILE,
TOGGLE_APPLET,
ACTIVATE_APPLETS, // Todo: remove? Possible redundant, handled by TOGGLE_APPLET?
TOGGLE_AUTOSHOW_APPLET,
SET_RECENTS,
ROTATE,
LAYOUT,
TOGGLE_BATTERY_ICON,
TOGGLE_NOTIFICATIONS,
TOGGLE_BACKLIGHT,
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,599 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./MenuApplet.h"
#include "RTC.h"
#include "airtime.h"
#include "power.h"
using namespace NicheGraphics;
static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes
// Options for the "Recents" menu
// These are offered to users as possible values for settings.recentlyActiveSeconds
static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120};
InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
{
// No timer tasks at boot
OSThread::disable();
// Note: don't get instance if we're not actually using the backlight,
// or else you will unintentionally instantiate it
if (settings->optionalMenuItems.backlight) {
backlight = Drivers::LatchingBacklight::getInstance();
}
}
void InkHUD::MenuApplet::onActivate() {}
void InkHUD::MenuApplet::onForeground()
{
// We do need this before we render, but we can optimize by just calculating it once now
systemInfoPanelHeight = getSystemInfoPanelHeight();
// Display initial menu page
showPage(MenuPage::ROOT);
// If device has a backlight which isn't controlled by aux button:
// backlight on always when menu opens.
// Courtesy to T-Echo users who removed the capacitive touch button
if (settings->optionalMenuItems.backlight) {
assert(backlight);
if (!backlight->isOn())
backlight->peek();
}
// Prevent user applets requesting update while menu is open
// Handle button input with this applet
SystemApplet::lockRequests = true;
SystemApplet::handleInput = true;
// Begin the auto-close timeout
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
// Upgrade the refresh to FAST, for guaranteed responsiveness
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onBackground()
{
// If device has a backlight which isn't controlled by aux button:
// Item in options submenu allows keeping backlight on after menu is closed
// If this item is deselected we will turn backlight off again, now that menu is closing
if (settings->optionalMenuItems.backlight) {
assert(backlight);
if (!backlight->isLatched())
backlight->off();
}
// Stop the auto-timeout
OSThread::disable();
// Resume normal rendering and button behavior of user applets
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
// Restore the user applet whose tile we borrowed
if (borrowedTileOwner)
borrowedTileOwner->bringToForeground();
Tile *t = getTile();
t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one)
borrowedTileOwner = nullptr;
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
// Open the menu
// Parameter specifies which user-tile the menu will use
// The user applet originally on this tile will be restored when the menu closes
void InkHUD::MenuApplet::show(Tile *t)
{
// Remember who *really* owns this tile
borrowedTileOwner = t->getAssignedApplet();
// Hide the owner, if it is a valid applet
if (borrowedTileOwner)
borrowedTileOwner->sendToBackground();
// Break the owner's link with tile
// Relink it to menu applet
t->assignApplet(this);
// Show menu
bringToForeground();
}
// Auto-exit the menu applet after a period of inactivity
// The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open.
// By exiting the menu, we prevent users mistakenly believing that the data will update.
int32_t InkHUD::MenuApplet::runOnce()
{
// runOnce's interval is pushed back when a button is pressed
// If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC,
// so we close the menu.
showPage(EXIT);
// Timer should disable after firing
// This is redundant, as onBackground() will also disable
return OSThread::disable();
}
// Perform action for a menu item, then change page
// Behaviors for MenuActions are defined here
void InkHUD::MenuApplet::execute(MenuItem item)
{
// Perform an action
// ------------------
switch (item.action) {
// Open a submenu without performing any action
// Also handles exit
case NO_ACTION:
break;
case NEXT_TILE:
inkhud->nextTile();
break;
case ROTATE:
inkhud->rotate();
break;
case LAYOUT:
// Todo: smarter incrementing of tile count
settings->userTiles.count++;
if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet
settings->userTiles.count++;
if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high
settings->userTiles.count = 1;
inkhud->updateLayout();
break;
case TOGGLE_APPLET:
settings->userApplets.active[cursor] = !settings->userApplets.active[cursor];
inkhud->updateAppletSelection();
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Select FULL, seeing how this action doesn't auto exit
break;
case ACTIVATE_APPLETS:
// Todo: remove this action? Already handled by TOGGLE_APPLET?
inkhud->updateAppletSelection();
break;
case TOGGLE_AUTOSHOW_APPLET:
// Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage()
*items.at(cursor).checkState = !(*items.at(cursor).checkState);
break;
case TOGGLE_NOTIFICATIONS:
settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications;
break;
case SET_RECENTS:
// Set value of settings.recentlyActiveSeconds
// Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file)
assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]));
settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes
break;
case SHUTDOWN:
LOG_INFO("Shutting down from menu");
power->shutdown();
// Menu is then sent to background via onShutdown
break;
case TOGGLE_BATTERY_ICON:
inkhud->toggleBatteryIcon();
break;
case TOGGLE_BACKLIGHT:
// Note: backlight is already on in this situation
// We're marking that it should *remain* on once menu closes
assert(backlight);
if (backlight->isLatched())
backlight->off();
else
backlight->latch();
break;
default:
LOG_WARN("Action not implemented");
}
// Move to next page, as defined for the MenuItem
showPage(item.nextPage);
}
// Display a new page of MenuItems
// May reload same page, or exit menu applet entirely
// Fills the MenuApplet::items vector
void InkHUD::MenuApplet::showPage(MenuPage page)
{
items.clear();
switch (page) {
case ROOT:
// Optional: next applet
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
// items.push_back(MenuItem("Send", MenuPage::SEND)); // TODO
items.push_back(MenuItem("Options", MenuPage::OPTIONS));
// items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case SEND:
items.push_back(MenuItem("Send Message", MenuPage::EXIT));
items.push_back(MenuItem("Send NodeInfo", MenuAction::SEND_NODEINFO));
items.push_back(MenuItem("Send Position", MenuAction::SEND_POSITION));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case OPTIONS:
// Optional: backlight
if (settings->optionalMenuItems.backlight) {
assert(backlight);
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
MenuAction::TOGGLE_BACKLIGHT, // Action
MenuPage::EXIT // Exit once complete
));
}
items.push_back(MenuItem("Applets", MenuPage::APPLETS));
items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW));
items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS));
if (settings->userTiles.maxCount > 1)
items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
&settings->optionalFeatures.notifications));
items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS,
&settings->optionalFeatures.batteryIcon));
// TODO - GPS and Wifi switches
/*
// Optional: has GPS
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED)
items.push_back(MenuItem("Enable GPS", MenuPage::EXIT)); // TODO
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
items.push_back(MenuItem("Disable GPS", MenuPage::EXIT)); // TODO
// Optional: using wifi
if (!config.bluetooth.enabled)
items.push_back(MenuItem("Enable Bluetooth", MenuPage::EXIT)); // TODO: escape hatch if wifi configured wrong
*/
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case APPLETS:
populateAppletPage();
items.push_back(MenuItem("Exit", MenuAction::ACTIVATE_APPLETS));
break;
case AUTOSHOW:
populateAutoshowPage();
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case RECENTS:
populateRecentsPage();
break;
case EXIT:
sendToBackground(); // Menu applet dismissed, allow normal behavior to resume
// requestUpdate(Drivers::EInk::UpdateTypes::FULL);
break;
default:
LOG_WARN("Page not implemented");
}
// Reset the cursor, unless reloading same page
// (or now out-of-bounds)
if (page != currentPage || cursor >= items.size()) {
cursor = 0;
// ROOT menu has special handling: unselected at first, to emphasise the system info panel
if (page == ROOT)
cursorShown = false;
}
// Remember which page we are on now
currentPage = page;
}
void InkHUD::MenuApplet::onRender()
{
if (items.size() == 0)
LOG_ERROR("Empty Menu");
// Dimensions for the slots where we will draw menuItems
const float padding = 0.05;
const uint16_t itemH = fontSmall.lineHeight() * 2;
const int16_t itemW = width() - X(padding) - X(padding);
const int16_t itemL = X(padding);
const int16_t itemR = X(1 - padding);
int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu.
// How many full menuItems will fit on screen
uint8_t slotCount = (height() - itemT) / itemH;
// System info panel at the top of the menu
// =========================================
uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground
const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel
// System info - top
// Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen.
// This is the same behavior we expect from the non-root menus.
// Implementing this with the systemp panel is slightly annoying though,
// and required adding the MenuApplet::getSystemInfoPanelHeight method
int16_t siT;
if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count)
siT = 0;
else
siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH);
// If showing ROOT menu,
// and the panel isn't yet scrolled off screen top
if (currentPage == ROOT) {
drawSystemInfoPanel(0, siT, width()); // Draw the panel.
itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel
}
// Draw menu items
// ===================
// Which item will be drawn to the top-most slot?
// Initially, this is the item 0, but may increase once we begin scrolling
uint8_t firstItem;
if (cursor < slotCount)
firstItem = 0;
else
firstItem = cursor - (slotCount - 1);
// Which item will be drawn to the bottom-most slot?
// This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow
// This may be less than the slot-count, if we are reaching the end of the menuItems
uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1);
// -- Loop: draw each (visible) menu item --
for (uint8_t i = firstItem; i <= lastItem; i++) {
// Grab the menuItem
MenuItem item = items.at(i);
// Center-line for the text
int16_t center = itemT + (itemH / 2);
if (cursorShown && i == cursor)
drawRect(itemL, itemT, itemW, itemH, BLACK);
printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE);
// Testing only: circle instead of check box
if (item.checkState) {
const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height
const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
const int16_t cbT = center - (cbWH / 2); // Checkbox : top
// Checkbox ticked
if (*(item.checkState)) {
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
// First point of tick: pen down
const int16_t t1Y = center;
const int16_t t1X = cbL + 3;
// Second point of tick: base
const int16_t t2Y = center + (cbWH / 2) - 2;
const int16_t t2X = cbL + (cbWH / 2);
// Third point of tick: end of tail
const int16_t t3Y = center - (cbWH / 2) - 2;
const int16_t t3X = cbL + cbWH + 2;
// Draw twice: faux bold
drawLine(t1X, t1Y, t2X, t2Y, BLACK);
drawLine(t2X, t2Y, t3X, t3Y, BLACK);
drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK);
drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK);
}
// Checkbox ticked
else
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
}
// Increment the y value (top) as we go
itemT += itemH;
}
}
void InkHUD::MenuApplet::onButtonShortPress()
{
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Move menu cursor to next entry, then update
if (cursorShown)
cursor = (cursor + 1) % items.size();
else
cursorShown = true;
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onButtonLongPress()
{
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
// If we didn't already request a specialized update, when handling a menu action,
// then perform the usual fast update.
// FAST keeps things responsive: important because we're dealing with user input
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
void InkHUD::MenuApplet::populateAppletPage()
{
assert(items.size() == 0);
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
const char *name = inkhud->userApplets.at(i)->name;
bool *isActive = &(settings->userApplets.active[i]);
items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive));
}
}
// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data
// We only populate this menu page with applets which are actually active
// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient.
void InkHUD::MenuApplet::populateAutoshowPage()
{
assert(items.size() == 0);
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
// Only add a menu item if applet is active
if (settings->userApplets.active[i]) {
const char *name = inkhud->userApplets.at(i)->name;
bool *isActive = &(settings->userApplets.autoshow[i]);
items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive));
}
}
}
void InkHUD::MenuApplet::populateRecentsPage()
{
// How many values are shown for use to choose from
constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]);
// Create an entry for each item in RECENTS_OPTIONS_MINUTES array
// (Defined at top of this file)
for (uint8_t i = 0; i < optionCount; i++) {
std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins";
items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT));
}
}
// Renders the panel shown at the top of the root menu.
// Displays the clock, and several other pieces of instantaneous system info,
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight)
{
// Reset the height
// We'll add to this as we add elements
uint16_t height = 0;
// Clock (potentially)
// ====================
std::string clockString = getTimeString();
if (clockString.length() > 0) {
setFont(fontLarge);
printAt(width / 2, top, clockString, CENTER, TOP);
height += fontLarge.lineHeight();
height += fontLarge.lineHeight() * 0.1; // Padding below clock
}
// Stats
// ===================
setFont(fontSmall);
// Position of the label row for the system info
const int16_t labelT = top + height;
height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing
// Position of the data row for the system info
const int16_t valT = top + height;
height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider)
// Position of divider between the info panel and the menu entries
const int16_t divY = top + height;
height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item)
// Create a variable number of columns
// Either 3 or 4, depending on whether we have GPS
// Todo
constexpr uint8_t N_COL = 3;
int16_t colL[N_COL];
int16_t colC[N_COL];
int16_t colR[N_COL];
for (uint8_t i = 0; i < N_COL; i++) {
colL[i] = left + ((width / N_COL) * i);
colC[i] = colL[i] + ((width / N_COL) / 2);
colR[i] = colL[i] + (width / N_COL);
}
// Info blocks, left to right
// Voltage
float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
char voltageStr[6]; // "XX.XV"
sprintf(voltageStr, "%.1fV", voltage);
printAt(colC[0], labelT, "Bat", CENTER, TOP);
printAt(colC[0], valT, voltageStr, CENTER, TOP);
// Divider
for (int16_t y = valT; y <= divY; y += 3)
drawPixel(colR[0], y, BLACK);
// Channel Util
char chUtilStr[4]; // "XX%"
sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent());
printAt(colC[1], labelT, "Ch", CENTER, TOP);
printAt(colC[1], valT, chUtilStr, CENTER, TOP);
// Divider
for (int16_t y = valT; y <= divY; y += 3)
drawPixel(colR[1], y, BLACK);
// Duty Cycle (AirTimeTx)
char dutyUtilStr[4]; // "XX%"
sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent());
printAt(colC[2], labelT, "Duty", CENTER, TOP);
printAt(colC[2], valT, dutyUtilStr, CENTER, TOP);
/*
// Divider
for (int16_t y = valT; y <= divY; y += 3)
drawPixel(colR[2], y, BLACK);
// GPS satellites - todo
printAt(colC[3], labelT, "Sats", CENTER, TOP);
printAt(colC[3], valT, "ToDo", CENTER, TOP);
*/
// Horizontal divider, at bottom of system info panel
for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item
drawPixel(x, divY, BLACK);
if (renderedHeight != nullptr)
*renderedHeight = height;
}
// Get the height of the the panel drawn at the top of the menu
// This is inefficient, as we do actually have to render the panel to determine the height
// It solves a catch-22 situation, where slotCount needs to know panel height, and panel height needs to know slotCount
uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
{
// Render *far* off screen
uint16_t height = 0;
drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height);
return height;
}
#endif

View File

@@ -0,0 +1,60 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "configuration.h"
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include "./MenuItem.h"
#include "./MenuPage.h"
#include "concurrency/OSThread.h"
namespace NicheGraphics::InkHUD
{
class Applet;
class MenuApplet : public SystemApplet, public concurrency::OSThread
{
public:
MenuApplet();
void onActivate() override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onRender() override;
void show(Tile *t); // Open the menu, onto a user tile
protected:
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
int32_t runOnce() override;
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
void showPage(MenuPage page); // Load and display a MenuPage
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
uint16_t getSystemInfoPanelHeight();
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu
MenuPage currentPage = MenuPage::ROOT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,47 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
One item of a MenuPage, in InkHUD::MenuApplet
Added to MenuPages in InkHUD::showPage
- May open a submenu or exit
- May perform an action
- May toggle a bool value, shown by a checkbox
*/
#pragma once
#include "configuration.h"
#include "./MenuAction.h"
#include "./MenuPage.h"
namespace NicheGraphics::InkHUD
{
// One item of a MenuPage
class MenuItem
{
public:
std::string label;
MenuAction action = NO_ACTION;
MenuPage nextPage = EXIT;
bool *checkState = nullptr;
// Various constructors, depending on the intended function of the item
MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {}
MenuItem(const char *label, MenuAction action) : label(label), action(action) {}
MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {}
MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState)
: label(label), action(action), nextPage(nextPage), checkState(checkState)
{
}
};
} // namespace NicheGraphics::InkHUD
#endif

Some files were not shown because too many files have changed in this diff Show More