Files
firmware/src/graphics/niche/InkHUD/UpdateMediator.cpp
T
todd-herbert 9fd100dd92 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
2025-02-11 16:01:17 -06:00

151 lines
5.9 KiB
C++

#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