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
This commit is contained in:
todd-herbert
2025-03-06 23:25:41 +13:00
committed by GitHub
parent b2ef92a328
commit e6a98b1d6b
70 changed files with 2381 additions and 1955 deletions

View File

@@ -2,6 +2,8 @@
#include "./Applet.h"
#include "main.h"
#include "RTC.h"
using namespace NicheGraphics;
@@ -16,10 +18,15 @@ InkHUD::Applet::Applet() : GFX(0, 0)
// The width and height will change dynamically, depending on Applet tiling
// If you're getting a "divide by zero error", consider it an assert:
// WindowManager should be the only one controlling the rendering
inkhud = InkHUD::getInstance();
settings = &inkhud->persistence->settings;
latestMessage = &inkhud->persistence->latestMessage;
}
// The raw pixel output generated by AdafruitGFX drawing
// Hand off to the applet's tile, which will in-turn pass to the window manager
// Draw a single pixel
// The raw pixel output generated by AdafruitGFX drawing all passes through here
// Hand off to the applet's tile, which will in-turn pass to the renderer
void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
{
// Only render pixels if they fall within user's cropped region
@@ -27,9 +34,10 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
assignedTile->handleAppletPixel(x, y, (Color)color);
}
// Sets which tile the applet renders for
// Link our applet to a tile
// This can only be called by Tile::assignApplet
// The tile determines the applets dimensions
// Pixel output is passed to tile during render()
// 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"
@@ -39,25 +47,32 @@ void InkHUD::Applet::setTile(Tile *t)
assignedTile = t;
}
// Which tile will the applet render() to?
// The tile to which our applet is assigned
InkHUD::Tile *InkHUD::Applet::getTile()
{
return assignedTile;
}
// Draw the applet
void InkHUD::Applet::render()
{
assert(assignedTile); // Ensure that we have a tile
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
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.
// WindowManager::update has now consumed the info about our update request
// Clear everything for future requests
wantRender = false; // Flag set by requestUpdate
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
updateDimensions();
resetDrawingSpace();
onRender(); // Derived applet's drawing takes place here
// Handle "Tile Highlighting"
// Some devices may use an auxiliary button to switch between tiles
// When this happens, we temporarily highlight the newly focused tile with a border
// If our tile is (or was) highlighted, to indicate a change in focus
if (Tile::highlightTarget == assignedTile) {
// Draw the highlight
@@ -77,7 +92,8 @@ void InkHUD::Applet::render()
}
// Does the applet want to render now?
// Checks whether the applet called requestUpdate() recently, in response to an event
// Checks whether the applet called requestUpdate recently, in response to an event
// Used by WindowManager::update
bool InkHUD::Applet::wantsToRender()
{
return wantRender;
@@ -85,18 +101,21 @@ bool InkHUD::Applet::wantsToRender()
// Does the applet want to be moved to foreground before next render, to show new data?
// User specifies whether an applet has permission for this, using the on-screen menu
// Used by WindowManager::update
bool InkHUD::Applet::wantsToAutoshow()
{
return wantAutoshow;
}
// Which technique would this applet prefer that the display use to change the image?
// Used by WindowManager::update
Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
{
return wantUpdateType;
}
// Get size of the applet's drawing space from its tile
// Performed immediately before derived applet's drawing code runs
void InkHUD::Applet::updateDimensions()
{
assert(assignedTile);
@@ -113,19 +132,20 @@ void InkHUD::Applet::resetDrawingSpace()
setTextColor(BLACK); // Reset text params
setCursor(0, 0);
setTextWrap(false);
setFont(AppletFont()); // Restore the default AdafruitGFX font
setFont(fontSmall);
}
// Tell the window manager that we want to render now
// Tell InkHUD::Renderer that we want to render now
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
// When an applet decides it has heard something important, and wants to redraw, it calls this method
// Once the 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)
// Once the renderer has given other applets a chance to process whatever event we just detected,
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
{
wantRender = true;
wantUpdateType = type;
WindowManager::getInstance()->requestUpdate();
inkhud->requestUpdate();
}
// Ask window manager to move this applet to foreground at start of next render
@@ -138,7 +158,7 @@ void InkHUD::Applet::requestAutoshow()
// 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
// They may also be unexpectedly renderer at any time by other InkHUD components
// Applets can be activated at run-time through the on-screen menu
void InkHUD::Applet::activate()
{
@@ -146,7 +166,7 @@ void InkHUD::Applet::activate()
active = true;
}
// Called when an Applet stop running
// Called when an Applet stops running
// Inactive applets are considered "disabled"
// They should not listen for events, process data
// They will not be rendered
@@ -173,7 +193,7 @@ bool InkHUD::Applet::isActive()
// 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
// The Renderer will also now honor requestUpdate() calls from this applet
void InkHUD::Applet::bringToForeground()
{
if (!foreground) {
@@ -186,7 +206,7 @@ void InkHUD::Applet::bringToForeground()
// Stop showing the Applet
// Calls to requestUpdate() will no longer be honored
// When one applet moves to background, another should move to foreground
// When one applet moves to background, another should move to foreground (exception: some system applets)
void InkHUD::Applet::sendToBackground()
{
if (foreground) {
@@ -196,6 +216,10 @@ void InkHUD::Applet::sendToBackground()
}
// Is the applet currently displayed on a tile
// Note: in some uncommon situations, an applet may be "foreground", and still not visible.
// This can occur when a system applet is covering the screen (e.g. during BLE pairing)
// This is not our applets responsibility to handle,
// as in those situations, the system applet will have "locked" rendering
bool InkHUD::Applet::isForeground()
{
return foreground;
@@ -248,7 +272,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalA
// Custom font
// - set with AppletFont::addSubstitution
// - find certain UTF8 chars
// - replace with glpyh from custom font (or suitable ASCII addSubstitution?)
// - replace with glyph from custom font (or suitable ASCII addSubstitution?)
getFont().applySubstitutions(&text);
// We do still have to run getTextBounds to find the width
@@ -271,8 +295,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalA
break;
}
// We're using a fixed line height (getFontDimensions), rather than sizing to text (getTextBounds)
// Note: the FontDimensions values for this are unsigned
// We're using a fixed line height, rather than sizing to text (getTextBounds)
switch (va) {
case TOP:
@@ -291,7 +314,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalA
}
// 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
// This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data
void InkHUD::Applet::setFont(AppletFont f)
{
GFX::setFont(f.gfxFont);
@@ -299,20 +322,12 @@ void InkHUD::Applet::setFont(AppletFont 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
// This is AppletFont type, which is a wrapper for AdafruitGFX 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)
@@ -327,7 +342,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text)
}
// Gets rendered width of a string
// Wrappe for getTextBounds
// Wrapper for getTextBounds
uint16_t InkHUD::Applet::getTextWidth(std::string text)
{
getFont().applySubstitutions(&text);
@@ -338,7 +353,7 @@ uint16_t InkHUD::Applet::getTextWidth(std::string text)
// 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)
InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi)
{
uint8_t score = 0;
@@ -376,12 +391,14 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
return std::string(nodeIdHex);
}
// Print text, with word wrapping
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
{
// Custom font glyphs
// - set with AppletFont::addSubstitution
// - find certain UTF8 chars
// - replace with glpyh from custom font (or suitable ASCII addSubstitution?)
// - replace with glyph from custom font (or suitable ASCII addSubstitution?)
getFont().applySubstitutions(&text);
// Place the AdafruitGFX cursor to suit our "top" coord
@@ -528,7 +545,7 @@ 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
constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT
#endif
uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true);
@@ -538,23 +555,17 @@ std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds)
// 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");
if (epochNow <= validAfterEpoch)
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");
if (daysAgo < -2)
return "";
}
// Times are probably invalid: more than 6 months ago
if (daysAgo > 6 * 30) {
LOG_DEBUG("RTC val > 6 months old");
if (daysAgo > 6 * 30)
return "";
}
if (daysAgo > 1)
return to_string(daysAgo) + " days ago";
@@ -602,7 +613,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount()
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Check if heard recently, and not our own node
if (sinceLastSeen(node) < settings.recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
count++;
}
@@ -619,7 +630,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters)
// Resulting string
std::string localized;
// Imeperial
// Imperial
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
uint32_t feet = meters * FEET_PER_METER;
// Distant (miles, rounded)
@@ -651,6 +662,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters)
return localized;
}
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
{
// How many times to draw along x axis
@@ -703,17 +715,24 @@ void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string te
// 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)
bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n)
{
// By default, no objection
return true;
}
// Draw the standard header, used by most Applets
/*
┌───────────────────────────────┐
│ Applet::name here │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ │
│ │
└───────────────────────────────┘
*/
void InkHUD::Applet::drawHeader(std::string text)
{
setFont(fontSmall);
// Y position for divider
// - between header text and messages
constexpr int16_t padDivH = 2;
@@ -771,6 +790,15 @@ uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t 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 {
@@ -788,6 +816,17 @@ void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width,
int16_t logoB = logoT + logoH - 1;
// Points for paths (a, b, and c)
/*
+-----------------------------+
--| a2 b2/c1 |
| |
| |
| |
--| a1 b1 c2 |
+-----------------------------+
| | | |
*/
Point a1 = {map(0, 0, 3, logoL, logoR), logoB};
Point a2 = {map(1, 0, 3, logoL, logoR), logoT};
Point b1 = {map(1, 0, 3, logoL, logoR), logoB};
@@ -795,17 +834,72 @@ void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width,
Point c1 = {map(2, 0, 3, logoL, logoR), logoT};
Point c2 = {map(3, 0, 3, logoL, logoR), logoB};
// Find right-angle to the path
// Find angle of the path(s)
// Used to thicken the single pixel paths
/*
+-------------------------------+
| a2 |
| -| |
| -/ | |
| -/ | |
| -/# | |
| -/ # | |
| / # | |
| a1---------- |
+-------------------------------+
*/
Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)};
float angle = tanh((float)deltaA.y / deltaA.x);
// Distance {at right angle from the paths), which will give corners for our "quads"
// Distance (at right angle to the paths), which will give corners for our "quads"
// The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner
/*
| a2
| .
| ..
| aq1 ..
| # ..
| | # ..
|fromPath.y | # ..
| +----a1
|
| fromPath.x
+--------------------------------
*/
Distance fromPath;
fromPath.x = cos(radians(90) - angle) * logoTh * 0.5;
fromPath.y = sin(radians(90) - angle) * logoTh * 0.5;
// Make the paths thick
// Corner points for the rectangles (quads):
/*
aq2
a2
/ aq3
/
/
aq1 /
a1
aq3
*/
// Filled as two triangles per quad:
/*
aq2 #
# ###
## # aq3
## ### -
## #### -/
## ### -/
## #### -/
aq1 ## -/
--- -/
\---aq4
*/
// Make the path thick: path a becomes quad a
Point aq1{a1.x - fromPath.x, a1.y - fromPath.y};
Point aq2{a2.x - fromPath.x, a2.y - fromPath.y};
@@ -822,7 +916,7 @@ void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width,
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
// Make the path thick: path c becomes quad c
Point cq1{c1.x - fromPath.x, c1.y + fromPath.y};
Point cq2{c2.x - fromPath.x, c2.y + fromPath.y};
Point cq3{c2.x + fromPath.x, c2.y - fromPath.y};
@@ -831,10 +925,21 @@ void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width,
fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, BLACK);
// Radius the intersection of quad b and quad c
/*
b2 / c1
####
## ##
/ \
/ \/ \
/ /\ \
/ / \ \
*/
// Don't attempt if logo is tiny
if (logoTh > 3) {
// The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding
// We get better results just rederiving it
// We get better results just re-deriving it
int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2));
fillCircle(b2.x, b2.y, capRad, BLACK);
}