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

@@ -30,7 +30,7 @@ class BluetoothStatus : public Status
BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; }
// New BluetoothStatus: connected or disconnected
BluetoothStatus(ConnectionState state)
explicit BluetoothStatus(ConnectionState state)
{
assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey
statusType = STATUS_TYPE_BLUETOOTH;
@@ -38,7 +38,7 @@ class BluetoothStatus : public Status
}
// New BluetoothStatus: pairing, with passkey
BluetoothStatus(std::string passkey) : Status()
explicit BluetoothStatus(const std::string &passkey) : Status()
{
statusType = STATUS_TYPE_BLUETOOTH;
this->state = ConnectionState::PAIRING;

View File

@@ -40,13 +40,11 @@ void LatchingBacklight::setPin(uint8_t pin, bool activeWhen)
// 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
// Contingency only
// - pin wasn't set
if (pin != (uint8_t)-1) {
off();
pinMode(pin, INPUT); // High impedence - unnecessary?
pinMode(pin, INPUT); // High impedance - unnecessary?
} else
LOG_WARN("LatchingBacklight instantiated, but pin not set");
return 0; // Continue with deep sleep

View File

@@ -12,7 +12,7 @@ EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported)
}
// 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
// Whether or not the update type is supported is specified in the constructor
bool EInk::supports(UpdateTypes type)
{
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.

View File

@@ -31,7 +31,7 @@ class EInk : private concurrency::OSThread
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 supports(UpdateTypes type); // Can display perform a certain update type
bool busy() { return updateRunning; } // Display able to update right now?
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
@@ -47,8 +47,8 @@ class EInk : private concurrency::OSThread
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)
uint32_t updateBegunAt = 0; // For initial pause before polling for update completion
uint32_t pollingInterval = 0; // How often to check if update complete (ms)
};
} // namespace NicheGraphics::Drivers

View File

@@ -4,7 +4,7 @@
using namespace NicheGraphics::Drivers;
// Map the display controller IC's output to the conected panel
// Map the display controller IC's output to the connected panel
void GDEY0154D67::configScanning()
{
// "Driver output control"

View File

@@ -98,6 +98,7 @@ void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t
reset();
}
// Display an image on the display
void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type)
{
this->updateType = type;
@@ -161,13 +162,6 @@ void LCMEN213EFC1::sendCommand(const uint8_t command)
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);
}

View File

@@ -45,21 +45,24 @@ class LCMEN213EFC1 : public EInk
void configFull(); // Configure display for FULL refresh
void configFast(); // Configure display for FAST refresh
void writeNewImage();
void writeOldImage();
void writeOldImage(); // Used for "differential update", aka FAST refresh
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 bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
uint32_t bufferSize = 0; // In bytes. Rows * Columns
uint8_t *buffer = nullptr;
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
uint8_t pin_dc, pin_cs, pin_busy, pin_rst;
SPIClass *spi;
uint8_t pin_dc = -1;
uint8_t pin_cs = -1;
uint8_t pin_busy = -1;
uint8_t pin_rst = -1;
SPIClass *spi = nullptr;
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
};

View File

@@ -3,7 +3,7 @@
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.
When you set up a hardware variant, you will use one of the specific display model classes, which extend the EInk class.
An example setup might look like this:
@@ -30,7 +30,7 @@ void setupNicheGraphics()
## Methods
### `update(uint8_t *imageData, UpdateTypes type, bool async=true)`
### `update(uint8_t *imageData, UpdateTypes type)`
Update the image on the display
@@ -39,7 +39,6 @@ Update the image on the display
- `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.
@@ -63,6 +62,10 @@ uint8_t xBits = (7-x) % 8;
image[yByte + xByte] |= (1 << xBits); // Set pixel x=12, y=2
```
### `await()`
Wait for an in-progress update to complete before continuing
### `supports(UpdateTypes type)`
Check if display supports a specific update type. `true` if supported.
@@ -75,7 +78,7 @@ 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.
Width of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software.
### `height()`

View File

@@ -30,7 +30,7 @@ void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_b
pinMode(pin_busy, INPUT);
// If using a reset pin, hold high
// Reset is active low for solmon systech ICs
// Reset is active low for Solomon Systech ICs
if (pin_rst != 0xFF)
pinMode(pin_rst, INPUT_PULLUP);
@@ -72,13 +72,6 @@ void SSD16XX::sendCommand(const uint8_t command)
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);
}

View File

@@ -39,21 +39,24 @@ class SSD16XX : public EInk
virtual void configUpdateSequence(); // Tell controller IC which operations to run
virtual void writeNewImage();
virtual void writeOldImage();
virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh"
virtual void detachFromUpdate();
virtual bool isUpdateDone() override;
virtual void finalizeUpdate() override;
protected:
uint8_t bufferOffsetX; // 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 bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
uint32_t bufferSize = 0; // In bytes. Rows * Columns
uint8_t *buffer = nullptr;
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
uint8_t pin_dc, pin_cs, pin_busy, pin_rst;
SPIClass *spi;
uint8_t pin_dc = -1;
uint8_t pin_cs = -1;
uint8_t pin_busy = -1;
uint8_t pin_rst = -1;
SPIClass *spi = nullptr;
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
};

View File

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

View File

@@ -119,7 +119,7 @@ template <typename T> class FlashData
// 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 *)data, sizeof(T)); // Write the actual data
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
// f.flush();

View File

@@ -4,7 +4,7 @@ Uses Windows-1251 encoding to map translingual Cyrillic characters to range betw
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.
A NicheGraphics implementation needs to identify these, and substitute the appropriate Windows-1251 char value.
*/

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

View File

@@ -7,103 +7,21 @@
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 <GFX.h> // GFXRoot drawing lib
#include "mesh/MeshTypes.h"
#include "./AppletFont.h"
#include "./Applets/System/Notification/Notification.h"
#include "./Applets/System/Notification/Notification.h" // The notification object, not the applet
#include "./InkHUD.h"
#include "./Persistence.h"
#include "./Tile.h"
#include "./Types.h"
#include "./WindowManager.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
@@ -112,37 +30,57 @@ namespace NicheGraphics::InkHUD
using NicheGraphics::Drivers::EInk;
using std::to_string;
class Tile;
class WindowManager;
class Applet : public GFX
{
public:
// Which edge Applet::printAt will place on the Y parameter
enum VerticalAlignment : uint8_t {
TOP,
MIDDLE,
BOTTOM,
};
// Which edge Applet::printAt will place on the X parameter
enum HorizontalAlignment : uint8_t {
LEFT,
RIGHT,
CENTER,
};
// An easy-to-understand interpretation of SNR and RSSI
// Calculate with Applet::getSignalStrength
enum SignalStrength : int8_t {
SIGNAL_UNKNOWN = -1,
SIGNAL_NONE,
SIGNAL_BAD,
SIGNAL_FAIR,
SIGNAL_GOOD,
};
Applet();
void setTile(Tile *t); // Applets draw via a tile (for multiplexing)
Tile *getTile();
void setTile(Tile *t); // Should only be called via Tile::setApplet
Tile *getTile(); // Tile with which this applet is linked
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
// Rendering
void render(); // Draw the applet
bool wantsToRender(); // Check whether applet wants to render
bool wantsToAutoshow(); // Check whether applet wants to become foreground
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
void updateDimensions(); // Get current size from tile
void resetDrawingSpace(); // Makes sure every render starts with same parameters
// Change the applet's state
void activate();
void deactivate();
void bringToForeground();
void sendToBackground();
// Info about applet's state
// State of the applet
void activate(); // Begin running
void deactivate(); // Stop running
void bringToForeground(); // Show
void sendToBackground(); // Hide
bool isActive();
bool isForeground();
// Allow derived applets to handle changes in state
// Event handlers
virtual void onRender() = 0; // All drawing happens here
virtual void onActivate() {}
@@ -150,62 +88,62 @@ class Applet : public GFX
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 void onButtonShortPress() {} // (System Applets only)
virtual void onButtonLongPress() {} // (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
static uint16_t getHeaderHeight(); // How tall the "standard" applet header is
const char *name = nullptr; // Shown in applet selection menu
static AppletFont fontSmall, fontLarge; // The general purpose fonts, used by all applets
const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet
protected:
// Place a single pixel. All drawing methods output through here
void drawPixel(int16_t x, int16_t y, uint16_t color) override;
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
// Tell WindowManager to update display
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED);
// Ask for applet to be moved to foreground
void requestAutoshow();
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
void requestAutoshow(); // Ask for applet to be moved to foreground
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
void resetCrop(); // Removes setCrop()
// Text
void setFont(AppletFont f);
AppletFont getFont();
uint16_t getTextWidth(std::string text);
uint16_t getTextWidth(const char *text);
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY);
// 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 printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
void drawHeader(std::string text); // Draw the standard applet header
// Meshtastic Logo
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height); // Draw the meshtastic logo
std::string hexifyNodeNum(NodeNum num);
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
std::string getTimeString(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
static AppletFont fontSmall, fontLarge; // General purpose fonts, used cross-applet
// Convenient references
InkHUD *inkhud = nullptr;
Persistence::Settings *settings = nullptr;
Persistence::LatestMessage *latestMessage = nullptr;
private:
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
@@ -223,10 +161,10 @@ class Applet : public GFX
AppletFont currentFont; // As passed to setFont
// As set by setCrop
int16_t cropLeft;
int16_t cropTop;
uint16_t cropWidth;
uint16_t cropHeight;
int16_t cropLeft = 0;
int16_t cropTop = 0;
uint16_t cropWidth = 0;
uint16_t cropHeight = 0;
};
}; // namespace NicheGraphics::InkHUD

View File

@@ -12,7 +12,7 @@ InkHUD::AppletFont::AppletFont()
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.
// they print as if the glyphs are resting on the line of piece of ruled paper.
// The glyphs also each have a different height.
// To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text
@@ -42,6 +42,19 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafru
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
}
/*
▲ ##### # ▲
│ # # │
lineHeight │ ### # │
│ # # # # │ heightAboveCursor
│ # # # # │
│ # # #### │
│ -----------------#----
│ # │ heightBelowCursor
▼ ### ▼
*/
uint8_t InkHUD::AppletFont::lineHeight()
{
return this->height;
@@ -78,7 +91,7 @@ void InkHUD::AppletFont::addSubstitution(const char *from, const char *to)
substitutions.push_back({.from = from, .to = to});
}
// Run all registered subtitutions on a string
// Run all registered substitutions on a string
// Used to swap out UTF8 special chars
void InkHUD::AppletFont::applySubstitutions(std::string *text)
{
@@ -87,7 +100,7 @@ void InkHUD::AppletFont::applySubstitutions(std::string *text)
// Find and replace
// - search for Substitution::from
// - replace with Subsitution::to
// - replace with Substitution::to
size_t i = text->find(s.from);
while (i != std::string::npos) {
text->replace(i, strlen(s.from), s.to);
@@ -97,7 +110,7 @@ void InkHUD::AppletFont::applySubstitutions(std::string *text)
}
// 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
// Windows-1251 is an 8-bit character encoding, suitable for several languages which use the Cyrillic script
void InkHUD::AppletFont::addSubstitutionsWin1251()
{
addSubstitution("Ђ", "\x80");

View File

@@ -15,7 +15,7 @@
#include "configuration.h"
#include <GFX.h>
#include <GFX.h> // GFXRoot drawing lib
namespace NicheGraphics::InkHUD
{
@@ -25,11 +25,12 @@ class AppletFont
{
public:
AppletFont();
AppletFont(const GFXfont &adafruitGFXFont);
explicit AppletFont(const GFXfont &adafruitGFXFont);
uint8_t lineHeight();
uint8_t heightAboveCursor();
uint8_t heightBelowCursor();
uint8_t widthBetweenWords();
uint8_t widthBetweenWords(); // Width of the space character
void applySubstitutions(std::string *text); // Run all char-substitution operations, prior to printing
void addSubstitution(const char *from, const char *to); // Register a find-replace action, for remapping UTF8 chars
@@ -50,8 +51,7 @@ class AppletFont
const char *to;
};
// List of all character substitutions to run, prior to printing a string
std::vector<Substitution> substitutions;
std::vector<Substitution> substitutions; // List of all character substitutions to run, prior to printing a string
};
} // namespace NicheGraphics::InkHUD

View File

@@ -6,8 +6,6 @@ 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);
@@ -27,6 +25,7 @@ void InkHUD::MapApplet::onRender()
// Set the region shown on the map
// - default: fit all nodes, plus padding
// - maybe overriden by derived applet
// - getMapSize *sets* passed parameters (C-style)
getMapSize(&widthMeters, &heightMeters);
// Set the metersToPx conversion value
@@ -71,7 +70,7 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
// - 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
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's surface
// Working totals, averaged after nodeDB processed
uint32_t positionCount = 0;
@@ -134,7 +133,7 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
// Latitude from cartesian cooods
// Latitude from cartesian coords
// (Angle from 3D coords describing a point on the globe's surface)
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
@@ -191,8 +190,8 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
// 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
float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
if (degEastward < degWestward)
easternmost = max(easternmost, lngCenter + degEastward);
else
@@ -258,7 +257,7 @@ 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->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
);
@@ -288,7 +287,7 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
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
// Hopefully avoid text spilling off screen
// Most values are the same, regardless of left-right handedness
// Pick emblem style
@@ -388,7 +387,7 @@ void InkHUD::MapApplet::calculateAllMarkers()
// 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->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
));

View File

@@ -38,13 +38,12 @@ class MapApplet : public Applet
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
// Position and size of a marker to be drawn
struct Marker {
float eastMeters = 0; // Meters east of mapCenter. Negative if west.
float northMeters = 0; // Meters north of mapCenter. Negative if south.
float eastMeters = 0; // Meters east of map center. Negative if west.
float northMeters = 0; // Meters north of map center. Negative if south.
bool hasHopsAway = false;
uint8_t hopsAway = 0;
uint8_t hopsAway = 0; // Determines marker size
};
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);

View File

@@ -12,7 +12,7 @@ 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
// For all other packets, we manually act as if isPromiscuous=false, in wantPacket
MeshModule::isPromiscuous = true;
}
@@ -25,17 +25,17 @@ bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p)
&& (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 achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here,
// to match the code in MeshModule::callModules
}
// MeshModule packets arrive here
// Extract the info and pass it to the derived applet
// Derived applet will store the CardInfo and perform any required sorting of the CardInfo collection
// Derived applet 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)
{
@@ -76,8 +76,8 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
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
// Calculate maximum number of cards we may ever need to render, in our tallest layout config
// Number might be slightly in excess of the true value: applet header text not accounted for
uint8_t InkHUD::NodeListApplet::maxCards()
{
// Cache result. Shouldn't change during execution
@@ -87,7 +87,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
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
// Add cards one by one, until the latest card extends below screen
uint16_t y = cardH; // First card: no margin above
cards = 1;
@@ -102,7 +102,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
return cards;
}
// Draw using info which derived applet placed into NodeListApplet::cards for us
// Draw, using info which derived applet placed into NodeListApplet::cards for us
void InkHUD::NodeListApplet::onRender()
{
@@ -120,9 +120,6 @@ void InkHUD::NodeListApplet::onRender()
// 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");
@@ -215,9 +212,8 @@ void InkHUD::NodeListApplet::onRender()
// Once we've run out of screen, stop drawing cards
// Depending on tiles / rotation, this may be before we hit maxCards
if (cardTopY > height()) {
if (cardTopY > height())
break;
}
}
}
@@ -246,20 +242,20 @@ void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t
constexpr float paddingW = 0.1; // Either side
constexpr float paddingH = 0.1; // Above and below
constexpr float gutterX = 0.1; // Between bars
constexpr float gutterW = 0.1; // Between bars
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the talleest
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest
constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.
// Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterX) + paddingW)) / barCount;
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount;
float barHMax = 1.0 - (paddingH + paddingH);
// Draw signal bar rectangles, then placeholder lines once strength reached
for (uint8_t i = 0; i < barCount; i++) {
// Co-ords for this specific bar
// Coords for this specific bar
float barH = barHMax * barHRel[i];
float barX = paddingW + (i * (gutterX + barW));
float barX = paddingW + (i * (gutterW + barW));
float barY = paddingH + (barHMax - barH);
// Rasterize to px coords at the last moment

View File

@@ -23,13 +23,16 @@ Used by the "Recents" and "Heard" applets. Possibly more in future?
#include "graphics/niche/InkHUD/Applet.h"
#include "main.h"
namespace NicheGraphics::InkHUD
{
class NodeListApplet : public Applet, public MeshModule
{
protected:
// Info used to draw one card to the node list
// Info needed to draw a node card to the list
// - generated each time we hear a node
struct CardInfo {
static constexpr uint8_t HOPS_UNKNOWN = -1;
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
@@ -37,31 +40,31 @@ class NodeListApplet : public Applet, public MeshModule
NodeNum nodeNum = 0;
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
uint32_t distanceMeters = DISTANCE_UNKNOWN;
uint8_t hopsAway = HOPS_UNKNOWN; // Unknown
uint8_t hopsAway = HOPS_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;
bool wantPacket(const meshtastic_MeshPacket *p) override;
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?
virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node
virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be
uint8_t maxCards(); // Calculate the maximum number of cards an applet could ever display
uint8_t maxCards(); // Max number of cards which could ever fit on screen
std::deque<CardInfo> cards; // Derived applet places cards here, for this base applet to render
std::deque<CardInfo> cards; // Cards to be rendered. Derived applet fills this.
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);
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h,
SignalStrength signal); // Draw a "mobile phone" style signal indicator
// Dimensions for drawing
// Used for render, and also for maxCards calc
// Card Dimensions
// - for rendering and for maxCards calc
const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
};

View File

@@ -36,8 +36,6 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
// 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

View File

@@ -53,7 +53,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
// Store info from handleReceived
bool haveMessage = false;
NodeNum fromWho;
NodeNum fromWho = 0;
};
} // namespace NicheGraphics::InkHUD

View File

@@ -4,10 +4,10 @@
using namespace NicheGraphics;
void InkHUD::BatteryIconApplet::onActivate()
InkHUD::BatteryIconApplet::BatteryIconApplet()
{
// Show at boot, if user has previously enabled the feature
if (settings.optionalFeatures.batteryIcon)
if (settings->optionalFeatures.batteryIcon)
bringToForeground();
// Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available
@@ -15,12 +15,6 @@ void InkHUD::BatteryIconApplet::onActivate()
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
@@ -41,7 +35,7 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
// 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)
if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon)
requestUpdate();
// Store the new value

View File

@@ -11,24 +11,22 @@ It should be optional, enabled by the on-screen menu
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include "PowerStatus.h"
namespace NicheGraphics::InkHUD
{
class BatteryIconApplet : public Applet
class BatteryIconApplet : public SystemApplet
{
public:
BatteryIconApplet();
void onRender() override;
void onActivate() override;
void onDeactivate() override;
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
protected:
private:
// Get informed when new information about the battery is available (via onPowerStatusUpdate method)
CallbackObserver<BatteryIconApplet, const meshtastic::Status *> powerStatusObserver =
CallbackObserver<BatteryIconApplet, const meshtastic::Status *>(this, &BatteryIconApplet::onPowerStatusUpdate);

View File

@@ -2,15 +2,22 @@
#include "./LogoApplet.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
{
// Don't autostart the runOnce() timer
OSThread::disable();
OSThread::setIntervalFromNow(8 * 1000UL);
OSThread::enabled = true;
// Grab the WindowManager singleton, for convenience
windowManager = WindowManager::getInstance();
textLeft = "";
textRight = "";
textTitle = xstr(APP_VERSION_SHORT);
fontTitle = fontSmall;
bringToForeground();
// This is then drawn with a FULL refresh by Renderer::begin
}
void InkHUD::LogoApplet::onRender()
@@ -48,53 +55,24 @@ void InkHUD::LogoApplet::onRender()
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
SystemApplet::lockRendering = true;
SystemApplet::lockRequests = true;
SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it.
}
void InkHUD::LogoApplet::onBackground()
{
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
SystemApplet::lockRendering = false;
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
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
inkhud->forceUpdate(EInk::UpdateTypes::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()
void InkHUD::LogoApplet::onShutdown()
{
textLeft = "";
textRight = "";
@@ -102,7 +80,13 @@ void InkHUD::LogoApplet::showShutdownScreen()
fontTitle = fontLarge;
bringToForeground();
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update
}
int32_t InkHUD::LogoApplet::runOnce()
{
sendToBackground();
return OSThread::disable();
}
#endif

View File

@@ -12,24 +12,19 @@
#include "configuration.h"
#include "concurrency/OSThread.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
namespace NicheGraphics::InkHUD
{
class LogoApplet : public Applet, public concurrency::OSThread
class LogoApplet : public SystemApplet, 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();
void onShutdown() override;
protected:
int32_t runOnce() override;
@@ -38,8 +33,6 @@ class LogoApplet : public Applet, public concurrency::OSThread
std::string textRight;
std::string textTitle;
AppletFont fontTitle;
WindowManager *windowManager = nullptr; // For convenience
};
} // namespace NicheGraphics::InkHUD

View File

@@ -2,9 +2,11 @@
#include "./MenuApplet.h"
#include "PowerStatus.h"
#include "RTC.h"
#include "airtime.h"
#include "power.h"
using namespace NicheGraphics;
static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes
@@ -17,23 +19,16 @@ 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) {
if (settings->optionalMenuItems.backlight) {
backlight = Drivers::LatchingBacklight::getInstance();
}
}
void InkHUD::MenuApplet::onActivate() {}
void InkHUD::MenuApplet::onForeground()
{
// We do need this before we render, but we can optimize by just calculating it once now
@@ -45,21 +40,23 @@ void InkHUD::MenuApplet::onForeground()
// 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) {
if (settings->optionalMenuItems.backlight) {
assert(backlight);
if (!backlight->isOn())
backlight->peek();
}
// Prevent user applets requested update while menu is open
windowManager->lock(this);
// Prevent user applets requesting update while menu is open
// Handle button input with this applet
SystemApplet::lockRequests = true;
SystemApplet::handleInput = true;
// Begin the auto-close timeout
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
// Upgrade the refresh to FAST, for guaranteed responsiveness
windowManager->forceUpdate(EInk::UpdateTypes::FAST);
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onBackground()
@@ -67,7 +64,7 @@ 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) {
if (settings->optionalMenuItems.backlight) {
assert(backlight);
if (!backlight->isLatched())
backlight->off();
@@ -77,7 +74,8 @@ void InkHUD::MenuApplet::onBackground()
OSThread::disable();
// Resume normal rendering and button behavior of user applets
windowManager->unlock(this);
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
// Restore the user applet whose tile we borrowed
if (borrowedTileOwner)
@@ -87,8 +85,8 @@ void InkHUD::MenuApplet::onBackground()
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);
// We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
// Open the menu
@@ -140,43 +138,35 @@ void InkHUD::MenuApplet::execute(MenuItem item)
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;
inkhud->nextTile();
break;
case ROTATE:
settings.rotation = (settings.rotation + 1) % 4;
windowManager->changeLayout();
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL
inkhud->rotate();
break;
case LAYOUT:
// Todo: smarter incrementing of tile count
settings.userTiles.count++;
settings->userTiles.count++;
if (settings.userTiles.count == 3) // Skip 3 tiles: not done yet
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;
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
inkhud->updateLayout();
break;
case TOGGLE_APPLET:
settings.userApplets.active[cursor] = !settings.userApplets.active[cursor];
windowManager->changeActivatedApplets();
settings->userApplets.active[cursor] = !settings->userApplets.active[cursor];
inkhud->updateAppletSelection();
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Select FULL, seeing how this action doesn't auto exit
break;
case ACTIVATE_APPLETS:
// Todo: remove this action? Already handled by TOGGLE_APPLET?
windowManager->changeActivatedApplets();
inkhud->updateAppletSelection();
break;
case TOGGLE_AUTOSHOW_APPLET:
@@ -185,14 +175,14 @@ void InkHUD::MenuApplet::execute(MenuItem item)
break;
case TOGGLE_NOTIFICATIONS:
settings.optionalFeatures.notifications = !settings.optionalFeatures.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
settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes
break;
case SHUTDOWN:
@@ -202,7 +192,7 @@ void InkHUD::MenuApplet::execute(MenuItem item)
break;
case TOGGLE_BATTERY_ICON:
windowManager->toggleBatteryIcon();
inkhud->toggleBatteryIcon();
break;
case TOGGLE_BACKLIGHT:
@@ -233,13 +223,13 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
switch (page) {
case ROOT:
// Optional: next applet
if (settings.optionalMenuItems.nextTile && settings.userTiles.count > 1)
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("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
@@ -252,7 +242,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case OPTIONS:
// Optional: backlight
if (settings.optionalMenuItems.backlight) {
if (settings->optionalMenuItems.backlight) {
assert(backlight);
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
MenuAction::TOGGLE_BACKLIGHT, // Action
@@ -263,13 +253,13 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
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)
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));
&settings->optionalFeatures.notifications));
items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS,
&settings->optionalFeatures.batteryIcon));
// TODO - GPS and Wifi switches
/*
@@ -329,9 +319,6 @@ 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;
@@ -397,7 +384,7 @@ void InkHUD::MenuApplet::onRender()
// Testing only: circle instead of check box
if (item.checkState) {
const uint16_t cbWH = fontSmall.lineHeight(); // Checbox: width / height
const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height
const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
const int16_t cbT = center - (cbWH / 2); // Checkbox : top
// Checkbox ticked
@@ -463,9 +450,9 @@ 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]);
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
const char *name = inkhud->userApplets.at(i)->name;
bool *isActive = &(settings->userApplets.active[i]);
items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive));
}
}
@@ -477,11 +464,11 @@ void InkHUD::MenuApplet::populateAutoshowPage()
{
assert(items.size() == 0);
for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) {
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
// Only add a menu item if applet is active
if (settings.userApplets.active[i]) {
const char *name = windowManager->getAppletName(i);
bool *isActive = &(settings.userApplets.autoshow[i]);
if (settings->userApplets.active[i]) {
const char *name = inkhud->userApplets.at(i)->name;
bool *isActive = &(settings->userApplets.autoshow[i]);
items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive));
}
}
@@ -599,10 +586,10 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t
// 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
// It solves a catch-22 situation, where slotCount needs to know panel height, and panel height needs to know slotCount
uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
{
// Render *waay* off screen
// Render *far* off screen
uint16_t height = 0;
drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height);

View File

@@ -3,8 +3,9 @@
#include "configuration.h"
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/WindowManager.h"
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include "./MenuItem.h"
#include "./MenuPage.h"
@@ -16,7 +17,7 @@ namespace NicheGraphics::InkHUD
class Applet;
class MenuApplet : public Applet, public concurrency::OSThread
class MenuApplet : public SystemApplet, public concurrency::OSThread
{
public:
MenuApplet();
@@ -30,6 +31,8 @@ class MenuApplet : public Applet, public concurrency::OSThread
void show(Tile *t); // Open the menu, onto a user tile
protected:
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
int32_t runOnce() override;
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
@@ -41,7 +44,7 @@ class MenuApplet : public Applet, public concurrency::OSThread
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu
MenuPage currentPage;
MenuPage currentPage = MenuPage::ROOT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
@@ -50,9 +53,6 @@ class MenuApplet : public Applet, public concurrency::OSThread
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

View File

@@ -3,22 +3,20 @@
#include "./NotificationApplet.h"
#include "./Notification.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "meshUtils.h"
#include "modules/TextMessageModule.h"
#include "RTC.h"
using namespace NicheGraphics;
void InkHUD::NotificationApplet::onActivate()
InkHUD::NotificationApplet::NotificationApplet()
{
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)
@@ -28,7 +26,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
// Abort if feature disabled
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
if (!settings.optionalFeatures.notifications)
if (!settings->optionalFeatures.notifications)
return 0;
// Abort if this is an outgoing message
@@ -36,7 +34,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
return 0;
// Abort if message was only an "emoji reaction"
// Possibly some implemetation of this in future?
// Possibly some implementation of this in future?
if (p->decoded.emoji)
return 0;
@@ -55,13 +53,16 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
n.sender = p->from;
}
// Close an old notification, if shown
dismiss();
// 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();
inkhud->forceUpdate();
} else
hasNotification = false; // Clear the pending notification: it was rejected
@@ -76,8 +77,6 @@ void InkHUD::NotificationApplet::onRender()
// 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;
@@ -137,6 +136,28 @@ void InkHUD::NotificationApplet::onRender()
printThick(textM, height() / 2, text, 2, 1);
}
void InkHUD::NotificationApplet::onForeground()
{
handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification
}
void InkHUD::NotificationApplet::onBackground()
{
handleInput = false;
}
void InkHUD::NotificationApplet::onButtonShortPress()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onButtonLongPress()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
// 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
@@ -148,7 +169,13 @@ bool InkHUD::NotificationApplet::isApproved()
return false;
}
return WindowManager::getInstance()->approveNotification(currentNotification);
// Ask all visible user applets for approval
for (Applet *ua : inkhud->userApplets) {
if (ua->isForeground() && !ua->approveNotification(currentNotification))
return false;
}
return true;
}
// Mark that the notification should no-longer be rendered
@@ -180,7 +207,8 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
// Pick source of message
MessageStore::Message *message = isBroadcast ? &latestMessage.broadcast : &latestMessage.dm;
MessageStore::Message *message =
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
// Find info about the sender
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);

View File

@@ -3,7 +3,7 @@
/*
Pop-up notification bar, on screen top edge
Displays information we feel is important, but which is not shown on currently focussed applet(s)
Displays information we feel is important, but which is not shown on currently focused applet(s)
E.g.: messages, while viewing map, etc
Feature should be optional; enable disable via on-screen menu
@@ -16,17 +16,21 @@ Feature should be optional; enable disable via on-screen menu
#include "concurrency/OSThread.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
namespace NicheGraphics::InkHUD
{
class NotificationApplet : public Applet
class NotificationApplet : public SystemApplet
{
public:
NotificationApplet();
void onRender() override;
void onActivate() override;
void onDeactivate() override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
@@ -40,8 +44,8 @@ class NotificationApplet : public Applet
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()
bool hasNotification = false; // Only used for assert. Todo: remove?
Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render()
};
} // namespace NicheGraphics::InkHUD

View File

@@ -6,8 +6,7 @@ using namespace NicheGraphics;
InkHUD::PairingApplet::PairingApplet()
{
// Grab the window manager singleton, for convenience
windowManager = WindowManager::getInstance();
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
}
void InkHUD::PairingApplet::onRender()
@@ -31,34 +30,22 @@ void InkHUD::PairingApplet::onRender()
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
// Prevent most other applets from requesting update, and skip their rendering entirely
// Another system applet with a higher precedence can potentially ignore this
SystemApplet::lockRendering = true;
SystemApplet::lockRequests = true;
}
void InkHUD::PairingApplet::onBackground()
{
windowManager->releaseFullscreen(); // Relinquish ownership of the fullscreen tile
windowManager->unlock(this); // Allow normal user applet update requests to resume
// Allow normal update behavior to resume
SystemApplet::lockRendering = false;
SystemApplet::lockRequests = false;
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
@@ -75,12 +62,6 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta
// 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();
}

View File

@@ -10,19 +10,19 @@
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include "main.h"
namespace NicheGraphics::InkHUD
{
class PairingApplet : public Applet
class PairingApplet : public SystemApplet
{
public:
PairingApplet();
void onRender() override;
void onActivate() override;
void onDeactivate() override;
void onForeground() override;
void onBackground() override;
@@ -34,8 +34,6 @@ class PairingApplet : public Applet
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

View File

@@ -4,14 +4,6 @@
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

View File

@@ -9,20 +9,19 @@ Fills the area with diagonal lines
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
namespace NicheGraphics::InkHUD
{
class PlaceholderApplet : public Applet
class PlaceholderApplet : public SystemApplet
{
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
// It may be drawn to several different tiles during an Renderer::render call
};
} // namespace NicheGraphics::InkHUD

View File

@@ -2,12 +2,44 @@
#include "./TipsApplet.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "main.h"
using namespace NicheGraphics;
InkHUD::TipsApplet::TipsApplet()
{
// Grab the window manager singleton, for convenience
windowManager = WindowManager::getInstance();
// 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 is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground
// LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector
if (!tipQueue.empty())
bringToForeground();
}
void InkHUD::TipsApplet::onRender()
@@ -53,7 +85,7 @@ void InkHUD::TipsApplet::onRender()
setFont(fontSmall);
std::string shutdown;
shutdown += "Before removing power, please shutdown from InkHUD menu, or a client app. \n";
shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n";
shutdown += "\n";
shutdown += "This ensures data is saved.";
printWrapped(0, fontLarge.lineHeight() * 1.5, width(), shutdown);
@@ -153,51 +185,31 @@ void InkHUD::TipsApplet::renderWelcome()
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);
// Prevent most other applets from requesting update, and skip their rendering entirely
// Another system applet with a higher precedence can potentially ignore this
SystemApplet::lockRendering = true;
SystemApplet::lockRequests = true;
SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first)
}
void InkHUD::TipsApplet::onBackground()
{
windowManager->releaseFullscreen();
windowManager->unlock(this);
// Allow normal update behavior to resume
SystemApplet::lockRendering = false;
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::TipsApplet::onActivate()
{
// Decide which tips (if any) should be shown to user after the boot screen
void InkHUD::TipsApplet::onActivate() {}
// 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
// While our SystemApplet::handleInput flag is true
void InkHUD::TipsApplet::onButtonShortPress()
{
tipQueue.pop_front();
@@ -206,15 +218,15 @@ void InkHUD::TipsApplet::onButtonShortPress()
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();
if (settings->tips.firstBoot) {
settings->tips.firstBoot = false;
inkhud->persistence->saveSettings();
}
// 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);
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
// More tips left
@@ -222,13 +234,4 @@ void InkHUD::TipsApplet::onButtonShortPress()
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

View File

@@ -12,12 +12,12 @@
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
namespace NicheGraphics::InkHUD
{
class TipsApplet : public Applet
class TipsApplet : public SystemApplet
{
protected:
enum class Tip {
@@ -37,7 +37,6 @@ class TipsApplet : public Applet
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

View File

@@ -41,14 +41,12 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
void InkHUD::AllMessageApplet::onRender()
{
setFont(fontSmall);
// Find newest message, regardless of whether DM or broadcast
MessageStore::Message *message;
if (latestMessage.wasBroadcast)
message = &latestMessage.broadcast;
if (latestMessage->wasBroadcast)
message = &latestMessage->broadcast;
else
message = &latestMessage.dm;
message = &latestMessage->dm;
// Short circuit: no text message
if (!message->sender) {

View File

@@ -44,10 +44,8 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
void InkHUD::DMApplet::onRender()
{
setFont(fontSmall);
// Abort if no text message
if (!latestMessage.dm.sender) {
if (!latestMessage->dm.sender) {
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
return;
}
@@ -63,7 +61,7 @@ void InkHUD::DMApplet::onRender()
// RX Time
// - if valid
std::string timeString = getTimeString(latestMessage.dm.timestamp);
std::string timeString = getTimeString(latestMessage->dm.timestamp);
if (timeString.length() > 0) {
header += timeString;
header += ": ";
@@ -72,14 +70,14 @@ void InkHUD::DMApplet::onRender()
// Sender's id
// - shortname, if available, or
// - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage.dm.sender);
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);
header += hexifyNodeNum(latestMessage->dm.sender);
// Draw a "standard" applet header
drawHeader(header);
@@ -103,14 +101,14 @@ void InkHUD::DMApplet::onRender()
// Determine size if printed large
setFont(fontLarge);
uint32_t textHeight = getWrappedTextHeight(0, width(), latestMessage.dm.text);
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);
printWrapped(0, textTop, width(), latestMessage->dm.text);
}
// Don't show notifications for direct messages when our applet is displayed

View File

@@ -29,13 +29,13 @@ class PositionsApplet : public MapApplet, public SinglePortModule
protected:
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
NodeNum lastFrom; // Sender of most recent (non-local) position packet
float lastLat;
float lastLng;
float lastHopsAway;
NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet
float lastLat = 0.0;
float lastLng = 0.0;
float lastHopsAway = 0;
float ourLastLat; // Info about the most recent (non-local) position packet
float ourLastLng; // Info about most recent *local* position
float ourLastLat = 0.0; // Info about the most recent (non-local) position packet
float ourLastLng = 0.0; // Info about most recent *local* position
};
} // namespace NicheGraphics::InkHUD

View File

@@ -122,7 +122,7 @@ bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs)
uint32_t now = millis();
uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe
return (secsAgo < settings.recentlyActiveSeconds);
return (secsAgo < settings->recentlyActiveSeconds);
}
// Text to be shown at top of applet
@@ -134,7 +134,7 @@ std::string InkHUD::RecentsListApplet::getHeaderText()
// Print the length of our "Recents" time-window
text += "Last ";
text += to_string(settings.recentlyActiveSeconds / 60);
text += to_string(settings->recentlyActiveSeconds / 60);
text += " mins";
// Print the node count

View File

@@ -23,8 +23,6 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : cha
void InkHUD::ThreadedMessageApplet::onRender()
{
setFont(fontSmall);
// =============
// Draw a header
// =============

View File

@@ -33,7 +33,7 @@ class Applet;
class ThreadedMessageApplet : public Applet
{
public:
ThreadedMessageApplet(uint8_t channelIndex);
explicit ThreadedMessageApplet(uint8_t channelIndex);
ThreadedMessageApplet() = delete;
void onRender() override;

View File

@@ -1,46 +1,73 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./UpdateMediator.h"
#include "./WindowManager.h"
#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::UpdateMediator::UpdateMediator() : concurrency::OSThread("Mediator")
InkHUD::DisplayHealth::DisplayHealth() : 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)
// 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 conveninece
// 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 halt itself when the timer next fires,
// 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 (requested == UpdateTypes::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 (requested == UpdateTypes::FAST) {
if (finalDecision == UpdateTypes::FAST) {
LOG_DEBUG("Explicit FAST");
// Add to the FULL refresh debt
if (debt < 1.0)
@@ -49,7 +76,8 @@ Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::evaluate(Drivers::EInk::Updat
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
// 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();
@@ -75,10 +103,8 @@ Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::evaluate(Drivers::EInk::Updat
// 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");
if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL)
OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow
}
return UpdateTypes::FULL;
}
@@ -86,8 +112,9 @@ Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::evaluate(Drivers::EInk::Updat
// 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)
// 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:
@@ -104,20 +131,20 @@ Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::prioritize(Drivers::EInk::Upd
}
// We're using the timer to perform "maintenance"
// If signifcant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked.
// If significant 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.
// 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::UpdateMediator::runOnce()
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
WindowManager::getInstance()->forceUpdate(EInk::UpdateTypes::FULL);
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);
@@ -134,17 +161,15 @@ int32_t InkHUD::UpdateMediator::runOnce()
// 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()
void InkHUD::DisplayHealth::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()
int32_t InkHUD::DisplayHealth::endMaintenance()
{
LOG_DEBUG("Maintenance disabled");
return OSThread::disable();
}

View File

@@ -2,7 +2,8 @@
/*
Responsible for display health
Responsible for maintaining display health, by optimizing the ratio of FAST vs FULL refreshes
- 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
@@ -13,21 +14,21 @@ Responsible for display health
#include "configuration.h"
#include "InkHUD.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
class UpdateMediator : protected concurrency::OSThread
class DisplayHealth : protected concurrency::OSThread
{
public:
UpdateMediator();
DisplayHealth();
// 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);
void requestUpdateType(Drivers::EInk::UpdateTypes type);
void forceUpdateType(Drivers::EInk::UpdateTypes type);
Drivers::EInk::UpdateTypes decideUpdateType();
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?
@@ -37,6 +38,13 @@ class UpdateMediator : protected concurrency::OSThread
void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health
int32_t endMaintenance(); // End unprovoked refreshing: debt paid
Drivers::EInk::UpdateTypes
prioritize(Drivers::EInk::UpdateTypes type1,
Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor
bool forced = false;
Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED;
float debt = 0.0; // How many full refreshes are due
};

View File

@@ -0,0 +1,179 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Events.h"
#include "RTC.h"
#include "modules/TextMessageModule.h"
#include "sleep.h"
#include "./Applet.h"
#include "./SystemApplet.h"
using namespace NicheGraphics;
InkHUD::Events::Events()
{
// Get convenient references
inkhud = InkHUD::getInstance();
settings = &inkhud->persistence->settings;
}
void InkHUD::Events::begin()
{
// Register our callbacks for the various events
deepSleepObserver.observe(&notifyDeepSleep);
rebootObserver.observe(&notifyReboot);
textMessageObserver.observe(textMessageModule);
#ifdef ARCH_ESP32
lightSleepObserver.observe(&notifyLightSleep);
#endif
}
void InkHUD::Events::onButtonShort()
{
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onButtonShortPress();
else
inkhud->nextApplet();
}
void InkHUD::Events::onButtonLong()
{
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
// If no system applet is handling input, default behavior instead is to open the menu
if (consumer)
consumer->onButtonLongPress();
else
inkhud->openMenu();
}
// Callback for deepSleepObserver
// Returns 0 to signal that we agree to sleep now
int InkHUD::Events::beforeDeepSleep(void *unused)
{
// Notify all applets that we're shutting down
for (Applet *ua : inkhud->userApplets) {
ua->onDeactivate();
ua->onShutdown();
}
for (SystemApplet *sa : inkhud->systemApplets) {
// Note: no onDeactivate. System applets are always active.
sa->onShutdown();
}
// User has successful executed a safe shutdown
// We don't need to nag at boot anymore
settings->tips.safeShutdownSeen = true;
inkhud->persistence->saveSettings();
inkhud->persistence->saveLatestMessage();
// LogoApplet::onShutdown will have requested an update, to draw the shutdown screen
// Draw that now, and wait here until the update is complete
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
return 0; // We agree: deep sleep now
}
// Callback for rebootObserver
// Same as shutdown, without drawing the logoApplet
// Makes sure we don't lose message history / InkHUD config
int InkHUD::Events::beforeReboot(void *unused)
{
// Notify all applets that we're "shutting down"
// They don't need to know that it's really a reboot
for (Applet *a : inkhud->userApplets) {
a->onDeactivate();
a->onShutdown();
}
for (Applet *sa : inkhud->systemApplets) {
// Note: no onDeactivate. System applets are always active.
sa->onShutdown();
}
inkhud->persistence->saveSettings();
inkhud->persistence->saveLatestMessage();
// Note: no forceUpdate call here
// Because OSThread will not be given another chance to run before reboot, this means that no display update will occur
return 0; // No special status to report. Ignored anyway by this Observable
}
// Callback when a new text message is received
// Caches the most recently received message, for use by applets
// Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc.
// Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message
int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
{
// Short circuit: don't store outgoing messages
if (getFrom(packet) == nodeDB->getNodeNum())
return 0;
// Short circuit: don't store "emoji reactions"
// Possibly some implementation of this in future?
if (packet->decoded.emoji)
return 0;
// Determine whether the message is broadcast or a DM
// Store this info to prevent confusion after a reboot
// Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set
inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to);
// Pick the appropriate variable to store the message in
MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast
? &inkhud->persistence->latestMessage.broadcast
: &inkhud->persistence->latestMessage.dm;
// Store nodenum of the sender
// Applets can use this to fetch user data from nodedb, if they want
storedMessage->sender = packet->from;
// Store the time (epoch seconds) when message received
storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
// Store the channel
// - (potentially) used to determine whether notification shows
// - (potentially) used to determine which applet to focus
storedMessage->channelIndex = packet->channel;
// Store the text
// Need to specify manually how many bytes, because source not null-terminated
storedMessage->text =
std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]);
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
}
#ifdef ARCH_ESP32
// Callback for lightSleepObserver
// Make sure the display is not partway through an update when we begin light sleep
// This is because some displays require active input from us to terminate the update process, and protect the panel hardware
int InkHUD::Events::beforeLightSleep(void *unused)
{
inkhud->awaitUpdate();
return 0; // No special status to report. Ignored anyway by this Observable
}
#endif
#endif

View File

@@ -0,0 +1,63 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#pragma once
/*
Handles non-specific events for InkHUD
Individual applets are responsible for listening for their own events via the module api etc,
however this class handles general events which concern InkHUD as a whole, e.g. shutdown
*/
#include "configuration.h"
#include "Observer.h"
#include "./InkHUD.h"
#include "./Persistence.h"
namespace NicheGraphics::InkHUD
{
class Events
{
public:
Events();
void begin();
void onButtonShort(); // User button: short press
void onButtonLong(); // User button: long press
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
private:
// For convenience
InkHUD *inkhud = nullptr;
Persistence::Settings *settings = nullptr;
// Get notified when the system is shutting down
CallbackObserver<Events, void *> deepSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeDeepSleep);
// Get notified when the system is rebooting
CallbackObserver<Events, void *> rebootObserver = CallbackObserver<Events, void *>(this, &Events::beforeReboot);
// Cache *incoming* text messages, for use by applets
CallbackObserver<Events, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
#ifdef ARCH_ESP32
// Get notified when the system is entering light sleep
CallbackObserver<Events, void *> lightSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeLightSleep);
#endif
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,218 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./InkHUD.h"
#include "./Applet.h"
#include "./Events.h"
#include "./Persistence.h"
#include "./Renderer.h"
#include "./SystemApplet.h"
#include "./Tile.h"
#include "./WindowManager.h"
using namespace NicheGraphics;
// Get or create the singleton
InkHUD::InkHUD *InkHUD::InkHUD::getInstance()
{
// Create the singleton instance of our class, if not yet done
static InkHUD *instance = nullptr;
if (!instance) {
instance = new InkHUD;
instance->persistence = new Persistence;
instance->windowManager = new WindowManager;
instance->renderer = new Renderer;
instance->events = new Events;
}
return instance;
}
// Connect the (fully set-up) E-Ink driver to InkHUD
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called
void InkHUD::InkHUD::setDriver(Drivers::EInk *driver)
{
renderer->setDriver(driver);
}
// Set the target number of FAST display updates in a row, before a FULL update is used for display health
// This value applies only to updates with an UNSPECIFIED update type
// If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many
// subsequent FULL updates will be performed, in an attempt to restore the display's health
void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
{
renderer->setDisplayResilience(fastPerFull, stressMultiplier);
}
// Register a user applet with InkHUD
// A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method
// Passing an applet to this method is all that is required to make it available to the user in your InkHUD build
void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile)
{
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
}
// Start InkHUD!
// Call this only after you have configured InkHUD
void InkHUD::InkHUD::begin()
{
persistence->loadSettings();
persistence->loadLatestMessage();
windowManager->begin();
events->begin();
renderer->begin();
// LogoApplet shows boot screen here
}
// Call this when your user button gets a short press
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
void InkHUD::InkHUD::shortpress()
{
events->onButtonShort();
}
// Call this when your user button gets a long press
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
void InkHUD::InkHUD::longpress()
{
events->onButtonLong();
}
// Cycle the next user applet to the foreground
// Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
void InkHUD::InkHUD::nextApplet()
{
windowManager->nextApplet();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
{
windowManager->openMenu();
}
// In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::nextTile()
{
windowManager->nextTile();
}
// Rotate the display image by 90 degrees
void InkHUD::InkHUD::rotate()
{
windowManager->rotate();
}
// Show / hide the battery indicator in top-right
void InkHUD::InkHUD::toggleBatteryIcon()
{
windowManager->toggleBatteryIcon();
}
// An applet asking for the display to be updated
// This does not occur immediately
// Instead, rendering is scheduled ASAP, for the next Renderer::runOnce call
// This allows multiple applets to observe the same event, and then share the same opportunity to update
// Applets should requestUpdate, whether or not they are currently displayed ("foreground")
// This is because they *might* be automatically brought to foreground by WindowManager::autoshow
void InkHUD::InkHUD::requestUpdate()
{
renderer->requestUpdate();
}
// Demand that the display be updated
// Ignores all diplomacy:
// - the display *will* update
// - the specified update type *will* be used
// If the async parameter is false, code flow is blocked while the update takes place
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async)
{
renderer->forceUpdate(type, async);
}
// Wait for any in-progress display update to complete before continuing
void InkHUD::InkHUD::awaitUpdate()
{
renderer->awaitUpdate();
}
// Ask the window manager to potentially bring a different user applet to foreground
// An applet will be brought to foreground if it has just received new and relevant info
// For Example: AllMessagesApplet has just received a new text message
// Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis
// If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event
void InkHUD::InkHUD::autoshow()
{
windowManager->autoshow();
}
// Tell the window manager that the Persistence::Settings value for applet activation has changed,
// and that it should reconfigure accordingly.
// This is triggered at boot, or when the user enables / disabled applets via the on-screen menu
void InkHUD::InkHUD::updateAppletSelection()
{
windowManager->changeActivatedApplets();
}
// Tell the window manager that the Persistence::Settings value for layout or rotation has changed,
// and that it should reconfigure accordingly.
// This is triggered at boot, or by rotate / layout options in the on-screen menu
void InkHUD::InkHUD::updateLayout()
{
windowManager->changeLayout();
}
// Width of the display, in the context of the current rotation
uint16_t InkHUD::InkHUD::width()
{
return renderer->width();
}
// Height of the display, in the context of the current rotation
uint16_t InkHUD::InkHUD::height()
{
return renderer->height();
}
// A collection of any user tiles which do not have a valid user applet
// This can occur in various situations, such as when a user enables fewer applets than their layout has tiles
// The tiles (and which regions the occupy) are private information of the window manager
// The renderer needs to know which regions (if any) are empty,
// in order to fill them with a "placeholder" pattern.
// -- There may be a tidier way to accomplish this --
std::vector<InkHUD::Tile *> InkHUD::InkHUD::getEmptyTiles()
{
return windowManager->getEmptyTiles();
}
// Get a system applet by its name
// This isn't particularly elegant, but it does avoid:
// - passing around a big set of references
// - having two sets of references (systemApplet vector for iteration)
InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name)
{
for (SystemApplet *sa : systemApplets) {
if (strcmp(name, sa->name) == 0)
return sa;
}
assert(false); // Invalid name
}
// Place a pixel into the image buffer
// The x and y coordinates are in the context of the current display rotation
// - Applets pass "relative" pixels to tiles
// - Tiles pass translated pixels to this method
// - this methods (Renderer) places rotated pixels into the image buffer
// This method provides the final formatting step required. The image buffer is suitable for writing to display
void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c)
{
renderer->handlePixel(x, y, c);
}
#endif

View File

@@ -0,0 +1,110 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
InkHUD's main class
- singleton
- mediator between the various components
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
#include "./AppletFont.h"
#include <vector>
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,
};
class Applet;
class Events;
class Persistence;
class Renderer;
class SystemApplet;
class Tile;
class WindowManager;
class InkHUD
{
public:
static InkHUD *getInstance(); // Access to this singleton class
// Configuration
// - before InkHUD::begin, in variant nicheGraphics.h,
void setDriver(Drivers::EInk *driver);
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
void begin();
// Handle user-button press
// - connected to an input source, in variant nicheGraphics.h
void shortpress();
void longpress();
// Trigger UI changes
// - called by various InkHUD components
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
void nextApplet();
void openMenu();
void nextTile();
void rotate();
void toggleBatteryIcon();
// Updating the display
// - called by various InkHUD components
void requestUpdate();
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
void awaitUpdate();
// (Re)configuring WindowManager
void autoshow(); // Bring an applet to foreground
void updateAppletSelection(); // Change which applets are active
void updateLayout(); // Change multiplexing (count, rotation)
// Information passed between components
uint16_t width(); // From E-Ink driver
uint16_t height(); // From E-Ink driver
std::vector<Tile *> getEmptyTiles(); // From WindowManager
// Applets
SystemApplet *getSystemApplet(const char *name);
std::vector<Applet *> userApplets;
std::vector<SystemApplet *> systemApplets;
// Pass drawing output to Renderer
void drawPixel(int16_t x, int16_t y, Color c);
// Shared data which persists between boots
Persistence *persistence = nullptr;
private:
InkHUD() {} // Constructor made private to force use of InkHUD::getInstance
Events *events = nullptr; // Handle non-specific firmware events
Renderer *renderer = nullptr; // Co-ordinate display updates
WindowManager *windowManager = nullptr; // Multiplexing of applets
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -31,7 +31,7 @@ class MessageStore
};
MessageStore() = delete;
MessageStore(std::string label); // Label determines filename in flash
explicit MessageStore(std::string label); // Label determines filename in flash
void saveToFlash();
void loadFromFlash();

View File

@@ -5,17 +5,21 @@
using namespace NicheGraphics;
// Load settings and latestMessage data
void InkHUD::loadDataFromFlash()
void InkHUD::Persistence::loadSettings()
{
// 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;
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 settings and latestMessage data
void InkHUD::Persistence::loadLatestMessage()
{
// Load previous "latestMessages" data from flash
MessageStore store("latest");
store.loadFromFlash();
@@ -32,12 +36,15 @@ void InkHUD::loadDataFromFlash()
}
}
// Save settings and latestMessage data
void InkHUD::saveDataToFlash()
// Save the InkHUD settings to flash
void InkHUD::Persistence::saveSettings()
{
// Save the InkHUD settings to flash
FlashData<Settings>::save(&settings, "settings");
}
// Save latestMessage data to flash
void InkHUD::Persistence::saveLatestMessage()
{
// Number of strings saved determines whether last message was broadcast or dm
MessageStore store("latest");
store.messages.push_back(latestMessage.dm);
@@ -46,14 +53,31 @@ void InkHUD::saveDataToFlash()
store.saveToFlash();
}
// Holds InkHUD settings while running
// Saved back to Flash at shutdown
// Accessed by including persistence.h
InkHUD::Settings InkHUD::settings;
/*
void InkHUD::Persistence::printSettings(Settings *settings)
{
if (SETTINGS_VERSION != 2)
LOG_WARN("Persistence::printSettings was written for SETTINGS_VERSION=2, current is %d", SETTINGS_VERSION);
// 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;
LOG_DEBUG("meta.version=%d", settings->meta.version);
LOG_DEBUG("userTiles.count=%d", settings->userTiles.count);
LOG_DEBUG("userTiles.maxCount=%d", settings->userTiles.maxCount);
LOG_DEBUG("userTiles.focused=%d", settings->userTiles.focused);
for (uint8_t i = 0; i < MAX_TILES_GLOBAL; i++)
LOG_DEBUG("userTiles.displayedUserApplet[%d]=%d", i, settings->userTiles.displayedUserApplet[i]);
for (uint8_t i = 0; i < MAX_USERAPPLETS_GLOBAL; i++)
LOG_DEBUG("userApplets.active[%d]=%d", i, settings->userApplets.active[i]);
for (uint8_t i = 0; i < MAX_USERAPPLETS_GLOBAL; i++)
LOG_DEBUG("userApplets.autoshow[%d]=%d", i, settings->userApplets.autoshow[i]);
LOG_DEBUG("optionalFeatures.notifications=%d", settings->optionalFeatures.notifications);
LOG_DEBUG("optionalFeatures.batteryIcon=%d", settings->optionalFeatures.batteryIcon);
LOG_DEBUG("optionalMenuItems.nextTile=%d", settings->optionalMenuItems.nextTile);
LOG_DEBUG("optionalMenuItems.backlight=%d", settings->optionalMenuItems.backlight);
LOG_DEBUG("tips.firstBoot=%d", settings->tips.firstBoot);
LOG_DEBUG("tips.safeShutdownSeen=%d", settings->tips.safeShutdownSeen);
LOG_DEBUG("rotation=%d", settings->rotation);
LOG_DEBUG("recentlyActiveSeconds=%d", settings->recentlyActiveSeconds);
}
*/
#endif

View File

@@ -14,110 +14,119 @@ The save / load mechanism is a shared NicheGraphics feature.
#include "configuration.h"
#include "./InkHUD.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;
class Persistence
{
public:
static constexpr uint8_t MAX_TILES_GLOBAL = 4;
static 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;
// Used to invalidate old settings, if needed
// Version 0 is reserved for testing, and will always load defaults
static 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 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;
struct UserTiles {
// How many tiles are shown
uint8_t count = 1;
// Maximum amount of tiles for this display
uint8_t maxCount = 4;
// 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 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;
// Which applet is displayed on which tile
// Index of array: which tile, as indexed in WindowManager::userTiles
// Value of array: which applet, as indexed in InkHUD::userApplets
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];
struct UserApplets {
// Which applets are running (either displayed, or in the background)
// Index of array: which applet, as indexed in InkHUD::userApplets
// Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method
bool active[MAX_USERAPPLETS_GLOBAL]{false};
// 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;
// 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 InkHUD::addApplets, in setupNicheGraphics method
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;
// Features which the user 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;
// 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 no 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;
// 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;
// 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;
// User is advised to shut down 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;
// 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;
// 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 courtesy 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
};
void loadSettings();
void saveSettings();
void loadLatestMessage();
void saveLatestMessage();
// void printSettings(Settings *settings); // Debugging use only
Settings settings;
LatestMessage latestMessage;
};
// 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

View File

@@ -1,5 +1,7 @@
[inkhud]
build_src_filter = +<../variants/$PIOENV> +<graphics/niche/>; Include nicheGraphics.h
build_src_filter =
+<graphics/niche/>; Include the nicheGraphics directory
+<../variants/$PIOENV>; Include nicheGraphics.h from our variant folder
build_flags =
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI)

View File

@@ -0,0 +1,412 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Renderer.h"
#include "main.h"
#include "./Applet.h"
#include "./SystemApplet.h"
#include "./Tile.h"
using namespace NicheGraphics;
InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer")
{
// Nothing for the timer to do just yet
OSThread::disable();
// Convenient references
inkhud = InkHUD::getInstance();
settings = &inkhud->persistence->settings;
}
// Connect the (fully set-up) E-Ink driver to InkHUD
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called
void InkHUD::Renderer::setDriver(Drivers::EInk *driver)
{
// Make sure not already set
if (this->driver) {
LOG_ERROR("Driver already set");
delay(2000); // Wait for native serial..
assert(false);
}
// Store the driver which was created in setupNicheGraphics()
this->driver = driver;
// Determine the dimensions 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.
imageBufferWidth = ((driver->width - 1) / 8) + 1;
imageBufferHeight = driver->height;
// Allocate the image buffer
imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight];
}
// Set the target number of FAST display updates in a row, before a FULL update is used for display health
// This value applies only to updates with an UNSPECIFIED update type
// If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many
// subsequent FULL updates will be performed, in an attempt to restore the display's health
void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
{
displayHealth.fastPerFull = fastPerFull;
displayHealth.stressMultiplier = stressMultiplier;
}
void InkHUD::Renderer::begin()
{
forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
}
// Set a flag, which will be picked up by runOnce, ASAP.
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
void InkHUD::Renderer::requestUpdate()
{
requested = true;
// We will run the thread as soon as we loop(),
// after all Applets have had a chance to observe whatever event set this off
OSThread::setIntervalFromNow(0);
OSThread::enabled = true;
runASAP = true;
}
// requestUpdate will not actually update if no requests were made by applets which are actually visible
// This can occur, because applets requestUpdate even from the background,
// in case the user's autoshow settings permit them to be moved to foreground.
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
// Display health, for example.
// In these situations, we use forceUpdate
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async)
{
requested = true;
forced = true;
displayHealth.forceUpdateType(type);
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
if (async) {
// We will run the thread as soon as we loop(),
// after all Applets have had a chance to observe whatever event set this off
OSThread::setIntervalFromNow(0);
OSThread::enabled = true;
runASAP = true;
}
// If the update is *not* asynchronous, we begin the render process directly here
// so that it can block code flow while running
else
render(false);
}
// Wait for any in-progress display update to complete before continuing
void InkHUD::Renderer::awaitUpdate()
{
if (driver->busy()) {
LOG_INFO("Waiting for display");
driver->await(); // Wait here for update to complete
}
}
// Set a ready-to-draw pixel into the image buffer
// All rotations / translations have already taken place: this buffer data is formatted ready for the driver
void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c)
{
rotatePixelCoords(&x, &y);
uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte.
bitWrite(imageBuffer[byteNum], bitNum, c);
}
// Width of the display, relative to rotation
uint16_t InkHUD::Renderer::width()
{
if (settings->rotation % 2)
return driver->height;
else
return driver->width;
}
// Height of the display, relative to rotation
uint16_t InkHUD::Renderer::height()
{
if (settings->rotation % 2)
return driver->width;
else
return driver->height;
}
// Runs at regular intervals
// - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render
// - queuing another render: while one is already is progress
int32_t InkHUD::Renderer::runOnce()
{
// If an applet asked to render, and hardware is able, lets try now
if (requested && !driver->busy()) {
render();
}
// If our render() call failed, try again shortly
// otherwise, stop our thread until next update due
if (requested)
return 250UL;
else
return OSThread::disable();
}
// Applies the system-wide rotation to pixel positions
// This step is applied to image data which has already been translated by a Tile object
// This is the final step before the pixel is placed into the image buffer
// No return: values of the *x and *y parameters are modified by the method
void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y)
{
// Apply a global rotation to pixel locations
int16_t x1 = 0;
int16_t y1 = 0;
switch (settings->rotation) {
case 0:
x1 = *x;
y1 = *y;
break;
case 1:
x1 = (driver->width - 1) - *y;
y1 = *x;
break;
case 2:
x1 = (driver->width - 1) - *x;
y1 = (driver->height - 1) - *y;
break;
case 3:
x1 = *y;
y1 = (driver->height - 1) - *x;
break;
}
*x = x1;
*y = y1;
}
// Make an attempt to gather image data from some / all applets, and update the display
// Might not be possible right now, if update already is progress.
void InkHUD::Renderer::render(bool async)
{
// Make sure the display is ready for a new update
if (async) {
// Previous update still running, Will try again shortly, via runOnce()
if (driver->busy())
return;
} else {
// Wait here for previous update to complete
driver->await();
}
// Determine if a system applet has requested exclusive rights to request an update,
// or exclusive rights to render
checkLocks();
// (Potentially) change applet to display new info,
// then check if this newly displayed applet makes a pending notification redundant
inkhud->autoshow();
// If an update is justified.
// We don't know this until after autoshow has run, as new applets may now be in foreground
if (shouldUpdate()) {
// Decide which technique the display will use to change image
// Done early, as rendering resets the Applets' requested types
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
// Render the new image
clearBuffer();
renderUserApplets();
renderPlaceholders();
renderSystemApplets();
// Tell display to begin process of drawing new image
LOG_INFO("Updating display");
driver->update(imageBuffer, updateType);
// If not async, wait here until the update is complete
if (!async)
driver->await();
}
// Our part is done now.
// If update is async, the display hardware is still performing the update process,
// but that's all handled by NicheGraphics::Drivers::EInk
// Tidy up, ready for a new request
requested = false;
forced = false;
}
// Manually fill the image buffer with WHITE
// Clears any old drawing
// Note: benchmarking revealed that this is *much* faster than setting pixels individually
// So much so that it's more efficient to re-render all applets,
// rather than rendering selectively, and manually blanking a portion of the display
void InkHUD::Renderer::clearBuffer()
{
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
}
void InkHUD::Renderer::checkLocks()
{
lockRendering = nullptr;
lockRequests = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (!lockRendering && sa->lockRendering && sa->isForeground()) {
lockRendering = sa;
}
if (!lockRequests && sa->lockRequests && sa->isForeground()) {
lockRequests = sa;
}
}
}
bool InkHUD::Renderer::shouldUpdate()
{
bool should = false;
// via forceUpdate
should |= forced;
// via a system applet (which has locked update requests)
if (lockRequests) {
should |= lockRequests->wantsToRender();
return should; // Early exit - no other requests considered
}
// via system applet (not locked)
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->wantsToRender() // This applet requested
&& sa->isForeground()) // This applet is currently shown
{
should = true;
break;
}
}
// via user applet
for (Applet *ua : inkhud->userApplets) {
if (ua // Tile has valid applet
&& ua->wantsToRender() // This applet requested display update
&& ua->isForeground()) // This applet is currently shown
{
should = true;
break;
}
}
return should;
}
// Determine which type of E-Ink update the display will perform, to change the image.
// Considers the needs of the various applets, then weighs against display health.
// An update type specified by forceUpdate will be granted with no further questioning.
Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
{
// Ask applets which update type they would prefer
// Some update types take priority over others
// No need to consider the "requests" if somebody already forced an update
if (!forced) {
// User applets
for (Applet *ua : inkhud->userApplets) {
if (ua && ua->isForeground())
displayHealth.requestUpdateType(ua->wantsUpdateType());
}
// System Applets
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa && sa->isForeground())
displayHealth.requestUpdateType(sa->wantsUpdateType());
}
}
return displayHealth.decideUpdateType();
}
// Run the drawing operations of any user applets which are currently displayed
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
void InkHUD::Renderer::renderUserApplets()
{
// Don't render user applets if a system applet has demanded the whole display to itself
if (lockRendering)
return;
// Render any user applets which are currently visible
for (Applet *ua : inkhud->userApplets) {
if (ua && ua->isActive() && ua->isForeground()) {
uint32_t start = millis();
ua->render(); // Draw!
uint32_t stop = millis();
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
}
}
}
// Run the drawing operations of any system applets which are currently displayed
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
void InkHUD::Renderer::renderSystemApplets()
{
SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon");
SystemApplet *menu = inkhud->getSystemApplet("Menu");
SystemApplet *notifications = inkhud->getSystemApplet("Notification");
// Each system applet
for (SystemApplet *sa : inkhud->systemApplets) {
// Skip if not shown
if (!sa->isForeground())
continue;
// Skip if locked by another applet
if (lockRendering && lockRendering != sa)
continue;
// Don't draw the battery or notifications overtop the menu
// Todo: smarter way to handle this
if (menu->isForeground() && (sa == battery || sa == notifications))
continue;
assert(sa->getTile());
// uint32_t start = millis();
sa->render(); // Draw!
// uint32_t stop = millis();
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
}
}
// In some situations (e.g. layout or applet selection changes),
// a user tile can end up without an assigned applet.
// In this case, we will fill the empty space with diagonal lines.
void InkHUD::Renderer::renderPlaceholders()
{
// Don't fill empty space with placeholders if a system applet wants exclusive use of the display
if (lockRendering)
return;
// Ask the window manager which tiles are empty
std::vector<Tile *> emptyTiles = inkhud->getEmptyTiles();
// No empty tiles
if (emptyTiles.size() == 0)
return;
SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder");
// uint32_t start = millis();
for (Tile *t : emptyTiles) {
t->assignApplet(placeholder);
placeholder->render();
t->assignApplet(nullptr);
}
// uint32_t stop = millis();
// LOG_DEBUG("Placeholders took %dms to render", stop - start);
}
#endif

View File

@@ -0,0 +1,96 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Orchestrates updating of the display image
- takes requests (or demands) for display update
- performs the various steps of the rendering operation
- interfaces with the E-Ink driver
*/
#pragma once
#include "configuration.h"
#include "./DisplayHealth.h"
#include "./InkHUD.h"
#include "./Persistence.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
namespace NicheGraphics::InkHUD
{
class Renderer : protected concurrency::OSThread
{
public:
Renderer();
// Configuration, before begin
void setDriver(Drivers::EInk *driver);
void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier);
void begin();
// Call these to make the image change
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
// Wait for an update to complete
void awaitUpdate();
// Receives pixel output from an applet (via a tile, which translates the coordinates)
void handlePixel(int16_t x, int16_t y, Color c);
// Size of display, in context of current rotation
uint16_t width();
uint16_t height();
private:
// Make attemps to render / update, once triggered by requestUpdate or forceUpdate
int32_t runOnce() override;
// Apply the display rotation to handled pixels
void rotatePixelCoords(int16_t *x, int16_t *y);
// Execute the render process now, then hand off to driver for display update
void render(bool async = true);
// Steps of the rendering process
void clearBuffer();
void checkLocks();
bool shouldUpdate();
Drivers::EInk::UpdateTypes decideUpdateType();
void renderUserApplets();
void renderSystemApplets();
void renderPlaceholders();
Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware
DisplayHealth displayHealth; // Manages display health by controlling type of update
uint8_t *imageBuffer = nullptr; // Fed into driver
uint16_t imageBufferHeight = 0;
uint16_t imageBufferWidth = 0;
uint32_t imageBufferSize = 0; // Bytes
SystemApplet *lockRendering = nullptr; // Render this applet *only*
SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only*
bool requested = false;
bool forced = false;
// For convenience
InkHUD *inkhud = nullptr;
Persistence::Settings *settings = nullptr;
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -0,0 +1,41 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
An applet with nonstandard behavior, which will require special handling
For features like the menu, and the battery icon.
*/
#pragma once
#include "configuration.h"
#include "./Applet.h"
namespace NicheGraphics::InkHUD
{
class SystemApplet : public Applet
{
public:
// System applets have the right to:
bool handleInput = false; // - respond to input from the user button
bool lockRendering = false; // - prevent other applets from being rendered during an update
bool lockRequests = false; // - prevent other applets from triggering display updates
// Other system applets may take precedence over our own system applet though
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank)
private:
// System applets are always running (active), but may not be visible (foreground)
void onActivate() override {}
void onDeactivate() override {}
};
}; // namespace NicheGraphics::InkHUD
#endif

View File

@@ -18,7 +18,7 @@ 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
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
return taskHighlight->disable();
}
static void inittaskHighlight()
@@ -33,21 +33,30 @@ static void inittaskHighlight()
InkHUD::Tile::Tile()
{
// For convenince
windowManager = InkHUD::WindowManager::getInstance();
inkhud = InkHUD::getInstance();
inittaskHighlight();
Tile::highlightTarget = nullptr;
Tile::highlightShown = false;
}
InkHUD::Tile::Tile(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;
}
// 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)
void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex)
{
uint16_t displayWidth = windowManager->getWidth();
uint16_t displayHeight = windowManager->getHeight();
uint16_t displayWidth = inkhud->width();
uint16_t displayHeight = inkhud->height();
bool landscape = displayWidth > displayHeight;
@@ -62,10 +71,9 @@ void InkHUD::Tile::placeUserTile(uint8_t userTileCount, uint8_t tileIndex)
return;
}
// Todo: special handling for the notification area
// Todo: special handling for 3 tile layout
// Gap between tiles
// Gutters between tiles
const uint16_t spacing = 4;
switch (userTileCount) {
@@ -124,17 +132,12 @@ void InkHUD::Tile::placeUserTile(uint8_t userTileCount, uint8_t tileIndex)
}
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)
void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height)
{
assert(width > 0 && height > 0);
@@ -182,31 +185,32 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
// 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);
// Pass to the renderer
inkhud->drawPixel(x, y, c);
}
}
// Called by Applet base class, when learning of its dimensions
// Called by Applet base class, when setting applet dimensions, immediately before render
uint16_t InkHUD::Tile::getWidth()
{
return width;
}
// Called by Applet base class, when learning of its dimensions
// Called by Applet base class, when setting applet dimensions, immediately before render
uint16_t InkHUD::Tile::getHeight()
{
return height;
}
// Longest edge of the display, in pixels
// A 296px x 250px display will return 296, for example
// Maximum possible size of any tile's width / height
// Used by some components to allocate resources for the "worst possible situtation"
// Used by some components to allocate resources for the "worst possible situation"
// "Sizing the cathedral for christmas eve"
uint16_t InkHUD::Tile::maxDisplayDimension()
{
WindowManager *wm = WindowManager::getInstance();
return max(wm->getHeight(), wm->getWidth());
InkHUD *inkhud = InkHUD::getInstance();
return max(inkhud->height(), inkhud->width());
}
// Ask for this tile to be highlighted
@@ -216,7 +220,7 @@ void InkHUD::Tile::requestHighlight()
{
Tile::highlightTarget = this;
Tile::highlightShown = false;
windowManager->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first

View File

@@ -14,47 +14,44 @@
#include "configuration.h"
#include "./Applet.h"
#include "./Types.h"
#include "./WindowManager.h"
#include <GFX.h>
#include "./InkHUD.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
Tile(int16_t left, int16_t top, uint16_t width, uint16_t height);
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
void setRegion(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();
uint16_t getHeight();
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 assignApplet(Applet *a); // Link an applet with this tile
Applet *getAssignedApplet(); // Applet which is currently linked with this 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
static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss
protected:
int16_t left;
int16_t top;
uint16_t width;
uint16_t height;
private:
InkHUD *inkhud = nullptr;
int16_t left = 0;
int16_t top = 0;
uint16_t width = 0;
uint16_t height = 0;
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

View File

@@ -1,62 +0,0 @@
#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

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,7 @@
/*
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)
Responsible for managing which applets are shown, and their sizes / positions
*/
@@ -16,48 +10,47 @@
#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 "./Applets/System/Notification/Notification.h" // The notification object, not the applet
#include "./InkHUD.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
class WindowManager
{
public:
static WindowManager *getInstance(); // Get or create singleton instance
WindowManager();
void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile);
void begin();
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)
// - call these to make stuff change
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 nextTile();
void openMenu();
void nextApplet();
void rotate();
void toggleBatteryIcon();
// - call these to manifest changes already made to the relevant Persistence::Settings values
void changeLayout(); // Change tile layout or count
void changeActivatedApplets(); // Change which applets are activated
// - called during the rendering operation
void autoshow(); // Show a different applet, to display new info
std::vector<Tile *> getEmptyTiles(); // Any user tiles without a valid applet
private:
// Steps for configuring (or reconfiguring) the window manager
// - all steps required at startup
// - various combinations of steps required for on-the-fly reconfiguration (by user, via menu)
void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile);
void createSystemApplets(); // Instantiate the system applets
void placeSystemTiles(); // Assign manual positions to (most) system applets
void createUserApplets(); // Activate user's selected applets
void createUserTiles(); // Instantiate enough tiles for user's selected layout
@@ -65,113 +58,15 @@ class WindowManager : protected concurrency::OSThread
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);
std::vector<Tile *> userTiles; // Tiles which can host user applets
// 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
// For convenience
InkHUD *inkhud = nullptr;
Persistence::Settings *settings = nullptr;
};
}; // namespace NicheGraphics::InkHUD
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -18,6 +18,10 @@ TwoButton::TwoButton() : concurrency::OSThread("TwoButton")
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
#endif
// Explicitly initialize these, just to keep cppcheck quiet..
buttons[0] = Button();
buttons[1] = Button();
}
// Get access to (or create) the singleton instance of this class
@@ -185,7 +189,7 @@ int32_t TwoButton::runOnce()
// 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].onDown(); // Run callback: 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;
@@ -197,17 +201,17 @@ int32_t TwoButton::runOnce()
// 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].onUp(); // Run callback: 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 (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
buttons[i].onShortPress(); // Run callback: short press
}
// 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
// Run callback: long press (once)
// Then continue waiting for release, to rearm
buttons[i].state = State::POLLING_FIRED;
buttons[i].onLongPress();
@@ -222,7 +226,7 @@ int32_t TwoButton::runOnce()
// 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)
buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
}
// Not yet released, keep polling
else

View File

@@ -6,7 +6,7 @@
// InkHUD-specific components
// ---------------------------
#include "graphics/niche/InkHUD/WindowManager.h"
#include "graphics/niche/InkHUD/InkHUD.h"
// Applets
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
@@ -49,30 +49,29 @@ void setupNicheGraphics()
// InkHUD
// ----------------------------
InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance();
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
// Set the driver
windowManager->setDriver(driver);
inkhud->setDriver(driver);
// Set how many FAST updates per FULL update
// Set how unhealthy additional FAST updates beyond this number are
windowManager->setDisplayResilience(10, 1.5);
inkhud->setDisplayResilience(10, 1.5);
// Prepare fonts
InkHUD::AppletFont largeFont(FreeSans9pt7b);
InkHUD::AppletFont smallFont(FreeSans6pt7b);
InkHUD::Applet::fontLarge = InkHUD::AppletFont(FreeSans9pt7b);
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt7b);
/*
// Font localization demo: Cyrillic
InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic);
smallFont.addSubstitutionsWin1251();
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt8bCyrillic);
InkHUD::Applet::fontSmall.addSubstitutionsWin1251();
*/
InkHUD::Applet::setDefaultFonts(largeFont, smallFont);
// Init settings, and customize defaults
InkHUD::settings.userTiles.maxCount = 2; // How many tiles can the display handle?
InkHUD::settings.rotation = 3; // 270 degrees clockwise
InkHUD::settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
InkHUD::settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise
inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
// Pick applets
// Note: order of applets determines priority of "auto-show" feature
@@ -80,18 +79,18 @@ void setupNicheGraphics()
// - is activated?
// - is autoshown?
// - is foreground on a specific tile (index)?
windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
windowManager->addApplet("DMs", new InkHUD::DMApplet);
windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet);
windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
// windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet);
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
// Start running window manager
windowManager->begin();
// Start running InkHUD
inkhud->begin();
// Buttons
// --------------------------
@@ -102,13 +101,13 @@ void setupNicheGraphics()
// Setup the main user button
buttons->setWiring(MAIN_BUTTON, BUTTON_PIN);
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); });
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::InkHUD::getInstance()->longpress(); });
// Setup the aux button
// Bonus feature of VME213
buttons->setWiring(AUX_BUTTON, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(AUX_BUTTON, []() { InkHUD::WindowManager::getInstance()->nextTile(); });
buttons->setHandlerShortPress(AUX_BUTTON, []() { InkHUD::InkHUD::getInstance()->nextTile(); });
buttons->start();
}

View File

@@ -30,8 +30,8 @@ build_flags =
${inkhud.build_flags}
-I variants/heltec_vision_master_e213
-D HELTEC_VISION_MASTER_E213
-D MAX_THREADS=40
-D MAX_THREADS=40 ; Required if used with WiFi
lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${esp32s3_base.lib_deps}
upload_speed = 115200
upload_speed = 921600

View File

@@ -62,30 +62,29 @@ void setupNicheGraphics()
// InkHUD
// ----------------------------
InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance();
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
// Set the driver
windowManager->setDriver(driver);
inkhud->setDriver(driver);
// Set how many FAST updates per FULL update
// Set how unhealthy additional FAST updates beyond this number are
windowManager->setDisplayResilience(7, 1.5);
inkhud->setDisplayResilience(7, 1.5);
// Prepare fonts
InkHUD::AppletFont largeFont(FreeSans9pt7b);
InkHUD::AppletFont smallFont(FreeSans6pt7b);
InkHUD::Applet::fontLarge = InkHUD::AppletFont(FreeSans9pt7b);
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt7b);
/*
// Font localization demo: Cyrillic
InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic);
smallFont.addSubstitutionsWin1251();
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt8bCyrillic);
InkHUD::Applet::fontSmall.addSubstitutionsWin1251();
*/
InkHUD::Applet::setDefaultFonts(largeFont, smallFont);
// Init settings, and customize defaults
InkHUD::settings.userTiles.maxCount = 2; // How many tiles can the display handle?
InkHUD::settings.rotation = 1; // 90 degrees clockwise
InkHUD::settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
InkHUD::settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise
inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
// Pick applets
// Note: order of applets determines priority of "auto-show" feature
@@ -93,35 +92,33 @@ void setupNicheGraphics()
// - is activated?
// - is autoshown?
// - is foreground on a specific tile (index)?
windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
windowManager->addApplet("DMs", new InkHUD::DMApplet);
windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet);
windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
// windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet);
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
// Start running window manager
windowManager->begin();
// Start running InkHUD
inkhud->begin();
// Buttons
// --------------------------
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component
constexpr uint8_t MAIN_BUTTON = 0;
constexpr uint8_t AUX_BUTTON = 1;
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component
// Setup the main user button
buttons->setWiring(MAIN_BUTTON, BUTTON_PIN);
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); });
// Setup the main user button (0)
buttons->setWiring(0, BUTTON_PIN);
buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); });
// Setup the aux button
// Setup the aux button (1)
// Bonus feature of VME290
buttons->setWiring(AUX_BUTTON, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(AUX_BUTTON, []() { InkHUD::WindowManager::getInstance()->nextTile(); });
buttons->setWiring(1, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); });
buttons->start();
}

View File

@@ -34,8 +34,8 @@ build_flags =
${inkhud.build_flags}
-I variants/heltec_vision_master_e290
-D HELTEC_VISION_MASTER_E290
-D MAX_THREADS=40
-D MAX_THREADS=40 ; Required if used with WiFi
lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${esp32s3_base.lib_deps}
upload_speed = 115200
upload_speed = 921600

View File

@@ -6,7 +6,7 @@
// InkHUD-specific components
// ---------------------------
#include "graphics/niche/InkHUD/WindowManager.h"
#include "graphics/niche/InkHUD/InkHUD.h"
// Applets
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
@@ -49,29 +49,28 @@ void setupNicheGraphics()
// InkHUD
// ----------------------------
InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance();
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
// Set the driver
windowManager->setDriver(driver);
inkhud->setDriver(driver);
// Set how many FAST updates per FULL update
// Set how unhealthy additional FAST updates beyond this number are
windowManager->setDisplayResilience(10, 1.5);
inkhud->setDisplayResilience(10, 1.5);
// Prepare fonts
InkHUD::AppletFont largeFont(FreeSans9pt7b);
InkHUD::AppletFont smallFont(FreeSans6pt7b);
InkHUD::Applet::fontLarge = InkHUD::AppletFont(FreeSans9pt7b);
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt7b);
/*
// Font localization demo: Cyrillic
InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic);
smallFont.addSubstitutionsWin1251();
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt8bCyrillic);
InkHUD::Applet::fontSmall.addSubstitutionsWin1251();
*/
InkHUD::Applet::setDefaultFonts(largeFont, smallFont);
// Init settings, and customize defaults
InkHUD::settings.userTiles.maxCount = 2; // How many tiles can the display handle?
InkHUD::settings.rotation = 3; // 270 degrees clockwise
InkHUD::settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise
inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
// Pick applets
// Note: order of applets determines priority of "auto-show" feature
@@ -79,18 +78,18 @@ void setupNicheGraphics()
// - is activated?
// - is autoshown?
// - is foreground on a specific tile (index)?
windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
windowManager->addApplet("DMs", new InkHUD::DMApplet);
windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet);
windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
// windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet);
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
// Start running window manager
windowManager->begin();
// Start running InkHUD
inkhud->begin();
// Buttons
// --------------------------
@@ -100,8 +99,8 @@ void setupNicheGraphics()
// Setup the main user button
buttons->setWiring(MAIN_BUTTON, BUTTON_PIN);
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); });
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::InkHUD::getInstance()->longpress(); });
// No aux button on this board

View File

@@ -31,8 +31,8 @@ build_flags =
${inkhud.build_flags}
-I variants/heltec_wireless_paper
-D HELTEC_WIRELESS_PAPER
-D MAX_THREADS=40
-D MAX_THREADS=40 ; Required if used with WiFi
lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${esp32s3_base.lib_deps}
upload_speed = 115200
upload_speed = 921600

View File

@@ -6,7 +6,7 @@
// InkHUD-specific components
// ---------------------------
#include "graphics/niche/InkHUD/WindowManager.h"
#include "graphics/niche/InkHUD/InkHUD.h"
// Applets
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
@@ -50,31 +50,30 @@ void setupNicheGraphics()
// InkHUD
// ----------------------------
InkHUD::WindowManager *windowManager = InkHUD::WindowManager::getInstance();
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
// Set the driver
windowManager->setDriver(driver);
inkhud->setDriver(driver);
// Set how many FAST updates per FULL update
// Set how unhealthy additional FAST updates beyond this number are
windowManager->setDisplayResilience(20, 1.5);
inkhud->setDisplayResilience(20, 1.5);
// Prepare fonts
InkHUD::AppletFont largeFont(FreeSans9pt7b);
InkHUD::AppletFont smallFont(FreeSans6pt7b);
InkHUD::Applet::fontLarge = InkHUD::AppletFont(FreeSans9pt7b);
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt7b);
/*
// Font localization demo: Cyrillic
InkHUD::AppletFont smallFont(FreeSans6pt8bCyrillic);
smallFont.addSubstitutionsWin1251();
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt8bCyrillic);
InkHUD::Applet::fontSmall.addSubstitutionsWin1251();
*/
InkHUD::Applet::setDefaultFonts(largeFont, smallFont);
// Init settings, and customize defaults
// Values ignored individually if found saved to flash
InkHUD::settings.userTiles.maxCount = 2; // Two applets side-by-side
InkHUD::settings.rotation = 3; // 270 degrees clockwise
InkHUD::settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
InkHUD::settings.optionalMenuItems.backlight = true; // Until proven (by touch) that user still has the capacitive button
inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side
inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise
inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it
// Setup backlight
// Note: AUX button behavior configured further down
@@ -83,30 +82,32 @@ void setupNicheGraphics()
// Pick applets
// Note: order of applets determines priority of "auto-show" feature
windowManager->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
windowManager->addApplet("DMs", new InkHUD::DMApplet);
windowManager->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
windowManager->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
windowManager->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
windowManager->addApplet("Recents List", new InkHUD::RecentsListApplet);
windowManager->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0
// windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
// windowManager->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet);
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
// Start running window manager
windowManager->begin();
// Start running InkHUD
inkhud->begin();
// Buttons
// --------------------------
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component
// (To improve code readability only)
constexpr uint8_t MAIN_BUTTON = 0;
constexpr uint8_t TOUCH_BUTTON = 1;
// Setup the main user button
buttons->setWiring(MAIN_BUTTON, BUTTON_PIN, LOW);
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonShort(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::WindowManager::getInstance()->handleButtonLong(); });
buttons->setHandlerShortPress(MAIN_BUTTON, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(MAIN_BUTTON, []() { InkHUD::InkHUD::getInstance()->longpress(); });
// Setup the capacitive touch button
// - short: momentary backlight
@@ -115,7 +116,8 @@ void setupNicheGraphics()
buttons->setTiming(TOUCH_BUTTON, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC
buttons->setHandlerDown(TOUCH_BUTTON, [backlight]() {
backlight->peek();
InkHUD::settings.optionalMenuItems.backlight = false; // We've proved user still has the button. No need for menu entry.
InkHUD::InkHUD::getInstance()->persistence->settings.optionalMenuItems.backlight =
false; // We've proved user still has the button. No need to make backlight togglable via the menu.
});
buttons->setHandlerLongPress(TOUCH_BUTTON, [backlight]() { backlight->latch(); });
buttons->setHandlerShortPress(TOUCH_BUTTON, [backlight]() { backlight->off(); });

View File

@@ -39,6 +39,6 @@ build_src_filter =
${inkhud.build_src_filter}
+<../variants/t-echo>
lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot intead of AdafruitGFX
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${nrf52840_base.lib_deps}
lewisxhe/PCF8563_Library@^1.0.1