* 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>
pull/6184/head^2
Ben Meadors 2025-03-01 06:18:33 -06:00 zatwierdzone przez GitHub
rodzic 088fce7d11
commit 99d3e5eb70
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
159 zmienionych plików z 12449 dodań i 427 usunięć

3
.gitmodules vendored
Wyświetl plik

@ -1,6 +1,9 @@
[submodule "protobufs"]
path = protobufs
url = https://github.com/meshtastic/protobufs.git
[submodule "lib/device-ui"]
path = lib/device-ui
url = https://github.com/meshtastic/device-ui.git
[submodule "meshtestic"]
path = meshtestic
url = https://github.com/meshtastic/meshTestic

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

1
lib/device-ui 160000

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,234 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Base class for InkHUD applets
Must be overriden
An applet is one "program" which may show info on the display.
===================================
Preliminary notes, for the curious
===================================
(This info to be streamlined, and moved to a more official documentation)
User Applets vs System Applets
-------------------------------
There are either "User Applets", or "System Applets".
This concept is only for our understanding; as far at the code is concerned, both are just "Applets"
User applets are the "normal" applets.
User applets are applets like "AllMessageApplet", or "MapApplet".
User applets may be enabled / disabled by user, via the on-screen menu.
Incorporating new UserApplets is easy: just add them during setupNicheGraphics
If a UserApplet is not added during setupNicheGraphics, it will not be built.
The set of available UserApplets is allowed to vary from device to device.
Examples of system applets include "NotificationApplet" and "MenuApplet".
For their own reasons, system applets each require some amount of special handling.
Drawing
--------
*All* drawing must be performed by an Applet.
Applets implement the onRender() method, where all drawing takes place.
Applets are told how wide and tall they are, and are expected to draw to suit this size.
When an applet draws, it uses co-ordinates in "Applet Space": between 0 and applet width/height.
Event-driven rendering
-----------------------
Applets don't render unless something on the display needs to change.
An applet is expected to determine for itself when it has new info to display.
It should interact with the firmware via the MeshModule API, via Observables, etc.
Please don't directly add hooks throughout the existing firmware code.
When an applet decides it would like to update the display, it should call requestUpdate()
The WindowManager will shortly call the onRender() method for all affected applets
An Applet may be unexpectedly asked to render at any point in time.
Applets should cache their data, but not their pixel output: they should re-render when onRender runs.
An Applet's dimensions are not know until onRender is called, so pre-rendering of UI elements is prohibited.
Tiles
-----
Applets are assigned to "Tiles".
Assigning an applet to a tile creates a reciprocal link between the two.
When an applet renders, it passes pixels to its tile.
The tile translates these to the correct position, to be placed into the fullscreen framebuffer.
User applets don't get to choose their own tile; the multiplexing is handled by the WindowManager.
System applets might do strange things though.
Foreground and Background
-------------------------
The user can cycle between applets by short-pressing the user button.
Any applets which are currently displayed on the display are "foreground".
When the user button is short pressed, and an applet is hidden, it becomes "background".
Although the WindowManager will not render background applets, they should still collect data,
so they are ready to display when they are brought to foreground again.
Even if they are in background, Applets should still request updates when an event affects them,
as the user may have given them permission to "autoshow"; bringing themselves foreground automatically
Applets can implement the onForeground and onBackground methods to handle this change in state.
They can also check their state by calling isForeground() at any time.
Active and Inactive
-------------------
The user can select which applets are available, using the onscreen applet selection menu.
Applets which are enabled in this menu are "active"; otherwise they are "inactive".
An inactive applet is expected not collect data; not to consume resources.
Applets are activated at boot, or when enabled via the menu.
They are deactivated at shutdown, or when disabled via the menu.
Applets can implement the onActivation and onDeactivation methods to handle this change in state.
*/
#pragma once
#include "configuration.h"
#include <GFX.h>
#include "./AppletFont.h"
#include "./Applets/System/Notification/Notification.h"
#include "./Tile.h"
#include "./Types.h"
#include "./WindowManager.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
using NicheGraphics::Drivers::EInk;
using std::to_string;
class Tile;
class WindowManager;
class Applet : public GFX
{
public:
Applet();
void setTile(Tile *t); // Applets draw via a tile (for multiplexing)
Tile *getTile();
void render();
bool wantsToRender(); // Check whether applet wants to render
bool wantsToAutoshow(); // Check whether applets wants to become foreground, to show new data, if permitted
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
void updateDimensions(); // Get current size from tile
void resetDrawingSpace(); // Makes sure every render starts with same parameters
// Change the applet's state
void activate();
void deactivate();
void bringToForeground();
void sendToBackground();
// Info about applet's state
bool isActive();
bool isForeground();
// Allow derived applets to handle changes in state
virtual void onRender() = 0; // All drawing happens here
virtual void onActivate() {}
virtual void onDeactivate() {}
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
virtual void onButtonShortPress() {} // For use by System Applets only
virtual void onButtonLongPress() {} // For use by System Applets only
virtual void onLockAvailable() {} // For use by System Applets only
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
static void setDefaultFonts(AppletFont large, AppletFont small); // Set the general purpose fonts
static uint16_t getHeaderHeight(); // How tall is the "standard" applet header
const char *name = nullptr; // Shown in applet selection menu
protected:
// Place a single pixel. All drawing methods output through here
void drawPixel(int16_t x, int16_t y, uint16_t color) override;
// Tell WindowManager to update display
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED);
// Ask for applet to be moved to foreground
void requestAutoshow();
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
void resetCrop(); // Removes setCrop()
void setFont(AppletFont f);
AppletFont getFont();
uint16_t getTextWidth(std::string text);
uint16_t getTextWidth(const char *text);
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY);
// Print text, with per-word line wrapping
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text);
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text);
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
void drawHeader(std::string text); // Draw the standard applet header
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height); // Draw the meshtastic logo
std::string hexifyNodeNum(NodeNum num);
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
std::string getTimeString(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
static AppletFont fontSmall, fontLarge; // General purpose fonts, used cross-applet
private:
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
bool active = false; // Has the user enabled this applet (at run-time)?
bool foreground = false; // Is the applet currently drawn on a tile?
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
AppletFont currentFont; // As passed to setFont
// As set by setCrop
int16_t cropLeft;
int16_t cropTop;
uint16_t cropWidth;
uint16_t cropHeight;
};
}; // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,71 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Base class for Applets which display a list of nodes
Used by the "Recents" and "Heard" applets. Possibly more in future?
+-------------------------------+
| | |
| SHRT . | | |
| Long name 50km |
| |
| ABCD 2 Hops |
| abcdedfghijk 30km |
| |
+-------------------------------+
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class NodeListApplet : public Applet, public MeshModule
{
protected:
// Info used to draw one card to the node list
struct CardInfo {
static constexpr uint8_t HOPS_UNKNOWN = -1;
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
NodeNum nodeNum = 0;
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
uint32_t distanceMeters = DISTANCE_UNKNOWN;
uint8_t hopsAway = HOPS_UNKNOWN; // Unknown
};
public:
NodeListApplet(const char *name);
void onRender() override;
// MeshModule overrides
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
protected:
virtual void handleParsed(CardInfo c) = 0; // Pass extracted info from a new packet to derived class, for sorting and storage
virtual std::string getHeaderText() = 0; // Title for the applet's header. Todo: get this info another way?
uint8_t maxCards(); // Calculate the maximum number of cards an applet could ever display
std::deque<CardInfo> cards; // Derived applet places cards here, for this base applet to render
private:
// UI element: a "mobile phone" style signal indicator
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength signal);
// Dimensions for drawing
// Used for render, and also for maxCards calc
const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,108 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./LogoApplet.h"
using namespace NicheGraphics;
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
{
// Don't autostart the runOnce() timer
OSThread::disable();
// Grab the WindowManager singleton, for convenience
windowManager = WindowManager::getInstance();
}
void InkHUD::LogoApplet::onRender()
{
// Size of the region which the logo should "scale to fit"
uint16_t logoWLimit = X(0.8);
uint16_t logoHLimit = Y(0.5);
// Get the max width and height we can manage within the region, while still maintaining aspect ratio
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
// Where to place the center of the logo
int16_t logoCX = X(0.5);
int16_t logoCY = Y(0.5 - 0.05);
drawLogo(logoCX, logoCY, logoW, logoH);
if (!textLeft.empty()) {
setFont(fontSmall);
printAt(0, 0, textLeft, LEFT, TOP);
}
if (!textRight.empty()) {
setFont(fontSmall);
printAt(X(1), 0, textRight, RIGHT, TOP);
}
if (!textTitle.empty()) {
int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo
setFont(fontTitle);
printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP);
}
}
void InkHUD::LogoApplet::onForeground()
{
// If another applet has locked the display, ask it to exit
Applet *other = windowManager->whoLocked();
if (other != nullptr)
other->sendToBackground();
windowManager->claimFullscreen(this); // Take ownership of fullscreen tile
windowManager->lock(this); // Prevent other applets from requesting updates
}
void InkHUD::LogoApplet::onBackground()
{
OSThread::disable(); // Disable auto-dismiss timer, in case applet was dismissed early (sendToBackground from outside class)
windowManager->releaseFullscreen(); // Relinquish ownership of fullscreen tile
windowManager->unlock(this); // Allow normal user applet update requests to resume
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
}
int32_t InkHUD::LogoApplet::runOnce()
{
LOG_DEBUG("Sent to background by timer");
sendToBackground();
return OSThread::disable();
}
// Begin displaying the screen which is shown at startup
// Suggest EInk::await after calling this method
void InkHUD::LogoApplet::showBootScreen()
{
OSThread::setIntervalFromNow(8 * 1000UL);
OSThread::enabled = true;
textLeft = "";
textRight = "";
textTitle = xstr(APP_VERSION_SHORT);
fontTitle = fontSmall;
bringToForeground();
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
}
// Begin displaying the screen which is shown at shutdown
// Needs EInk::await after calling this method, to ensure display updates before shutdown
void InkHUD::LogoApplet::showShutdownScreen()
{
textLeft = "";
textRight = "";
textTitle = owner.short_name;
fontTitle = fontLarge;
bringToForeground();
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
}
#endif

Wyświetl plik

@ -0,0 +1,47 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows the Meshtastic logo fullscreen, with accompanying text
Used for boot and shutdown
*/
#pragma once
#include "configuration.h"
#include "concurrency/OSThread.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class LogoApplet : public Applet, public concurrency::OSThread
{
public:
LogoApplet();
void onRender() override;
void onForeground() override;
void onBackground() override;
// Note: interacting directly with an applet like this is non-standard
// Only permitted because this is a "system applet", which has special behavior and interacts directly with WindowManager
void showBootScreen();
void showShutdownScreen();
protected:
int32_t runOnce() override;
std::string textLeft;
std::string textRight;
std::string textTitle;
AppletFont fontTitle;
WindowManager *windowManager = nullptr; // For convenience
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,30 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Sub-menu for InkHUD::MenuApplet
Structure of the menu is defined in InkHUD::showPage
*/
#pragma once
#include "configuration.h"
namespace NicheGraphics::InkHUD
{
// Sub-menu for MenuApplet
enum MenuPage : uint8_t {
ROOT, // Initial menu page
SEND,
OPTIONS,
APPLETS,
AUTOSHOW,
RECENTS, // Select length of "recentlyActiveSeconds"
EXIT, // Dismiss the menu applet
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,40 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
A notification which might be displayed by the NotificationApplet
An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification.
An Applet should veto a notification if it is already displaying the same info which the notification would convey.
*/
#pragma once
#include "configuration.h"
namespace NicheGraphics::InkHUD
{
class Notification
{
public:
enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type;
uint32_t timestamp;
uint8_t getChannel() { return channel; }
uint32_t getSender() { return sender; }
uint8_t getBatteryPercentage() { return batteryPercentage; }
friend class NotificationApplet;
protected:
uint8_t channel;
uint32_t sender;
uint8_t batteryPercentage;
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,219 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./NotificationApplet.h"
#include "./Notification.h"
#include "RTC.h"
using namespace NicheGraphics;
void InkHUD::NotificationApplet::onActivate()
{
textMessageObserver.observe(textMessageModule);
}
// Note: This applet probably won't ever be deactivated
void InkHUD::NotificationApplet::onDeactivate()
{
textMessageObserver.unobserve(textMessageModule);
}
// Collect meta-info about the text message, and ask for approval for the notification
// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render()
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
{
// System applets are always active
assert(isActive());
// Abort if feature disabled
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
if (!settings.optionalFeatures.notifications)
return 0;
// Abort if this is an outgoing message
if (getFrom(p) == nodeDB->getNodeNum())
return 0;
// Abort if message was only an "emoji reaction"
// Possibly some implemetation of this in future?
if (p->decoded.emoji)
return 0;
Notification n;
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
// Gather info: in-channel message
if (isBroadcast(p->to)) {
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
n.channel = p->channel;
}
// Gather info: DM
else {
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
n.sender = p->from;
}
// Check if we should display the notification
// A foreground applet might already be displaying this info
hasNotification = true;
currentNotification = n;
if (isApproved()) {
bringToForeground();
WindowManager::getInstance()->forceUpdate();
} else
hasNotification = false; // Clear the pending notification: it was rejected
// Return zero: no issues here, carry on notifying other observers!
return 0;
}
void InkHUD::NotificationApplet::onRender()
{
// Clear the region beneath the tile
// Most applets are drawing onto an empty frame buffer and don't need to do this
// We do need to do this with the battery though, as it is an "overlay"
fillRect(0, 0, width(), height(), WHITE);
setFont(fontSmall);
// Padding (horizontal)
const uint16_t padW = 4;
// Main border
drawRect(0, 0, width(), height(), BLACK);
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
// Timestamp (potentially)
// ====================
std::string ts = getTimeString(currentNotification.timestamp);
uint16_t tsW = 0;
int16_t divX = 0;
// Timestamp available
if (ts.length() > 0) {
tsW = getTextWidth(ts);
divX = padW + tsW + padW;
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
setCrop(1, 1, divX - 1, height() - 2);
// Drop shadow
setTextColor(WHITE);
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
// Bold text
setTextColor(BLACK);
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
}
// Main text
// =====================
// Background fill
// - medium dark (1/3)
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
uint16_t availableWidth = width() - divX - padW;
std::string text = getNotificationText(availableWidth);
int16_t textM = divX + padW + (getTextWidth(text) / 2);
// Restrict area for printing
// - don't overlap border, or diveder
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
// Drop shadow
// - thick white text
setTextColor(WHITE);
printThick(textM, height() / 2, text, 4, 4);
// Main text
// - faux bold: double width
setTextColor(BLACK);
printThick(textM, height() / 2, text, 2, 1);
}
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
// Called internally when we first get a "notifiable event", and then again before render,
// in case autoshow swapped which applet was displayed
bool InkHUD::NotificationApplet::isApproved()
{
// Instead of an assert
if (!hasNotification) {
LOG_WARN("No notif to approve");
return false;
}
return WindowManager::getInstance()->approveNotification(currentNotification);
}
// Mark that the notification should no-longer be rendered
// In addition to calling thing method, code needs to request a re-render of all applets
void InkHUD::NotificationApplet::dismiss()
{
sendToBackground();
hasNotification = false;
// Not requesting update directly from this method,
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn
}
// Get a string for the main body text of a notification
// Formatted to suit screen width
// Takes info from InkHUD::currentNotification
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable)
{
assert(hasNotification);
std::string text;
// Text message
// ==============
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT,
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
// Pick source of message
MessageStore::Message *message = isBroadcast ? &latestMessage.broadcast : &latestMessage.dm;
// Find info about the sender
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
// Leading tag (channel vs. DM)
text += isBroadcast ? "From:" : "DM: ";
// Sender id
if (node && node->has_user)
text += node->user.short_name;
else
text += hexifyNodeNum(message->sender);
// Check if text fits
// - use a longer string, if we have the space
if (getTextWidth(text) < widthAvailable * 0.5) {
text.clear();
// Leading tag (channel vs. DM)
text += isBroadcast ? "Msg from " : "DM from ";
// Sender id
if (node && node->has_user)
text += node->user.short_name;
else
text += hexifyNodeNum(message->sender);
text += ": ";
text += message->text;
}
}
return text;
}
#endif

Wyświetl plik

@ -0,0 +1,49 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Pop-up notification bar, on screen top edge
Displays information we feel is important, but which is not shown on currently focussed applet(s)
E.g.: messages, while viewing map, etc
Feature should be optional; enable disable via on-screen menu
*/
#pragma once
#include "configuration.h"
#include "concurrency/OSThread.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class NotificationApplet : public Applet
{
public:
void onRender() override;
void onActivate() override;
void onDeactivate() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
bool isApproved(); // Does a foreground applet make notification redundant?
void dismiss(); // Close the Notification Popup
protected:
// Get notified when a new text message arrives
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *>(this, &NotificationApplet::onReceiveTextMessage);
std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width
bool hasNotification = false; // Only used for assert. Todo: remove?
Notification currentNotification; // Set when something notification-worthy happens. Used by render()
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,96 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./PairingApplet.h"
using namespace NicheGraphics;
InkHUD::PairingApplet::PairingApplet()
{
// Grab the window manager singleton, for convenience
windowManager = WindowManager::getInstance();
}
void InkHUD::PairingApplet::onRender()
{
// Header
setFont(fontLarge);
printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM);
setFont(fontSmall);
printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP);
// Passkey
setFont(fontLarge);
printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2);
// Device's bluetooth name, if it will fit
setFont(fontSmall);
std::string name = "Name: " + std::string(getDeviceName());
if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: "
name = std::string(getDeviceName());
if (getTextWidth(name) < width()) // Does it fit?
printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE);
}
void InkHUD::PairingApplet::onActivate()
{
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
}
void InkHUD::PairingApplet::onDeactivate()
{
bluetoothStatusObserver.unobserve(&bluetoothStatus->onNewStatus);
}
void InkHUD::PairingApplet::onForeground()
{
// If another applet has locked the display, ask it to exit
Applet *other = windowManager->whoLocked();
if (other != nullptr)
other->sendToBackground();
windowManager->claimFullscreen(this); // Take ownership of the fullscreen tile
windowManager->lock(this); // Prevent user applets from requesting update
}
void InkHUD::PairingApplet::onBackground()
{
windowManager->releaseFullscreen(); // Relinquish ownership of the fullscreen tile
windowManager->unlock(this); // Allow normal user applet update requests to resume
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
}
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
{
// The standard Meshtastic convention is to pass these "generic" Status objects,
// check their type, and then cast them.
// We'll mimic that behavior, just to keep in line with the other Statuses,
// even though I'm not sure what the original reason for jumping through these extra hoops was.
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
// When pairing begins
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
// Store the passkey for rendering
passkey = bluetoothStatus->getPasskey();
// Make sure no other system applets have a lock on the display
// Boot screen, menu, etc
Applet *lockOwner = windowManager->whoLocked();
if (lockOwner)
lockOwner->sendToBackground();
// Show pairing screen
bringToForeground();
}
// When pairing ends
// or rather, when something changes, and we shouldn't be showing the pairing screen
else if (isForeground())
sendToBackground();
return 0; // No special result to report back to Observable
}
#endif

Wyświetl plik

@ -0,0 +1,43 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows the Bluetooth passkey during pairing
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class PairingApplet : public Applet
{
public:
PairingApplet();
void onRender() override;
void onActivate() override;
void onDeactivate() override;
void onForeground() override;
void onBackground() override;
int onBluetoothStatusUpdate(const meshtastic::Status *status);
protected:
// Get notified when status of the Bluetooth connection changes
CallbackObserver<PairingApplet, const meshtastic::Status *> bluetoothStatusObserver =
CallbackObserver<PairingApplet, const meshtastic::Status *>(this, &PairingApplet::onBluetoothStatusUpdate);
std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros
WindowManager *windowManager = nullptr; // For convenience. Set in constructor.
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,21 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./PlaceholderApplet.h"
using namespace NicheGraphics;
InkHUD::PlaceholderApplet::PlaceholderApplet()
{
// Because this applet sometimes gets processed as if it were a bonafide user applet,
// it's probably better that we do give it a human readable name, just in case it comes up later.
// For genuine user applets, this is set by WindowManager::addApplet
Applet::name = "Placeholder";
}
void InkHUD::PlaceholderApplet::onRender()
{
// This placeholder applet fills its area with sparse diagonal lines
hatchRegion(0, 0, width(), height(), 8, BLACK);
}
#endif

Wyświetl plik

@ -0,0 +1,30 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shown when a tile doesn't have any other valid Applets
Fills the area with diagonal lines
*/
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class PlaceholderApplet : public Applet
{
public:
PlaceholderApplet();
void onRender() override;
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
// The window manager decides when and where it should be rendered
// It may be drawn to several different tiles during on WindowManager::render call
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,234 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./TipsApplet.h"
using namespace NicheGraphics;
InkHUD::TipsApplet::TipsApplet()
{
// Grab the window manager singleton, for convenience
windowManager = WindowManager::getInstance();
}
void InkHUD::TipsApplet::onRender()
{
switch (tipQueue.front()) {
case Tip::WELCOME:
renderWelcome();
break;
case Tip::FINISH_SETUP: {
setFont(fontLarge);
printAt(0, 0, "Tip: Finish Setup");
setFont(fontSmall);
int16_t cursorY = fontLarge.lineHeight() * 1.5;
printAt(0, cursorY, "- connect antenna");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- connect a client app");
// Only if region not set
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- set region");
}
// Only if tz not set
if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) {
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- set timezone");
}
cursorY += fontSmall.lineHeight() * 1.5;
printAt(0, cursorY, "More info at meshtastic.org");
setFont(fontSmall);
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break;
case Tip::SAFE_SHUTDOWN: {
setFont(fontLarge);
printAt(0, 0, "Tip: Shutdown");
setFont(fontSmall);
std::string shutdown;
shutdown += "Before removing power, please shutdown from InkHUD menu, or a client app. \n";
shutdown += "\n";
shutdown += "This ensures data is saved.";
printWrapped(0, fontLarge.lineHeight() * 1.5, width(), shutdown);
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break;
case Tip::CUSTOMIZATION: {
setFont(fontLarge);
printAt(0, 0, "Tip: Customization");
setFont(fontSmall);
printWrapped(0, fontLarge.lineHeight() * 1.5, width(),
"Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more.");
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break;
case Tip::BUTTONS: {
setFont(fontLarge);
printAt(0, 0, "Tip: Buttons");
setFont(fontSmall);
int16_t cursorY = fontLarge.lineHeight() * 1.5;
printAt(0, cursorY, "User Button");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- short press: next");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- long press: select / open menu");
cursorY += fontSmall.lineHeight() * 1.5;
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break;
case Tip::ROTATION: {
setFont(fontLarge);
printAt(0, 0, "Tip: Rotation");
setFont(fontSmall);
printWrapped(0, fontLarge.lineHeight() * 1.5, width(),
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
// Revert the "flip screen" setting, preventing this message showing again
config.display.flip_screen = false;
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
} break;
}
}
// This tip has its own render method, only because it's a big block of code
// Didn't want to clutter up the switch in onRender too much
void InkHUD::TipsApplet::renderWelcome()
{
uint16_t padW = X(0.05);
// Block 1 - logo & title
// ========================
// Logo size
uint16_t logoWLimit = X(0.3);
uint16_t logoHLimit = Y(0.3);
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
// Title size
setFont(fontLarge);
std::string title;
if (width() >= 200) // Future proofing: hide if *tiny* display
title = "meshtastic.org";
uint16_t titleW = getTextWidth(title);
// Center the block
// Desired effect: equal margin from display edge for logo left and title right
int16_t block1Y = Y(0.3);
int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2);
int16_t logoCX = block1CX - (logoW / 2) - (padW / 2);
int16_t titleCX = block1CX + (titleW / 2) + (padW / 2);
// Draw block
drawLogo(logoCX, block1Y, logoW, logoH);
printAt(titleCX, block1Y, title, CENTER, MIDDLE);
// Block 2 - subtitle
// =======================
setFont(fontSmall);
std::string subtitle = "InkHUD";
if (width() >= 200)
subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display
printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE);
// Block 3 - press to continue
// ============================
printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM);
}
// Grab fullscreen tile, and lock the window manager, when applet is shown
void InkHUD::TipsApplet::onForeground()
{
windowManager->lock(this);
windowManager->claimFullscreen(this);
}
void InkHUD::TipsApplet::onBackground()
{
windowManager->releaseFullscreen();
windowManager->unlock(this);
}
void InkHUD::TipsApplet::onActivate()
{
// Decide which tips (if any) should be shown to user after the boot screen
// Welcome screen
if (settings.tips.firstBoot)
tipQueue.push_back(Tip::WELCOME);
// Antenna, region, timezone
// Shown at boot if region not yet set
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
tipQueue.push_back(Tip::FINISH_SETUP);
// Shutdown info
// Shown until user performs one valid shutdown
if (!settings.tips.safeShutdownSeen)
tipQueue.push_back(Tip::SAFE_SHUTDOWN);
// Using the UI
if (settings.tips.firstBoot) {
tipQueue.push_back(Tip::CUSTOMIZATION);
tipQueue.push_back(Tip::BUTTONS);
}
// Catch an incorrect attempt at rotating display
if (config.display.flip_screen)
tipQueue.push_back(Tip::ROTATION);
// Applet will be brought to foreground when boot screen closes, via TipsApplet::onLockAvailable
}
// While our applet has the window manager locked, we will receive the button input
void InkHUD::TipsApplet::onButtonShortPress()
{
tipQueue.pop_front();
// All tips done
if (tipQueue.empty()) {
// Record that user has now seen the "tutorial" set of tips
// Don't show them on subsequent boots
if (settings.tips.firstBoot) {
settings.tips.firstBoot = false;
saveDataToFlash();
}
// Close applet, and full refresh to clean the screen
// Need to force update, because our request would be ignored otherwise, as we are now background
sendToBackground();
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
}
// More tips left
else
requestUpdate();
}
// If the wm lock has just become availale (rendering, input), and we've still got tips, grab it!
// This situation would arise if bluetooth pairing occurs while TipsApplet was already shown (after pairing)
// Note: this event is only raised when *other* applets unlock the window manager
void InkHUD::TipsApplet::onLockAvailable()
{
if (!tipQueue.empty())
bringToForeground();
}
#endif

Wyświetl plik

@ -0,0 +1,52 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows info on how to use InkHUD
- tutorial at first boot
- additional tips in certain situation (e.g. bad shutdown, region unset)
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class TipsApplet : public Applet
{
protected:
enum class Tip {
WELCOME,
FINISH_SETUP,
SAFE_SHUTDOWN,
CUSTOMIZATION,
BUTTONS,
ROTATION,
};
public:
TipsApplet();
void onRender() override;
void onActivate() override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
void onLockAvailable() override; // Reopen if interrupted by bluetooth pairing
protected:
void renderWelcome(); // Very first screen of tutorial
std::deque<Tip> tipQueue; // List of tips to show, one after another
WindowManager *windowManager = nullptr; // For convenience. Set in constructor.
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,133 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./AllMessageApplet.h"
using namespace NicheGraphics;
void InkHUD::AllMessageApplet::onActivate()
{
textMessageObserver.observe(textMessageModule);
}
void InkHUD::AllMessageApplet::onDeactivate()
{
textMessageObserver.unobserve(textMessageModule);
}
// We're not consuming the data passed to this method;
// we're just just using it to trigger a render
int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
{
// Abort if applet fully deactivated
// Already handled by onActivate and onDeactivate, but good practice for all applets
if (!isActive())
return 0;
// Abort if this is an outgoing message
if (getFrom(p) == nodeDB->getNodeNum())
return 0;
// Abort if message was only an "emoji reaction"
// Possibly some implemetation of this in future?
if (p->decoded.emoji)
return 0;
requestAutoshow(); // Want to become foreground, if permitted
requestUpdate(); // Want to update display, if applet is foreground
// Return zero: no issues here, carry on notifying other observers!
return 0;
}
void InkHUD::AllMessageApplet::onRender()
{
setFont(fontSmall);
// Find newest message, regardless of whether DM or broadcast
MessageStore::Message *message;
if (latestMessage.wasBroadcast)
message = &latestMessage.broadcast;
else
message = &latestMessage.dm;
// Short circuit: no text message
if (!message->sender) {
printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE);
return;
}
// ===========================
// Header (sender, timestamp)
// ===========================
// Y position for divider
// - between header text and messages
std::string header;
// RX Time
// - if valid
std::string timeString = getTimeString(message->timestamp);
if (timeString.length() > 0) {
header += timeString;
header += ": ";
}
// Sender's id
// - shortname, if available, or
// - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
if (sender && sender->has_user) {
header += sender->user.short_name;
header += " (";
header += sender->user.long_name;
header += ")";
} else
header += hexifyNodeNum(message->sender);
// Draw a "standard" applet header
drawHeader(header);
// Fade the right edge of the header, if text spills over edge
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
uint8_t hF = getHeaderHeight(); // Height of fade effect
if (getCursorX() > width())
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
// Dimensions of the header
constexpr int16_t padDivH = 2;
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
// ===================
// Print message text
// ===================
// Extra gap below the header
int16_t textTop = headerDivY + padDivH;
// Determine size if printed large
setFont(fontLarge);
uint32_t textHeight = getWrappedTextHeight(0, width(), message->text);
// If too large, swap to small font
if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned)
setFont(fontSmall);
// Print text
printWrapped(0, textTop, width(), message->text);
}
// Don't show notifications for text messages when our applet is displayed
bool InkHUD::AllMessageApplet::approveNotification(Notification &n)
{
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)
return false;
else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
return false;
else
return true;
}
#endif

Wyświetl plik

@ -0,0 +1,49 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows the latest incoming text message, as well as sender.
Both broadcast and direct messages will be shown here, from all channels.
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
We do still receive notifications from the text message module though,
to know when a new message has arrived, and trigger the update.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "modules/TextMessageModule.h"
namespace NicheGraphics::InkHUD
{
class Applet;
class AllMessageApplet : public Applet
{
public:
void onRender() override;
void onActivate() override;
void onDeactivate() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
bool approveNotification(Notification &n) override; // Which notifications to suppress
protected:
// Used to register our text message callback
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *>(this, &AllMessageApplet::onReceiveTextMessage);
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,126 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./DMApplet.h"
using namespace NicheGraphics;
void InkHUD::DMApplet::onActivate()
{
textMessageObserver.observe(textMessageModule);
}
void InkHUD::DMApplet::onDeactivate()
{
textMessageObserver.unobserve(textMessageModule);
}
// We're not consuming the data passed to this method;
// we're just just using it to trigger a render
int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
{
// Abort if applet fully deactivated
// Already handled by onActivate and onDeactivate, but good practice for all applets
if (!isActive())
return 0;
// Abort if only an "emoji reactions"
// Possibly some implemetation of this in future?
if (p->decoded.emoji)
return 0;
// If DM (not broadcast)
if (!isBroadcast(p->to)) {
// Want to update display, if applet is foreground
requestUpdate();
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
if (getFrom(p) != nodeDB->getNodeNum())
requestAutoshow();
}
// Return zero: no issues here, carry on notifying other observers!
return 0;
}
void InkHUD::DMApplet::onRender()
{
setFont(fontSmall);
// Abort if no text message
if (!latestMessage.dm.sender) {
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
return;
}
// ===========================
// Header (sender, timestamp)
// ===========================
// Y position for divider
// - between header text and messages
std::string header;
// RX Time
// - if valid
std::string timeString = getTimeString(latestMessage.dm.timestamp);
if (timeString.length() > 0) {
header += timeString;
header += ": ";
}
// Sender's id
// - shortname, if available, or
// - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage.dm.sender);
if (sender && sender->has_user) {
header += sender->user.short_name;
header += " (";
header += sender->user.long_name;
header += ")";
} else
header += hexifyNodeNum(latestMessage.dm.sender);
// Draw a "standard" applet header
drawHeader(header);
// Fade the right edge of the header, if text spills over edge
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
uint8_t hF = getHeaderHeight(); // Height of fade effect
if (getCursorX() > width())
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
// Dimensions of the header
constexpr int16_t padDivH = 2;
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
// ===================
// Print message text
// ===================
// Extra gap below the header
int16_t textTop = headerDivY + padDivH;
// Determine size if printed large
setFont(fontLarge);
uint32_t textHeight = getWrappedTextHeight(0, width(), latestMessage.dm.text);
// If too large, swap to small font
if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned)
setFont(fontSmall);
// Print text
printWrapped(0, textTop, width(), latestMessage.dm.text);
}
// Don't show notifications for direct messages when our applet is displayed
bool InkHUD::DMApplet::approveNotification(Notification &n)
{
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
return false;
else
return true;
}
#endif

Wyświetl plik

@ -0,0 +1,49 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows the latest incoming *Direct Message* (DM), as well as sender.
This compliments the threaded message applets
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
We do still receive notifications from the text message module though,
to know when a new message has arrived, and trigger the update.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "modules/TextMessageModule.h"
namespace NicheGraphics::InkHUD
{
class Applet;
class DMApplet : public Applet
{
public:
void onRender() override;
void onActivate() override;
void onDeactivate() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
bool approveNotification(Notification &n) override; // Which notifications to suppress
protected:
// Used to register our text message callback
CallbackObserver<DMApplet, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<DMApplet, const meshtastic_MeshPacket *>(this, &DMApplet::onReceiveTextMessage);
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,123 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "RTC.h"
#include "gps/GeoCoord.h"
#include "./HeardApplet.h"
using namespace NicheGraphics;
void InkHUD::HeardApplet::onActivate()
{
// When applet begins, pre-fill with stale info from NodeDB
populateFromNodeDB();
}
void InkHUD::HeardApplet::onDeactivate()
{
// Avoid an unlikely situation where frquent activation / deactivation populated duplicate info from node DB
cards.clear();
}
// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo
// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result
void InkHUD::HeardApplet::handleParsed(CardInfo c)
{
// Grab the previous entry.
// To check if the new data is different enough to justify re-render
// Need to cache now, before we manipulate the deque
CardInfo previous;
if (!cards.empty())
previous = cards.at(0);
// If we're updating an existing entry, remove the old one. Will reinsert at front
for (auto it = cards.begin(); it != cards.end(); ++it) {
if (it->nodeNum == c.nodeNum) {
cards.erase(it);
break;
}
}
cards.push_front(c); // Insert into base class' card collection
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
// Our rendered image needs to change if:
if (previous.nodeNum != c.nodeNum // Different node
|| previous.signal != c.signal // or different signal strength
|| previous.distanceMeters != c.distanceMeters // or different position
|| previous.hopsAway != c.hopsAway) // or different hops away
{
requestAutoshow();
requestUpdate();
}
}
// When applet is activated, pre-fill with stale data from NodeDB
// We're sorting using the last_heard value. Succeptible to weirdness if node's RTC changes.
// No SNR is available in node db, so we can't calculate signal either
// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead
void InkHUD::HeardApplet::populateFromNodeDB()
{
// Fill a collection with pointers to each node in db
std::vector<meshtastic_NodeInfoLite *> ordered;
for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) {
// Only copy if valid, and not our own node
if (mn->num != 0 && mn->num != nodeDB->getNodeNum())
ordered.push_back(&*mn);
}
// Sort the collection by age
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
return (top->last_heard > bottom->last_heard);
});
// Keep the most recent entries onlyt
// Just enough to fill the screen
if (ordered.size() > maxCards())
ordered.resize(maxCards());
// Create card info for these (stale) node observations
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
for (meshtastic_NodeInfoLite *node : ordered) {
CardInfo c;
c.nodeNum = node->num;
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);
}
// Insert into the card collection (member of base class)
cards.push_back(c);
}
}
// Text drawn in the usual applet header
// Handled by base class: ChronoListApplet
std::string InkHUD::HeardApplet::getHeaderText()
{
uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node
std::string text = "Heard: ";
// Print node count, if nodeDB not yet nearing full
if (nodeCount < MAX_NUM_NODES) {
text += to_string(nodeCount); // Max nodes
text += " ";
text += (nodeCount == 1) ? "node" : "nodes";
}
return text;
}
#endif

Wyświetl plik

@ -0,0 +1,35 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows a list of all nodes (recently heard or not), sorted by time last heard.
Most of the work is done by the InkHUD::NodeListApplet base class
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h"
namespace NicheGraphics::InkHUD
{
class HeardApplet : public NodeListApplet
{
public:
HeardApplet() : NodeListApplet("HeardApplet") {}
void onActivate() override;
void onDeactivate() override;
protected:
void handleParsed(CardInfo c) override; // Store new info, and update display if needed
std::string getHeaderText() override; // Set title for this applet
void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,110 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./PositionsApplet.h"
using namespace NicheGraphics;
void InkHUD::PositionsApplet::onRender()
{
// Draw the usual map applet first
MapApplet::onRender();
// Draw our latest "node of interest" as a special marker
// -------------------------------------------------------
// We might be rendering because we got a position packet from them
// We might be rendering because our own position updated
// Either way, we still highlight which node most recently sent us a position packet
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
if (node && nodeDB->hasValidPosition(node) && enoughMarkers())
drawLabeledMarker(node);
}
// Determine if we need to redraw the map, when we receive a new position packet
ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
// If applet is not active, we shouldn't be handling any data
// It's good practice for all applets to implement an early return like this
// for PositionsApplet, this is **required** - it's where we're handling active vs deactive
if (!isActive())
return ProcessMessage::CONTINUE;
// Try decode a position from the packet
bool hasPosition = false;
float lat;
float lng;
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
meshtastic_Position position = meshtastic_Position_init_default;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
if (position.has_latitude_i && position.has_longitude_i // Actually has position
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
{
hasPosition = true;
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
lng = position.longitude_i * 1e-7;
}
}
}
// Skip if we didn't get a valid position
if (!hasPosition)
return ProcessMessage::CONTINUE;
bool hasHopsAway = (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start); // From NodeDB::updateFrom
uint8_t hopsAway = mp.hop_start - mp.hop_limit;
// Determine if the position packet would change anything on-screen
// -----------------------------------------------------------------
bool somethingChanged = false;
// If our own position
if (isFromUs(&mp)) {
// We get frequent position updates from connected phone
// Only update if we're travelled some distance, for rate limiting
// Todo: smarter detection of position changes
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
somethingChanged = true;
ourLastLat = lat;
ourLastLng = lng;
}
}
// If someone else's position
else {
// Check if this position is from someone different than our previous position packet
if (mp.from != lastFrom) {
somethingChanged = true;
lastFrom = mp.from;
lastLat = lat;
lastLng = lng;
lastHopsAway = hopsAway;
}
// Same sender: check if position changed
// Todo: smarter detection of position changes
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
somethingChanged = true;
lastLat = lat;
lastLng = lng;
}
// Same sender, same position: check if hops changed
// Only pay attention if the hopsAway value is valid
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
somethingChanged = true;
lastHopsAway = hopsAway;
}
}
// Decision reached
// -----------------
if (somethingChanged) {
requestAutoshow(); // Todo: only request this in some situations?
requestUpdate();
}
return ProcessMessage::CONTINUE;
}
#endif

Wyświetl plik

@ -0,0 +1,43 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Plots position of all nodes from DB, with North facing up.
Scaled to fit the most distant node.
Size of cross represents hops away.
The node which has most recently sent a position will be labeled.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
#include "SinglePortModule.h"
namespace NicheGraphics::InkHUD
{
class PositionsApplet : public MapApplet, public SinglePortModule
{
public:
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
void onRender() override;
protected:
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
NodeNum lastFrom; // Sender of most recent (non-local) position packet
float lastLat;
float lastLng;
float lastHopsAway;
float ourLastLat; // Info about the most recent (non-local) position packet
float ourLastLng; // Info about most recent *local* position
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,150 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./RecentsListApplet.h"
#include "RTC.h"
using namespace NicheGraphics;
InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet")
{
// No scheduled tasks initially
OSThread::disable();
}
void InkHUD::RecentsListApplet::onActivate()
{
// When the applet is activated, begin scheduled purging of any nodes which are no longer "active"
OSThread::enabled = true;
OSThread::setIntervalFromNow(60 * 1000UL); // Every minute
}
void InkHUD::RecentsListApplet::onDeactivate()
{
// Halt scheduled purging
OSThread::disable();
}
int32_t InkHUD::RecentsListApplet::runOnce()
{
prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently
return OSThread::interval;
}
// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo
// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result
// We also need to record the current time against the nodenum, so we know when it becomes inactive
void InkHUD::RecentsListApplet::handleParsed(CardInfo c)
{
// Grab the previous entry.
// To check if the new data is different enough to justify re-render
// Need to cache now, before we manipulate the deque
CardInfo previous;
if (!cards.empty())
previous = cards.at(0);
// If we're updating an existing entry, remove the old one. Will reinsert at front
for (auto it = cards.begin(); it != cards.end(); ++it) {
if (it->nodeNum == c.nodeNum) {
cards.erase(it);
break;
}
}
cards.push_front(c); // Store this CardInfo
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
// Record the time of this observation
// Used to count active nodes, and to know when to prune inactive nodes
seenNow(c.nodeNum);
// Our rendered image needs to change if:
if (previous.nodeNum != c.nodeNum // Different node
|| previous.signal != c.signal // or different signal strength
|| previous.distanceMeters != c.distanceMeters // or different position
|| previous.hopsAway != c.hopsAway) // or different hops away
{
prune(); // Take the opportunity now to remove inactive nodes
requestAutoshow();
requestUpdate();
}
}
// Record the time (millis, right now) that we hear a node
// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly
void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum)
{
// If we're updating an existing entry, remove the old one. Will reinsert at front
for (auto it = ages.begin(); it != ages.end(); ++it) {
if (it->nodeNum == nodeNum) {
ages.erase(it);
break;
}
}
Age a;
a.nodeNum = nodeNum;
a.seenAtMs = millis();
ages.push_front(a);
}
// Remove Card and Age info for any nodes which are now inactive
// Determined by when a node was last heard, in our internal record (not from nodeDB)
void InkHUD::RecentsListApplet::prune()
{
// Iterate age records from newest to oldest
for (uint16_t i = 0; i < ages.size(); i++) {
// Found the first record which is too old
if (!isActive(ages.at(i).seenAtMs)) {
// Drop this item, and all others behind it
ages.resize(i);
cards.resize(i);
// Request an update, if pruning did modify our data
// Required if pruning was scheduled. Redundent if pruning was prior to rendering.
requestAutoshow();
requestUpdate();
break;
}
}
// Push next scheduled pruning back
// Pruning may be called from by handleParsed, immediately prior to rendering
// In that case, we can slightly delay our scheduled pruning
OSThread::setIntervalFromNow(60 * 1000UL);
}
// Is a timestamp old enough that it would make a node inactive, and in need of purging?
bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs)
{
uint32_t now = millis();
uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe
return (secsAgo < settings.recentlyActiveSeconds);
}
// Text to be shown at top of applet
// ChronoListApplet base class allows us to set this dynamically
// Might want to adjust depending on node count, RTC status, etc
std::string InkHUD::RecentsListApplet::getHeaderText()
{
std::string text;
// Print the length of our "Recents" time-window
text += "Last ";
text += to_string(settings.recentlyActiveSeconds / 60);
text += " mins";
// Print the node count
const uint16_t nodeCount = ages.size();
text += ": ";
text += to_string(nodeCount);
text += " ";
text += (nodeCount == 1) ? "node" : "nodes";
return text;
}
#endif

Wyświetl plik

@ -0,0 +1,52 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Shows a list of nodes which have been recently active
The length of this "recently active" window is configurable using the onscreen menu
Most of the work is done by the shared InkHUD::NodeListApplet base class
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h"
namespace NicheGraphics::InkHUD
{
class RecentsListApplet : public NodeListApplet, public concurrency::OSThread
{
protected:
// Used internally to count the number of active nodes
// We count for ourselves, instead of using the value provided by NodeDB,
// as the values occasionally differ, due to the timing of our Applet's purge method
struct Age {
uint32_t nodeNum;
uint32_t seenAtMs;
};
public:
RecentsListApplet();
void onActivate() override;
void onDeactivate() override;
protected:
int32_t runOnce() override;
void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed
std::string getHeaderText() override; // Set title for this applet
void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count
void prune(); // Remove cards for nodes which we haven't seen recently
bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it?
std::deque<Age> ages; // Information about when we last heard nodes. Independent of NodeDB
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,270 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./ThreadedMessageApplet.h"
#include "RTC.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
// Hard limits on how much message data to write to flash
// Avoid filling the storage if something goes wrong
// Normal usage should be well below this size
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex)
{
// Create the message store
// Will shortly attempt to load messages from RAM, if applet is active
// Label (filename in flash) is set from channel index
store = new MessageStore("ch" + to_string(channelIndex));
}
void InkHUD::ThreadedMessageApplet::onRender()
{
setFont(fontSmall);
// =============
// Draw a header
// =============
// Header text
std::string headerText;
headerText += "Channel ";
headerText += to_string(channelIndex);
headerText += ": ";
if (channels.isDefaultChannel(channelIndex))
headerText += "Public";
else
headerText += channels.getByIndex(channelIndex).settings.name;
// Draw a "standard" applet header
drawHeader(headerText);
// Y position for divider
const int16_t dividerY = Applet::getHeaderHeight() - 1;
// ==================
// Draw each message
// ==================
// Restrict drawing area
// - don't overdraw the header
// - small gap below divider
setCrop(0, dividerY + 2, width(), height() - (dividerY + 2));
// Set padding
// - separates text from the vertical line which marks its edge
constexpr uint16_t padW = 2;
constexpr int16_t msgL = padW;
const int16_t msgR = (width() - 1) - padW;
const uint16_t msgW = (msgR - msgL) + 1;
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
uint8_t i = 0; // Index of stored message
// Loop over messages
// - until no messages left, or
// - until no part of message fits on screen
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
// Grab data for message
MessageStore::Message &m = store->messages.at(i);
bool outgoing = (m.sender == 0);
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
// Cache bottom Y of message text
// - Used when drawing vertical line alongside
const int16_t dotsB = msgB;
// Get dimensions for message text
uint16_t bodyH = getWrappedTextHeight(msgL, msgW, m.text);
int16_t bodyT = msgB - bodyH;
// Print message
// - if incoming
if (!outgoing)
printWrapped(msgL, bodyT, msgW, m.text);
// Print message
// - if outgoing
else {
if (getTextWidth(m.text) < width()) // If short,
printAt(msgR, bodyT, m.text, RIGHT); // print right align
else // If long,
printWrapped(msgL, bodyT, msgW, m.text); // need printWrapped(), which doesn't support right align
}
// Move cursor up
// - above message text
msgB -= bodyH;
msgB -= getFont().lineHeight() * 0.2; // Padding between message and header
// Compose info string
// - shortname, if possible, or "me"
// - time received, if possible
std::string info;
if (sender && sender->has_user)
info += sender->user.short_name;
else if (outgoing)
info += "Me";
else
info += hexifyNodeNum(m.sender);
std::string timeString = getTimeString(m.timestamp);
if (timeString.length() > 0) {
info += " - ";
info += timeString;
}
// Print the info string
// - Faux bold: printed twice, shifted horizontally by one px
printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
// Underline the info string
const int16_t divY = msgB;
int16_t divL;
int16_t divR;
if (!outgoing) {
// Left side - incoming
divL = msgL;
divR = getTextWidth(info) + getFont().lineHeight() / 2;
} else {
// Right side - outgoing
divR = msgR;
divL = divR - getTextWidth(info) - getFont().lineHeight() / 2;
}
for (int16_t x = divL; x <= divR; x += 2)
drawPixel(x, divY, BLACK);
// Move cursor up: above info string
msgB -= fontSmall.lineHeight();
// Vertical line alongside message
for (int16_t y = msgB; y < dotsB; y += 1)
drawPixel(outgoing ? width() - 1 : 0, y, BLACK);
// Move cursor up: padding before next message
msgB -= fontSmall.lineHeight() * 0.5;
i++;
} // End of loop: drawing each message
// Fade effect:
// Area immediately below the divider. Overdraw with sparse white lines.
// Make text appear to pass behind the header
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
// Those messages have been pushed off the screen-top by newer ones
while (i < store->messages.size())
store->messages.pop_back();
}
// Code which runs when the applet begins running
// This might happen at boot, or if user enables the applet at run-time, via the menu
void InkHUD::ThreadedMessageApplet::onActivate()
{
loadMessagesFromFlash();
textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage
}
// Code which runs when the applet stop running
// This might be happen at shutdown, or if user disables the applet at run-time
void InkHUD::ThreadedMessageApplet::onDeactivate()
{
textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage
}
// Handle new text messages
// These might be incoming, from the mesh, or outgoing from phone
// Each instance of the ThreadMessageApplet will only listen on one specific channel
// Method should return 0, to indicate general success to TextMessageModule
int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
{
// Abort if applet fully deactivated
// Already handled by onActivate and onDeactivate, but good practice for all applets
if (!isActive())
return 0;
// Abort if wrong channel
if (p->channel != this->channelIndex)
return 0;
// Abort if message was a DM
if (p->to != NODENUM_BROADCAST)
return 0;
// Abort if messages was an "emoji reaction"
// Possibly some implemetation of this in future?
if (p->decoded.emoji)
return 0;
// Extract info into our slimmed-down "StoredMessage" type
MessageStore::Message newMessage;
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
newMessage.sender = p->from;
newMessage.channelIndex = p->channel;
newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]);
// Store newest message at front
// These records are used when rendering, and also stored in flash at shutdown
store->messages.push_front(newMessage);
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
if (getFrom(p) != nodeDB->getNodeNum())
requestAutoshow();
// Redraw the applet, perhaps.
requestUpdate(); // Want to update display, if applet is foreground
return 0;
}
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
{
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex)
return false;
// None of our business. Allow the notification.
else
return true;
}
// Save several recent messages to flash
// Stores the contents of ThreadedMessageApplet::messages
// Just enough messages to fill the display
// Messages are packed "back-to-back", to minimize blocks of flash used
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
{
// Create a label (will become the filename in flash)
std::string label = "ch" + to_string(channelIndex);
store->saveToFlash();
}
// Load recent messages to flash
// Fills ThreadedMessageApplet::messages with previous messages
// Just enough messages have been stored to cover the display
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
{
// Create a label (will become the filename in flash)
std::string label = "ch" + to_string(channelIndex);
store->loadFromFlash();
}
// Code to run when device is shutting down
// This is in addition to any onDeactivate() code, which will also run
// Todo: implement before a reboot also
void InkHUD::ThreadedMessageApplet::onShutdown()
{
// Save our current set of messages to flash, provided the applet isn't disabled
if (isActive())
saveMessagesToFlash();
}
#endif

Wyświetl plik

@ -0,0 +1,63 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Displays a thread-view of incoming and outgoing message for a specific channel
The channel for this applet is set in the constructor,
when the applet is added to WindowManager in the setupNicheGraphics method.
Several messages are saved to flash at shutdown, to preseve applet between reboots.
This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer.
If the amount of flash usage is unacceptable, we could keep these in RAM only.
Multiple instances of this channel may be used. This must be done at buildtime.
Suggest a max of two channel, to minimize fs usage?
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/MessageStore.h"
#include "modules/TextMessageModule.h"
namespace NicheGraphics::InkHUD
{
class Applet;
class ThreadedMessageApplet : public Applet
{
public:
ThreadedMessageApplet(uint8_t channelIndex);
ThreadedMessageApplet() = delete;
void onRender() override;
void onActivate() override;
void onDeactivate() override;
void onShutdown() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
bool approveNotification(Notification &n) override; // Which notifications to suppress
protected:
// Used to register our text message callback
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *>(this,
&ThreadedMessageApplet::onReceiveTextMessage);
void saveMessagesToFlash();
void loadMessagesFromFlash();
MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown
uint8_t channelIndex = 0;
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,139 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./MessageStore.h"
#include "SafeFile.h"
using namespace NicheGraphics;
// Hard limits on how much message data to write to flash
// Avoid filling the storage if something goes wrong
// Normal usage should be well below this size
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::MessageStore::MessageStore(std::string label)
{
filename = "";
filename += "/NicheGraphics";
filename += "/";
filename += label;
filename += ".msgs";
}
// Write the contents of the MessageStore::messages object to flash
void InkHUD::MessageStore::saveToFlash()
{
assert(!filename.empty());
#ifdef FSCom
// Make the directory, if doesn't already exist
// This is the same directory accessed by NicheGraphics::FlashData
FSCom.mkdir("/NicheGraphics");
// Open or create the file
// No "full atomic": don't save then rename
auto f = SafeFile(filename.c_str(), false);
LOG_INFO("Saving messages in %s", filename.c_str());
// 1st byte: how many messages will be written to store
f.write(messages.size());
// For each message
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
Message &m = messages.at(i);
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
}
bool writeSucceeded = f.close();
if (!writeSucceeded) {
LOG_ERROR("Can't write data!");
}
#else
LOG_ERROR("ERROR: Filesystem not implemented\n");
#endif
}
// Attempt to load the previous contents of the MessageStore:message deque from flash.
// Filename is controlled by the "label" parameter
void InkHUD::MessageStore::loadFromFlash()
{
// Hopefully redundant. Initial intention is to only load / save once per boot.
messages.clear();
#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());
return;
}
// Check that the file *does* actually exist
if (!FSCom.exists(filename.c_str())) {
LOG_INFO("'%s' not found.", filename.c_str());
return;
}
// Open the file
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
if (f.size() == 0) {
LOG_INFO("%s is empty", filename.c_str());
f.close();
return;
}
// If opened, start reading
if (f) {
LOG_INFO("Loading threaded messages '%s'", filename.c_str());
// First byte: how many messages are in the flash store
uint8_t flashMessageCount = 0;
f.readBytes((char *)&flashMessageCount, 1);
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
// For each message
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
Message m;
// Read meta data (fixed width)
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
f.readBytes((char *)&m.sender, sizeof(m.sender));
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
// Read characters until we find a null term
char c;
while (m.text.size() < MAX_MESSAGE_SIZE) {
f.readBytes(&c, 1);
if (c != '\0')
m.text += c;
else
break;
}
// Store in RAM
messages.push_back(m);
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
}
f.close();
} else {
LOG_ERROR("Could not open / read %s", filename.c_str());
}
#else
LOG_ERROR("Filesystem not implemented");
state = LoadFileState::NO_FILESYSTEM;
#endif
return;
}
#endif

Wyświetl plik

@ -0,0 +1,47 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
We hold a few recent messages, for features like the threaded message applet.
This class contains a struct for storing those messages,
and methods for serializing them to flash.
*/
#pragma once
#include "configuration.h"
#include <deque>
#include "mesh/MeshTypes.h"
namespace NicheGraphics::InkHUD
{
class MessageStore
{
public:
// A stored message
struct Message {
uint32_t timestamp; // Epoch seconds
NodeNum sender = 0;
uint8_t channelIndex;
std::string text;
};
MessageStore() = delete;
MessageStore(std::string label); // Label determines filename in flash
void saveToFlash();
void loadFromFlash();
std::deque<Message> messages; // Interact with this object!
private:
std::string filename;
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,59 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Persistence.h"
using namespace NicheGraphics;
// Load settings and latestMessage data
void InkHUD::loadDataFromFlash()
{
// Load the InkHUD settings from flash, and check version number
// We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data
InkHUD::Settings loadedSettings;
bool loadSucceeded = FlashData<Settings>::load(&loadedSettings, "settings");
if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0)
settings = loadedSettings; // Version matched, replace the defaults with the loaded values
else
LOG_WARN("Settings version changed. Using defaults");
// Load previous "latestMessages" data from flash
MessageStore store("latest");
store.loadFromFlash();
// Place into latestMessage struct, for convenient access
// Number of strings loaded determines whether last message was broadcast or dm
if (store.messages.size() == 1) {
latestMessage.dm = store.messages.at(0);
latestMessage.wasBroadcast = false;
} else if (store.messages.size() == 2) {
latestMessage.dm = store.messages.at(0);
latestMessage.broadcast = store.messages.at(1);
latestMessage.wasBroadcast = true;
}
}
// Save settings and latestMessage data
void InkHUD::saveDataToFlash()
{
// Save the InkHUD settings to flash
FlashData<Settings>::save(&settings, "settings");
// Number of strings saved determines whether last message was broadcast or dm
MessageStore store("latest");
store.messages.push_back(latestMessage.dm);
if (latestMessage.wasBroadcast)
store.messages.push_back(latestMessage.broadcast);
store.saveToFlash();
}
// Holds InkHUD settings while running
// Saved back to Flash at shutdown
// Accessed by including persistence.h
InkHUD::Settings InkHUD::settings;
// Holds copies of the most recent broadcast and DM messages while running
// Saved to Flash at shutdown
// Accessed by including persistence.h
InkHUD::LatestMessage InkHUD::latestMessage;
#endif

Wyświetl plik

@ -0,0 +1,123 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
A quick and dirty alternative to storing "device only" settings using the protobufs
Convenient during development.
Potentially a polite option, to avoid polluting the generated code with values for obscure use cases like this.
The save / load mechanism is a shared NicheGraphics feature.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/FlashData.h"
#include "graphics/niche/InkHUD/MessageStore.h"
namespace NicheGraphics::InkHUD
{
constexpr uint8_t MAX_TILES_GLOBAL = 4;
constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16;
// Used to invalidate old settings, if needed
// Version 0 is reserved for testing, and will always load defaults
constexpr uint32_t SETTINGS_VERSION = 2;
struct Settings {
struct Meta {
// Used to invalidate old savefiles, if we make breaking changes
uint32_t version = SETTINGS_VERSION;
} meta;
struct UserTiles {
// How many tiles are shown
uint8_t count = 1;
// Maximum amount of tiles for this display
uint8_t maxCount = 4;
// Which tile is focused (responding to user button input)
uint8_t focused = 0;
// Which applet is displayed on which tile
// Index of array: which tile, as indexed in WindowManager::tiles
// Value of array: which applet, as indexed in WindowManager::activeApplets
uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3};
} userTiles;
struct UserApplets {
// Which applets are running (either displayed, or in the background)
// Index of array: which applet, as indexed in WindowManager::applets
// Initial value is set by the "activeByDefault" parameter of WindowManager::addApplet, in setupNicheGraphics()
bool active[MAX_USERAPPLETS_GLOBAL];
// Which user applets should be automatically shown when they have important data to show
// If none set, foreground applets should remain foreground without manual user input
// If multiple applets request this at once,
// priority is the order which they were passed to WindowManager::addApplets, in setupNicheGraphics()
bool autoshow[MAX_USERAPPLETS_GLOBAL]{false};
} userApplets;
// Features which the use can enable / disable via the on-screen menu
struct OptionalFeatures {
bool notifications = true;
bool batteryIcon = false;
} optionalFeatures;
// Some menu items may not be required, based on device / configuration
// We can enable them only when needed, to de-clutter the menu
struct OptionalMenuItems {
// If aux button is used to swap between tiles, we have to need for this menu item
bool nextTile = true;
// Used if backlight present, and not controlled by AUX button
// If this item is added to menu: backlight is always active when menu is open
// The added menu items then allows the user to "Keep Backlight On", globally.
bool backlight = false;
} optionalMenuItems;
// Allows tips to be run once only
struct Tips {
// Enables the longer "tutorial" shown only on first boot
// Once tutorial has been completed, it is no longer shown
bool firstBoot = true;
// User is advised to shutdown before removing device power
// Once user executes a shutdown (either via menu or client app),
// this tip is no longer shown
bool safeShutdownSeen = false;
} tips;
// Rotation of the display
// Multiples of 90 degrees clockwise
// Most commonly: rotation is 0 when flex connector is oriented below display
uint8_t rotation = 1;
// How long do we consider another node to be "active"?
// Used when applets want to filter for "active nodes" only
uint32_t recentlyActiveSeconds = 2 * 60;
};
// Most recently received text message
// Value is updated by InkHUD::WindowManager, as a courtesty to applets
// Note: different from devicestate.rx_text_message,
// which may contain an *outgoing message* to broadcast
struct LatestMessage {
MessageStore::Message broadcast; // Most recent message received broadcast
MessageStore::Message dm; // Most recent received DM
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
};
extern Settings settings;
extern LatestMessage latestMessage;
void loadDataFromFlash();
void saveDataToFlash();
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,10 @@
[inkhud]
board_level = extra
build_src_filter = +<../variants/$PIOENV> ; Include nicheGraphics.h
build_flags =
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI)
-D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX

Wyświetl plik

@ -0,0 +1,12 @@
# InkHUD
A heads-up-display for E-Ink devices, intended to supplement a connected phone / client. Implemented as a "NicheGraphics" UI.
Supported devices (as of 1st Feb. 2025):
- Heltec Vision Master E213
- Heltec Vision Master E290
- Heltec Wireless Paper V1.1
- LILYGO T-Echo
More to follow

Wyświetl plik

@ -0,0 +1,237 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Tile.h"
#include "concurrency/Periodic.h"
using namespace NicheGraphics;
// Static members of Tile class (for linking)
InkHUD::Tile *InkHUD::Tile::highlightTarget;
bool InkHUD::Tile::highlightShown;
// For dismissing the highlight indicator, after a few seconds
// Highlighting is used to inform user of which tile is now focused
static concurrency::Periodic *taskHighlight;
static int32_t runtaskHighlight()
{
LOG_DEBUG("Dismissing Highlight");
InkHUD::Tile::highlightShown = false;
InkHUD::Tile::highlightTarget = nullptr;
InkHUD::WindowManager::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
return taskHighlight->disable();
}
static void inittaskHighlight()
{
static bool doneOnce = false;
if (!doneOnce) {
taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight);
taskHighlight->disable();
doneOnce = true;
}
}
InkHUD::Tile::Tile()
{
// For convenince
windowManager = InkHUD::WindowManager::getInstance();
inittaskHighlight();
Tile::highlightTarget = nullptr;
Tile::highlightShown = false;
}
// Set the region of the tile automatically, based on the user's chosen layout
// This method places tiles which will host user applets
// The WindowManager multiplexes the applets to these tiles automatically
void InkHUD::Tile::placeUserTile(uint8_t userTileCount, uint8_t tileIndex)
{
uint16_t displayWidth = windowManager->getWidth();
uint16_t displayHeight = windowManager->getHeight();
bool landscape = displayWidth > displayHeight;
// Check for any stray tiles
if (tileIndex > (userTileCount - 1)) {
// Dummy values to prevent rendering
LOG_WARN("Tile index out of bounds");
left = -2;
top = -2;
width = 1;
height = 1;
return;
}
// Todo: special handling for the notification area
// Todo: special handling for 3 tile layout
// Gap between tiles
const uint16_t spacing = 4;
switch (userTileCount) {
// One tile only
case 1:
left = 0;
top = 0;
width = displayWidth;
height = displayHeight;
break;
// Two tiles
case 2:
if (landscape) {
// Side by side
left = ((displayWidth / 2) + (spacing / 2)) * tileIndex;
top = 0;
width = (displayWidth / 2) - (spacing / 2);
height = displayHeight;
} else {
// Above and below
left = 0;
top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex);
width = displayWidth;
height = (displayHeight / 2) - (spacing / 2);
}
break;
// Four tiles
case 4:
width = (displayWidth / 2) - (spacing / 2);
height = (displayHeight / 2) - (spacing / 2);
switch (tileIndex) {
case 0:
left = 0;
top = 0;
break;
case 1:
left = 0 + (width - 1) + spacing;
top = 0;
break;
case 2:
left = 0;
top = 0 + (height - 1) + spacing;
break;
case 3:
left = 0 + (width - 1) + spacing;
top = 0 + (height - 1) + spacing;
break;
}
break;
default:
LOG_ERROR("Unsupported tile layout");
assert(0);
}
assert(width > 0 && height > 0);
this->left = left;
this->top = top;
this->width = width;
this->height = height;
}
// Manually set the region for a tile
// This is only done for tiles which will host certain "System Applets", which have unique position / sizes:
// Things like the NotificationApplet, BatteryIconApplet, etc
void InkHUD::Tile::placeSystemTile(int16_t left, int16_t top, uint16_t width, uint16_t height)
{
assert(width > 0 && height > 0);
this->left = left;
this->top = top;
this->width = width;
this->height = height;
}
// Place an applet onto a tile
// Creates a reciprocal link between applet and tile
// The tile should always know which applet is displayed
// The applet should always know which tile it is display on
// This is enforced with asserts
// Assigning a new applet will break a previous link
// Link may also be broken by assigning a nullptr
void InkHUD::Tile::assignApplet(Applet *a)
{
// Break the link between old applet and this tile
if (assignedApplet)
assignedApplet->setTile(nullptr);
// Store the new applet
assignedApplet = a;
// Create the reciprocal link between the new applet and this tile
if (a)
a->setTile(this);
}
// Get pointer to whichever applet is displayed on this tile
InkHUD::Applet *InkHUD::Tile::getAssignedApplet()
{
return assignedApplet;
}
// Receive drawing output from the assigned applet,
// and translate it from "applet-space" coordinates, to it's true location.
// The final "rotation" step is performed by the windowManager
void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
{
// Move pixels from applet-space to tile-space
x += left;
y += top;
// Crop to tile borders
if (x >= left && x < (left + width) && y >= top && y < (top + height)) {
// Pass to the window manager
windowManager->handleTilePixel(x, y, c);
}
}
// Called by Applet base class, when learning of its dimensions
uint16_t InkHUD::Tile::getWidth()
{
return width;
}
// Called by Applet base class, when learning of its dimensions
uint16_t InkHUD::Tile::getHeight()
{
return height;
}
// Longest edge of the display, in pixels
// Maximum possible size of any tile's width / height
// Used by some components to allocate resources for the "worst possible situtation"
// "Sizing the cathedral for christmas eve"
uint16_t InkHUD::Tile::maxDisplayDimension()
{
WindowManager *wm = WindowManager::getInstance();
return max(wm->getHeight(), wm->getWidth());
}
// Ask for this tile to be highlighted
// Used to indicate which tile is now indicated after focus changes
// Only used for aux button focus changes, not changes via menu
void InkHUD::Tile::requestHighlight()
{
Tile::highlightTarget = this;
Tile::highlightShown = false;
windowManager->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
void InkHUD::Tile::startHighlightTimeout()
{
taskHighlight->setIntervalFromNow(5 * 1000UL);
taskHighlight->enabled = true;
}
// Stop the timer which would automatically dismiss the highlighting
// Called if the tile organically renders before the timer is up
void InkHUD::Tile::cancelHighlightTimeout()
{
if (taskHighlight->enabled)
taskHighlight->disable();
}
#endif

Wyświetl plik

@ -0,0 +1,62 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Class which represents a region of the display area
Applets are assigned to a tile
Tile controls the Applet's dimensions
Tile receives pixel output from the applet, and translates it to the correct display region
*/
#pragma once
#include "configuration.h"
#include "./Applet.h"
#include "./Types.h"
#include "./WindowManager.h"
#include <GFX.h>
namespace NicheGraphics::InkHUD
{
class Applet;
class WindowManager;
class Tile
{
public:
Tile();
void placeUserTile(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
void placeSystemTile(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
uint16_t getWidth(); // Used to set the assigned applet's width before render
uint16_t getHeight(); // Used to set the assigned applet's height before render
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
void assignApplet(Applet *a); // Place an applet onto a tile
Applet *getAssignedApplet(); // Applet which is on a tile
void requestHighlight(); // Ask for this tile to be highlighted
static void startHighlightTimeout(); // Start the auto-dismissal timer
static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed
static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?)
static bool highlightShown; // Is the tile highlighted yet? Controlls highlight vs dismiss
protected:
int16_t left;
int16_t top;
uint16_t width;
uint16_t height;
Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile
WindowManager *windowManager; // Convenient access to the WindowManager singleton
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,62 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Custom data types for InkHUD
Only "general purpose" data-types should be defined here.
If your applet has its own structs or enums, which won't be useful to other applets,
please define them inside (or in the same folder as) your applet.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
// Color, understood by display controller IC (as bit values)
// Also suitable for use as AdafruitGFX colors
enum Color : uint8_t {
BLACK = 0,
WHITE = 1,
};
// Info contained within AppletFont
struct FontDimensions {
uint8_t height;
uint8_t ascenderHeight;
uint8_t descenderHeight;
};
// Which edge Applet::printAt will place on the X parameter
enum HorizontalAlignment : uint8_t {
LEFT,
RIGHT,
CENTER,
};
// Which edge Applet::printAt will place on the Y parameter
enum VerticalAlignment : uint8_t {
TOP,
MIDDLE,
BOTTOM,
};
// An easy-to-understand intepretation of SNR and RSSI
// Calculate with Applet::getSignalStringth
enum SignalStrength : int8_t {
SIGNAL_UNKNOWN = -1,
SIGNAL_NONE,
SIGNAL_BAD,
SIGNAL_FAIR,
SIGNAL_GOOD,
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,151 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./UpdateMediator.h"
#include "./WindowManager.h"
using namespace NicheGraphics;
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL;
static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL;
InkHUD::UpdateMediator::UpdateMediator() : concurrency::OSThread("Mediator")
{
// Timer disabled by default
OSThread::disable();
}
// Ask which type of update operation we should perform
// Even if we explicitly want a FAST or FULL update, we should pass it through this method,
// as it allows UpdateMediator to count the refreshes.
// Internal "maintenance" refreshes are not passed through evaluate, however.
Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::evaluate(Drivers::EInk::UpdateTypes requested)
{
LOG_DEBUG("FULL-update debt:%f", debt);
// For conveninece
typedef Drivers::EInk::UpdateTypes UpdateTypes;
// Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress)
// This maintenance behavior will also halt itself when the timer next fires,
// but that could be an hour away, so we can stop it early here and free up resources
if (OSThread::enabled && debt == 0.0)
endMaintenance();
// Explicitly requested FULL
if (requested == UpdateTypes::FULL) {
LOG_DEBUG("Explicit FULL");
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
return UpdateTypes::FULL;
}
// Explicitly requested FAST
if (requested == UpdateTypes::FAST) {
LOG_DEBUG("Explicit FAST");
// Add to the FULL refresh debt
if (debt < 1.0)
debt += 1.0 / fastPerFull;
else
debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes
// If *significant debt*, begin occasionally refreshing *unprovoked*
// This maintenance behavior is only triggered here, during periods of user interaction
if (debt >= 2.0)
beginMaintenance();
return UpdateTypes::FAST; // Give them what the asked for
}
// Handling UpdateTypes::UNSPECIFIED
// -----------------------------------
// In this case, the UI doesn't care which refresh we use
// Not much debt: suggest FAST
if (debt < 1.0) {
LOG_DEBUG("UNSPECIFIED: using FAST");
debt += 1.0 / fastPerFull;
return UpdateTypes::FAST;
}
// In debt: suggest FULL
else {
LOG_DEBUG("UNSPECIFIED: using FULL");
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
// When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so)
// If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh
// We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically
if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) {
LOG_DEBUG("Initial maintenance skipped");
OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow
}
return UpdateTypes::FULL;
}
}
// Determine which of two update types is more important to honor
// Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness
// Explicit FULL is more important than explicint FAST - prioritize image quality: explicit FULL is rare
Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2)
{
switch (type1) {
case Drivers::EInk::UpdateTypes::UNSPECIFIED:
return type2;
case Drivers::EInk::UpdateTypes::FAST:
return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST;
case Drivers::EInk::UpdateTypes::FULL:
return type1;
}
return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only
}
// We're using the timer to perform "maintenance"
// If signifcant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked.
// This prevents gradual build-up of debt,
// in case we don't have enough UNSPECIFIED refreshes to pay the debt back organically.
// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration
// Subsequent refreshes take place *much* less frequently.
// Hopefully an applet will want to render before this, meaning we can cancel the maintenance.
int32_t InkHUD::UpdateMediator::runOnce()
{
if (debt > 0.0) {
LOG_DEBUG("debt=%f: performing maintenance", debt);
// Ask WindowManager to redraw everything, purely for the refresh
// Todo: optimize? Could update without re-rendering
WindowManager::getInstance()->forceUpdate(EInk::UpdateTypes::FULL);
// Record that we have paid back (some of) the FULL refresh debt
debt = max(debt - 1.0, 0.0);
// Next maintenance refresh scheduled - long wait (an hour?)
return MAINTENANCE_MS;
}
else
return endMaintenance();
}
// Begin periodically refreshing the display, to repay FULL-refresh debt
// We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED
// After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently
// This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable
void InkHUD::UpdateMediator::beginMaintenance()
{
LOG_DEBUG("Maintenance enabled");
OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL);
OSThread::enabled = true;
}
// FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates
int32_t InkHUD::UpdateMediator::endMaintenance()
{
LOG_DEBUG("Maintenance disabled");
return OSThread::disable();
}
#endif

Wyświetl plik

@ -0,0 +1,45 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Responsible for display health
- counts number of FULL vs FAST refresh
- suggests whether to use FAST or FULL, when not explicitly specified
- periodically requests update unprovoked, if required for display health
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
class UpdateMediator : protected concurrency::OSThread
{
public:
UpdateMediator();
// Tell the mediator what we want, get told what we can have
Drivers::EInk::UpdateTypes evaluate(Drivers::EInk::UpdateTypes requested);
// Determine which of two update types is more important to honor
Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2);
uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes
float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull?
private:
int32_t runOnce() override;
void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health
int32_t endMaintenance(); // End unprovoked refreshing: debt paid
float debt = 0.0; // How many full refreshes are due
};
} // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,177 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Singleton class, which manages the broadest InkHUD behaviors
Tasks include:
- containing instances of Tiles and Applets
- co-ordinating display updates
- interacting with other NicheGraphics componets, such as the driver, and input sources
- handling system-wide events (e.g. shutdown)
*/
#pragma once
#include "configuration.h"
#include <vector>
#include "main.h"
#include "modules/TextMessageModule.h"
#include "power.h"
#include "sleep.h"
#include "./Applet.h"
#include "./Applets/System/Notification/Notification.h"
#include "./Persistence.h"
#include "./Tile.h"
#include "./Types.h"
#include "./UpdateMediator.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
class Applet;
class Tile;
class LogoApplet;
class MenuApplet;
class NotificationApplet;
class WindowManager : protected concurrency::OSThread
{
public:
static WindowManager *getInstance(); // Get or create singleton instance
void setDriver(NicheGraphics::Drivers::EInk *driver); // Assign a driver class
void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); // How many FAST updates before FULL
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false,
uint8_t onTile = -1); // Select which applets are used with InkHUD
void begin(); // Start running the window manager (provisioning done)
void createSystemApplets(); // Instantiate and activate system applets
void createSystemTiles(); // Instantiate tiles which host system applets
void assignSystemAppletsToTiles();
void placeSystemTiles(); // Set position and size
void claimFullscreen(Applet *sa); // Assign a system applet to the fullscreen tile
void releaseFullscreen(); // Remove any system applet from the fullscreen tile
void createUserApplets(); // Activate user's selected applets
void createUserTiles(); // Instantiate enough tiles for user's selected layout
void assignUserAppletsToTiles();
void placeUserTiles(); // Automatically place tiles, according to user's layout
void refocusTile(); // Ensure focused tile has a valid applet
int beforeDeepSleep(void *unused); // Prepare for shutdown
int beforeReboot(void *unused); // Prepare for reboot
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused); // Prepare for light sleep
#endif
void handleButtonShort(); // User button: short press
void handleButtonLong(); // User button: long press
void nextApplet(); // Cycle through user applets
void nextTile(); // Focus the next tile (when showing multiple applets at once)
void changeLayout(); // Change tile layout or count
void changeActivatedApplets(); // Change which applets are activated
void toggleBatteryIcon(); // Change whether the battery icon is shown
bool approveNotification(Notification &n); // Ask applets if a notification is worth showing
void handleTilePixel(int16_t x, int16_t y, Color c); // Apply rotation, then store the pixel in framebuffer
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
bool async = true); // Update display, regardless of whether any applets requested this
uint16_t getWidth(); // Display width, relative to rotation
uint16_t getHeight(); // Display height, relative to rotation
uint8_t getAppletCount(); // How many user applets are available, including inactivated
const char *getAppletName(uint8_t index); // By order in userApplets
void lock(Applet *owner); // Allows system applets to prevent other applets triggering a refresh
void unlock(Applet *owner); // Allows normal updating of user applets to continue
bool canRequestUpdate(Applet *a = nullptr); // Checks if allowed to request an update (not locked by other applet)
Applet *whoLocked(); // Find which applet is blocking update requests, if any
protected:
WindowManager(); // Private constructor for singleton
int32_t runOnce() override;
void clearBuffer(); // Empty the framebuffer
void autoshow(); // Show a different applet, to display new info
bool shouldUpdate(); // Check if reason to change display image
Drivers::EInk::UpdateTypes selectUpdateType(); // Determine how the display hardware will perform the image update
void renderUserApplets(); // Draw all currently displayed user applets to the frame buffer
void renderSystemApplets(); // Draw all currently displayed system applets to the frame buffer
void renderPlaceholders(); // Draw diagonal lines on user tiles which have no assigned applet
void render(bool async = true); // Attempt to update the display
void setBufferPixel(int16_t x, int16_t y, Color c); // Place pixels into the frame buffer. All translation / rotation done.
void rotatePixelCoords(int16_t *x, int16_t *y); // Apply the display rotation
void findOrphanApplets(); // Find any applets left-behind when layout changes
// Get notified when the system is shutting down
CallbackObserver<WindowManager, void *> deepSleepObserver =
CallbackObserver<WindowManager, void *>(this, &WindowManager::beforeDeepSleep);
// Get notified when the system is rebooting
CallbackObserver<WindowManager, void *> rebootObserver =
CallbackObserver<WindowManager, void *>(this, &WindowManager::beforeReboot);
// Cache *incoming* text messages, for use by applets
CallbackObserver<WindowManager, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<WindowManager, const meshtastic_MeshPacket *>(this, &WindowManager::onReceiveTextMessage);
#ifdef ARCH_ESP32
// Get notified when the system is entering light sleep
CallbackObserver<WindowManager, void *> lightSleepObserver =
CallbackObserver<WindowManager, void *>(this, &WindowManager::beforeLightSleep);
#endif
NicheGraphics::Drivers::EInk *driver = nullptr;
uint8_t *imageBuffer; // Fed into driver
uint16_t imageBufferHeight;
uint16_t imageBufferWidth;
uint32_t imageBufferSize; // Bytes
// Encapsulates decision making about E-Ink update types
// Responsible for display health
UpdateMediator mediator;
// User Applets
std::vector<Applet *> userApplets;
std::vector<Tile *> userTiles;
// System Applets
std::vector<Applet *> systemApplets;
Tile *fullscreenTile = nullptr;
Tile *notificationTile = nullptr;
Tile *batteryIconTile = nullptr;
LogoApplet *logoApplet;
Applet *pairingApplet;
Applet *tipsApplet;
NotificationApplet *notificationApplet;
Applet *batteryIconApplet;
MenuApplet *menuApplet;
Applet *placeholderApplet;
// requestUpdate
bool requestingUpdate = false; // WindowManager::render run pending
// forceUpdate
bool forcingUpdate = false; // WindowManager::render run pending, guaranteed no skip of update
Drivers::EInk::UpdateTypes forcedUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // guaranteed update using this type
Applet *lockOwner = nullptr; // Which system applet (if any) is preventing other applets from requesting update
};
}; // namespace NicheGraphics::InkHUD
#endif

Wyświetl plik

@ -0,0 +1,7 @@
# NiceGraphics - Inputs
General purpose input sources, for use with NicheGraphics UIs.
By remaining independent, we can have tailored input sources with further complicating the code in ButtonThread and the canned messages module.
Depending on its role, a NicheGraphics UI may or may not want to make use of the existing input broker.

Wyświetl plik

@ -0,0 +1,272 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./TwoButton.h"
#include "PowerFSM.h"
#include "sleep.h"
using namespace NicheGraphics::Inputs;
TwoButton::TwoButton() : concurrency::OSThread("TwoButton")
{
// Don't start polling buttons for release immediately
// Assume they are in a "released" state at boot
OSThread::disable();
#ifdef ARCH_ESP32
// Register callbacks for before and after lightsleep
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
#endif
}
// Get access to (or create) the singleton instance of this class
// Accessible inside the ISRs, even though we maybe shouldn't
TwoButton *TwoButton::getInstance()
{
// Instantiate the class the first time this method is called
static TwoButton *const singletonInstance = new TwoButton;
return singletonInstance;
}
// Begin receiving button input
// We probably need to do this after sleep, as well as at boot
void TwoButton::start()
{
if (buttons[0].pin != 0xFF)
attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
if (buttons[1].pin != 0xFF)
attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
}
// Stop receiving button input, and run custom sleep code
// Called before device sleeps. This might be power-off, or just ESP32 light sleep
// Some devices will want to attach interrupts here, for the user button to wake from sleep
void TwoButton::stop()
{
if (buttons[0].pin != 0xFF)
detachInterrupt(buttons[0].pin);
if (buttons[1].pin != 0xFF)
detachInterrupt(buttons[1].pin);
}
// Configures the wiring and logic of either button
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
{
assert(whichButton < 2);
buttons[whichButton].pin = pin;
buttons[whichButton].activeLogic = LOW;
buttons[whichButton].mode = internalPullup ? INPUT_PULLUP : INPUT; // fix me
pinMode(buttons[whichButton].pin, buttons[whichButton].mode);
}
void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
{
assert(whichButton < 2);
buttons[whichButton].debounceLength = debounceMs;
buttons[whichButton].longpressLength = longpressMs;
}
// Set what should happen when a button becomes pressed
// Use this to implement a "while held" behavior
void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown)
{
assert(whichButton < 2);
buttons[whichButton].onDown = onDown;
}
// Set what should happen when a button becomes unpressed
// Use this to implement a "While held" behavior
void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp)
{
assert(whichButton < 2);
buttons[whichButton].onUp = onUp;
}
// Set what should happen when a "short press" event has occurred
void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress)
{
assert(whichButton < 2);
buttons[whichButton].onShortPress = onShortPress;
}
// Set what should happen when a "long press" event has fired
// Note: this will occur while the button is still held
void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress)
{
assert(whichButton < 2);
buttons[whichButton].onLongPress = onLongPress;
}
// Handle the start of a press to the primary button
// Wakes our button thread
void TwoButton::isrPrimary()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButton *b = TwoButton::getInstance();
if (b->buttons[0].state == State::REST) {
b->buttons[0].state = State::IRQ;
b->buttons[0].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
// Handle the start of a press to the secondary button
// Wakes our button thread
void TwoButton::isrSecondary()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButton *b = TwoButton::getInstance();
if (b->buttons[1].state == State::REST) {
b->buttons[1].state = State::IRQ;
b->buttons[1].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
// Concise method to start our button thread
// Follows an ISR, listening for button release
void TwoButton::startThread()
{
if (!OSThread::enabled) {
OSThread::setInterval(50);
OSThread::enabled = true;
}
}
// Concise method to stop our button thread
// Called when we no longer need to poll for button release
void TwoButton::stopThread()
{
if (OSThread::enabled) {
OSThread::disable();
}
// Reset both buttons manually
// Just in case an IRQ fires during the process of resetting the system
// Can occur with super rapid presses?
buttons[0].state = REST;
buttons[1].state = REST;
}
// Our button thread
// Started by an IRQ, on either button
// Polls for button releases
// Stops when both buttons released
int32_t TwoButton::runOnce()
{
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
// Allow either button to request that our thread should continue polling
bool awaitingRelease = false;
// Check both primary and secondary buttons
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
switch (buttons[i].state) {
// No action: button has not been pressed
case REST:
break;
// New press detected by interrupt
case IRQ:
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
buttons[i].onDown(); // Inform that press has begun (possible hold behavior)
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
awaitingRelease = true; // Mark that polling-for-release should continue
break;
// An existing press continues
// Not held long enough to register as longpress
case POLLING_UNFIRED: {
uint32_t length = millis() - buttons[i].irqAtMillis;
// If button released since last thread tick,
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
buttons[i].onUp(); // Inform that press has ended (possible release of a hold)
buttons[i].state = State::REST; // Mark that the button has reset
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength)
buttons[i].onShortPress();
}
// If button not yet released
else {
awaitingRelease = true; // Mark that polling-for-release should continue
if (length >= buttons[i].longpressLength) {
// Raise a long press event, once
// Then continue waiting for release, to rearm
buttons[i].state = State::POLLING_FIRED;
buttons[i].onLongPress();
}
}
break;
}
// Button still held, but duration long enough that longpress event already fired
// Just waiting for release
case POLLING_FIRED:
// Release detected
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
buttons[i].state = State::REST;
buttons[i].onUp(); // Possible release of hold (in this case: *after* longpress has fired)
}
// Not yet released, keep polling
else
awaitingRelease = true;
break;
}
}
// If both buttons are now released
// we don't need to waste cpu resources polling
// IRQ will restart this thread when we next need it
if (!awaitingRelease)
stopThread();
// Run this method again, or don't..
// Use whatever behavior was previously set by stopThread() or startThread()
return OSThread::interval;
}
#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 TwoButton::beforeLightSleep(void *unused)
{
stop();
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 TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause)
{
start();
// Manually trigger the button-down ISR
// - during light sleep, our ISR is disabled
// - if light sleep ends by button press, pretend our own ISR caught it
if (cause == ESP_SLEEP_WAKEUP_GPIO)
isrPrimary();
return 0; // Indicates success
}
#endif
#endif

Wyświetl plik

@ -0,0 +1,103 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
/*
Re-usable NicheGraphics input source
Short and Long press for up to two buttons
Interrupt driven
*/
#pragma once
#include "configuration.h"
#include "assert.h"
#include "functional"
#ifdef ARCH_ESP32
#include "esp_sleep.h" // For light-sleep handling
#endif
#include "Observer.h"
namespace NicheGraphics::Inputs
{
class TwoButton : protected concurrency::OSThread
{
public:
typedef std::function<void()> Callback;
static TwoButton *getInstance(); // Create or get the singleton instance
void start(); // Start handling button input
void stop(); // Stop handling button input (disconnect ISRs for sleep)
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPulldown = false);
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
#endif
private:
// Internal state of a specific button
enum State {
REST, // Up, no activity
IRQ, // Down detected, not yet handled
POLLING_UNFIRED, // Down handled, polling for release
POLLING_FIRED, // Longpress fired, button still held
};
// Contains info about a specific button
// (Array of this struct below)
class Button
{
public:
// Per-button config
uint8_t pin = 0xFF; // 0xFF: unset
bool activeLogic = LOW; // Active LOW by default. Todo: remove, unused
uint8_t mode = INPUT; // Whether to use internal pull up / pull down resistors
uint32_t debounceLength = 50; // Minimum length for shortpress, in ms
uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms
volatile State state = State::REST; // Internal state
volatile uint32_t irqAtMillis; // millis() when button went down
// Per-button event callbacks
static void noop(){};
std::function<void()> onDown = noop;
std::function<void()> onUp = noop;
std::function<void()> onShortPress = noop;
std::function<void()> onLongPress = noop;
};
#ifdef ARCH_ESP32
// Get notified when lightsleep begins and ends
CallbackObserver<TwoButton, void *> lsObserver = CallbackObserver<TwoButton, void *>(this, &TwoButton::beforeLightSleep);
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t> lsEndObserver =
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t>(this, &TwoButton::afterLightSleep);
#endif
int32_t runOnce() override; // Timer method. Polls for button release
void startThread(); // Start polling for release
void stopThread(); // Stop polling for release
static void isrPrimary(); // Detect start of press
static void isrSecondary(); // Detect start of press (optional aux button)
TwoButton(); // Constructor made private: force use of Button::instance()
// Info about both buttons
Button buttons[2];
};
}; // namespace NicheGraphics::Inputs
#endif

Wyświetl plik

@ -0,0 +1,15 @@
# NicheGraphics
A pattern / collection of resources for creating custom UIs, to target small groups of devices which have specific design requirements.
For an example, see the `heltec-vision-master-e290-inkhud` platformio env.
- platformio.ini
- suppress default Meshtastic components (Screen, ButtonThread, etc)
- define `MESHTASTIC_INCLUDE_NICHE_GRAPHICS`
- (possibly) Edit `build_src_filter` to include our new nicheGraphics.h file
- nicheGraphics.h
- `#include` all necessary components
- perform all setup and config inside a `setupNicheGraphics()` method

Wyświetl plik

@ -0,0 +1,126 @@
#if HAS_TFT
#include "SPILock.h"
#include "sleep.h"
#include "api/PacketAPI.h"
#include "comms/PacketClient.h"
#include "comms/PacketServer.h"
#include "graphics/DeviceScreen.h"
#include "graphics/driver/DisplayDriverConfig.h"
#ifdef ARCH_PORTDUINO
#include "PortduinoGlue.h"
#endif
DeviceScreen *deviceScreen = nullptr;
#ifdef ARCH_ESP32
// Get notified when the system is entering light sleep
CallbackObserver<DeviceScreen, void *> tftSleepObserver =
CallbackObserver<DeviceScreen, void *>(deviceScreen, &DeviceScreen::prepareSleep);
CallbackObserver<DeviceScreen, esp_sleep_wakeup_cause_t> endSleepObserver =
CallbackObserver<DeviceScreen, esp_sleep_wakeup_cause_t>(deviceScreen, &DeviceScreen::wakeUp);
#endif
void tft_task_handler(void *param = nullptr)
{
while (true) {
if (deviceScreen) {
spiLock->lock();
deviceScreen->task_handler();
spiLock->unlock();
deviceScreen->sleep();
}
}
}
void tftSetup(void)
{
#ifndef ARCH_PORTDUINO
deviceScreen = &DeviceScreen::create();
PacketAPI::create(PacketServer::init());
deviceScreen->init(new PacketClient);
#else
if (settingsMap[displayPanel] != no_screen) {
DisplayDriverConfig displayConfig;
static char *panels[] = {"NOSCREEN", "X11", "ST7789", "ST7735", "ST7735S", "ST7796",
"ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"};
static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"};
#ifdef USE_X11
if (settingsMap[displayPanel] == x11) {
if (settingsMap[displayWidth] && settingsMap[displayHeight])
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth],
(uint16_t)settingsMap[displayHeight]);
else
displayConfig.device(DisplayDriverConfig::device_t::X11);
} else
#endif
{
displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT)
.panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]],
.panel_width = (uint16_t)settingsMap[displayWidth],
.panel_height = (uint16_t)settingsMap[displayHeight],
.rotation = (bool)settingsMap[displayRotate],
.pin_cs = (int16_t)settingsMap[displayCS],
.pin_rst = (int16_t)settingsMap[displayReset],
.offset_x = (uint16_t)settingsMap[displayOffsetX],
.offset_y = (uint16_t)settingsMap[displayOffsetY],
.offset_rotation = (uint8_t)settingsMap[displayOffsetRotate],
.invert = settingsMap[displayInvert] ? true : false,
.rgb_order = (bool)settingsMap[displayRGBOrder],
.dlen_16bit = settingsMap[displayPanel] == ili9486 ||
settingsMap[displayPanel] == ili9488})
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency],
.freq_read = 16000000,
.spi{.pin_dc = (int8_t)settingsMap[displayDC],
.use_lock = true,
.spi_host = (uint16_t)settingsMap[displayspidev]}})
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice],
.pointerDevice = settingsStrings[pointerDevice]})
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight],
.pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel],
.invert = (bool)settingsMap[displayBacklightInvert]});
if (settingsMap[touchscreenI2CAddr] == -1) {
displayConfig.touch(
DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]],
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
.spi{
.spi_host = (int8_t)settingsMap[touchscreenspidev],
},
.pin_cs = (int16_t)settingsMap[touchscreenCS]});
} else {
displayConfig.touch(DisplayDriverConfig::touch_config_t{
.type = touch[settingsMap[touchscreenModule]],
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
.x_min = 0,
.x_max =
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) -
1),
.y_min = 0,
.y_max =
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) -
1),
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
.i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}});
}
}
deviceScreen = &DeviceScreen::create(&displayConfig);
PacketAPI::create(PacketServer::init());
deviceScreen->init(new PacketClient);
} else {
LOG_INFO("Running without TFT display!");
}
#endif
#ifdef ARCH_ESP32
tftSleepObserver.observe(&notifyLightSleep);
endSleepObserver.observe(&notifyLightSleepEnd);
xTaskCreatePinnedToCore(tft_task_handler, "tft", 8192, NULL, 1, NULL, 0);
#endif
}
#endif

Wyświetl plik

@ -115,6 +115,24 @@ AccelerometerThread *accelerometerThread = nullptr;
AudioThread *audioThread = nullptr;
#endif
#if HAS_TFT
extern void tftSetup(void);
#endif
#ifdef HAS_UDP_MULTICAST
#include "mesh/udp/UdpMulticastThread.h"
UdpMulticastThread *udpThread = nullptr;
#endif
#if defined(TCXO_OPTIONAL)
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
#endif
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
void setupNicheGraphics();
#include "nicheGraphics.h"
#endif
using namespace concurrency;
volatile static const char slipstreamTZString[] = {USERPREFS_TZ_STRING};
@ -131,6 +149,9 @@ meshtastic::GPSStatus *gpsStatus = new meshtastic::GPSStatus();
// Global Node status
meshtastic::NodeStatus *nodeStatus = new meshtastic::NodeStatus();
// Global Bluetooth status
meshtastic::BluetoothStatus *bluetoothStatus = new meshtastic::BluetoothStatus();
// Scan for I2C Devices
/// The I2C address of our display (if found)
@ -249,6 +270,15 @@ void setup()
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
pinMode(KB_POWERON, OUTPUT);
digitalWrite(KB_POWERON, HIGH);
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
// e.g. when detecting the SD card
pinMode(LORA_CS, OUTPUT);
digitalWrite(LORA_CS, HIGH);
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
delay(100);
#endif
@ -426,6 +456,10 @@ void setup()
digitalWrite(AQ_SET_PIN, HIGH);
#endif
#if HAS_TFT
tftSetup();
#endif
// Currently only the tbeam has a PMU
// PMU initialization needs to be placed before i2c scanning
power = new Power();
@ -644,9 +678,9 @@ void setup()
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
nodeDB = new NodeDB;
// If we're taking on the repeater role, use flood router and turn off 3V3_S rail because peripherals are not needed
// If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
router = new FloodingRouter();
router = new NextHopRouter();
#ifdef PIN_3V3_EN
digitalWrite(PIN_3V3_EN, LOW);
#endif
@ -731,8 +765,9 @@ void setup()
#endif
// Initialize the screen first so we can show the logo while we start up everything else.
#if HAS_SCREEN
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
#endif
// setup TZ prior to time actions.
#if !MESHTASTIC_EXCLUDE_TZ
LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string
@ -781,12 +816,22 @@ void setup()
LOG_DEBUG("Start audio thread");
audioThread = new AudioThread();
#endif
#ifdef HAS_UDP_MULTICAST
LOG_DEBUG("Start multicast thread");
udpThread = new UdpMulticastThread();
#endif
service = new MeshService();
service->init();
// Now that the mesh service is created, create any modules
setupModules();
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
// After modules are setup, so we can observe modules
setupNicheGraphics();
#endif
#ifdef LED_PIN
// Turn LED off after boot, if heartbeat by config
if (config.device.led_heartbeat_disabled)
@ -1124,7 +1169,15 @@ void setup()
// This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values
PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS
powerFSMthread = new PowerFSMThread();
#if !HAS_TFT
setCPUFast(false); // 80MHz is fine for our slow peripherals
#endif
#ifdef ARDUINO_ARCH_ESP32
LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap());
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
#endif
}
#endif
uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes)
@ -1221,4 +1274,5 @@ void loop()
mainDelay.delay(delayMsec);
}
}
#endif
#endif

Wyświetl plik

@ -1,5 +1,6 @@
#pragma once
#include "BluetoothStatus.h"
#include "GPSStatus.h"
#include "NodeStatus.h"
#include "PowerStatus.h"
@ -49,6 +50,11 @@ extern Adafruit_DRV2605 drv;
extern AudioThread *audioThread;
#endif
#ifdef HAS_UDP_MULTICAST
#include "mesh/udp/UdpMulticastThread.h"
extern UdpMulticastThread *udpThread;
#endif
// Global Screen singleton.
extern graphics::Screen *screen;

Wyświetl plik

@ -93,6 +93,35 @@ void Channels::initDefaultLoraConfig()
#endif
}
bool Channels::ensureLicensedOperation()
{
if (!owner.is_licensed) {
return false;
}
bool hasEncryptionOrAdmin = false;
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
auto channel = channels.getByIndex(i);
if (!channel.has_settings) {
continue;
}
auto &channelSettings = channel.settings;
if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) {
channel.role = meshtastic_Channel_Role_DISABLED;
channelSettings.psk.bytes[0] = 0;
channelSettings.psk.size = 0;
hasEncryptionOrAdmin = true;
channels.setChannel(channel);
} else if (channelSettings.psk.size > 0) {
channelSettings.psk.bytes[0] = 0;
channelSettings.psk.size = 0;
hasEncryptionOrAdmin = true;
channels.setChannel(channel);
}
}
return hasEncryptionOrAdmin;
}
/**
* Write a default channel to the specified channel index
*/

Wyświetl plik

@ -92,6 +92,8 @@ class Channels
// Returns true if any of our channels have enabled MQTT uplink or downlink
bool anyMqttEnabled();
bool ensureLicensedOperation();
private:
/** Given a channel index, change to use the crypto key specified by that index
*

Wyświetl plik

@ -13,7 +13,8 @@ FloodingRouter::FloodingRouter() {}
ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p)
{
// Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see)
wasSeenRecently(p); // FIXME, move this to a sniffSent method
p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us
wasSeenRecently(p); // FIXME, move this to a sniffSent method
return Router::send(p);
}
@ -23,26 +24,17 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
if (wasSeenRecently(p)) { // Note: this will also add a recent packet record
printPacket("Ignore dupe incoming msg", p);
rxDupe++;
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater!
if (Router::cancelSending(p->from, p->id))
txRelayCanceled++;
}
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) {
iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
}
/* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, e.g., when
the ACK got lost, we will handle the packet again to make sure it gets an ACK to its packet. */
the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */
bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit;
if (isRepeated) {
LOG_DEBUG("Repeated reliable tx");
if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
// FIXME - channel index should be used, but the packet is still encrypted here
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, 0, 0);
}
// Check if it's still in the Tx queue, if not, we have to relay it again
if (!findInTxQueue(p->from, p->id))
perhapsRebroadcast(p);
} else {
perhapsCancelDupe(p);
}
return true;
@ -51,13 +43,27 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
return Router::shouldFilterReceived(p);
}
void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
{
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater!
if (Router::cancelSending(p->from, p->id))
txRelayCanceled++;
}
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) {
iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
}
}
bool FloodingRouter::isRebroadcaster()
{
return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE &&
config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE;
}
bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
void FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
{
if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) {
if (p->id != 0) {
@ -72,13 +78,12 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
tosend->hop_limit = 2;
}
#endif
tosend->next_hop = NO_NEXT_HOP_PREFERENCE; // this should already be the case, but just in case
LOG_INFO("Rebroadcast received floodmsg");
// Note: we are careful to resend using the original senders node id
// We are careful not to call our hooked version of send() - because we don't want to check this again
Router::send(tosend);
return true;
} else {
LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
}
@ -86,13 +91,12 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
LOG_DEBUG("Ignore 0 id broadcast");
}
}
return false;
}
void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c)
{
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0);
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) &&
(p->decoded.request_id != 0 || p->decoded.reply_id != 0);
if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) {
// do not flood direct message that is ACKed or replied to
LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast");

Wyświetl plik

@ -1,6 +1,5 @@
#pragma once
#include "PacketHistory.h"
#include "Router.h"
/**
@ -26,14 +25,11 @@
Any entries in recentBroadcasts that are older than X seconds (longer than the
max time a flood can take) will be discarded.
*/
class FloodingRouter : public Router, protected PacketHistory
class FloodingRouter : public Router
{
private:
bool isRebroadcaster();
/** Check if we should rebroadcast this packet, and do so if needed
* @return true if rebroadcasted */
bool perhapsRebroadcast(const meshtastic_MeshPacket *p);
/* Check if we should rebroadcast this packet, and do so if needed */
void perhapsRebroadcast(const meshtastic_MeshPacket *p);
public:
/**
@ -62,4 +58,10 @@ class FloodingRouter : public Router, protected PacketHistory
* Look for broadcasts we need to rebroadcast
*/
virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override;
/* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */
void perhapsCancelDupe(const meshtastic_MeshPacket *p);
// Return true if we are a rebroadcaster
bool isRebroadcaster();
};

Wyświetl plik

@ -262,10 +262,17 @@ template <typename T> void LR11x0Interface<T>::startReceive()
template <typename T> bool LR11x0Interface<T>::isChannelActive()
{
// check if we can detect a LoRa preamble on the current channel
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
.detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
.detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
.exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
.timeout = 0,
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
int16_t result;
setStandby();
result = lora.scanChannel();
result = lora.scanChannel(cfg);
if (result == RADIOLIB_LORA_DETECTED)
return true;

Wyświetl plik

@ -117,6 +117,19 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t
return NULL;
}
/* Attempt to find a packet from this queue. Return true if it was found. */
bool MeshPacketQueue::find(NodeNum from, PacketId id)
{
for (auto it = queue.begin(); it != queue.end(); it++) {
auto p = (*it);
if (getFrom(p) == from && p->id == id) {
return true;
}
}
return false;
}
/**
* Attempt to find a lower-priority packet in the queue and replace it with the provided one.
* @return True if the replacement succeeded, false otherwise

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