firmware/src/graphics/niche/InkHUD/DisplayHealth.cpp
todd-herbert e6a98b1d6b
InkHUD refactoring (#6216)
* chore: todo.txt
* chore: comments
* fix: no fast refresh on VME290
Reverts a line of code which was accidentally committed
* refactor: god class
Divide the behavior from the old WindowManager class into several subclasses which each have a clear role.
* refactor: cppcheck medium warnings
Enough to pass github CI for now
* refactor: updateType selection
* refactor: don't use a setter for the shared AppletFonts
* fix: update prioritization
forceUpdate calls weren't being prioritized
* refactor: remove unhelpful logging
getTimeString is used for parsing our own time, but also the timestamps of messages. The "one time only" log printing will likely fire in unhelpful situations.
* fix: " "
* refactor: get rid of types.h file for enums
* Keep that sneaky todo file out of commits
2025-03-06 11:25:41 +01:00

176 lines
7.1 KiB
C++

#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./DisplayHealth.h"
#include "DisplayHealth.h"
using namespace NicheGraphics;
// Timing for "maintenance"
// Paying off full-refresh debt with unprovoked updates, if the display is not very active
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL;
static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL;
InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator")
{
// Timer disabled by default
OSThread::disable();
}
// Request which update type we would prefer, when the display image next changes
// DisplayHealth class will consider our suggestion, and weigh it against other requests
void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type)
{
// Update our "working decision", to decide if this request is important enough to change our plan
if (!forced)
workingDecision = prioritize(workingDecision, type);
}
// Demand that a specific update type be used, when the display image next changes
// Note: multiple DisplayHealth::force calls should not be made,
// but if they are, the importance of the type will be weighed the same as if both calls were to DisplayHealth::request
void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type)
{
if (!forced)
workingDecision = type;
else
workingDecision = prioritize(workingDecision, type);
forced = true;
}
// Find out which update type the DisplayHealth has chosen for us
// Calling this method consumes the result, and resets for the next update
Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType()
{
LOG_DEBUG("FULL-update debt:%f", debt);
// For convenience
typedef Drivers::EInk::UpdateTypes UpdateTypes;
// Grab our final decision for the update type, so we can reset now, for the next update
// We do this at top of the method, so we can return early
UpdateTypes finalDecision = workingDecision;
workingDecision = UpdateTypes::UNSPECIFIED;
forced = false;
// Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress)
// This maintenance behavior will also have opportunity to 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 (finalDecision == 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 (finalDecision == 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, by periods of user interaction
// Debt would otherwise not be able to climb above 1.0
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)
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 explicit FAST - prioritize image quality: explicit FULL is rare
// Used when multiple applets have all requested update simultaneously, each with their own preferred UpdateType
Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::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 significant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked.
// This prevents gradual build-up of debt,
// in case we aren't doing 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::DisplayHealth::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
InkHUD::getInstance()->forceUpdate(Drivers::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::DisplayHealth::beginMaintenance()
{
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::DisplayHealth::endMaintenance()
{
return OSThread::disable();
}
#endif