mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-05 17:40:51 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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()`
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,8 +23,6 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : cha
|
||||
|
||||
void InkHUD::ThreadedMessageApplet::onRender()
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
// =============
|
||||
// Draw a header
|
||||
// =============
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
179
src/graphics/niche/InkHUD/Events.cpp
Normal file
179
src/graphics/niche/InkHUD/Events.cpp
Normal 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(¬ifyDeepSleep);
|
||||
rebootObserver.observe(¬ifyReboot);
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
#ifdef ARCH_ESP32
|
||||
lightSleepObserver.observe(¬ifyLightSleep);
|
||||
#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
|
||||
63
src/graphics/niche/InkHUD/Events.h
Normal file
63
src/graphics/niche/InkHUD/Events.h
Normal 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
|
||||
218
src/graphics/niche/InkHUD/InkHUD.cpp
Normal file
218
src/graphics/niche/InkHUD/InkHUD.cpp
Normal 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
|
||||
110
src/graphics/niche/InkHUD/InkHUD.h
Normal file
110
src/graphics/niche/InkHUD/InkHUD.h
Normal 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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
412
src/graphics/niche/InkHUD/Renderer.cpp
Normal file
412
src/graphics/niche/InkHUD/Renderer.cpp
Normal 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
|
||||
96
src/graphics/niche/InkHUD/Renderer.h
Normal file
96
src/graphics/niche/InkHUD/Renderer.h
Normal 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
|
||||
41
src/graphics/niche/InkHUD/SystemApplet.h
Normal file
41
src/graphics/niche/InkHUD/SystemApplet.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -18,6 +18,10 @@ TwoButton::TwoButton() : concurrency::OSThread("TwoButton")
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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(); });
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user