From e6a98b1d6b953a312cb892bfdbd5ed6872561f8f Mon Sep 17 00:00:00 2001 From: todd-herbert Date: Thu, 6 Mar 2025 23:25:41 +1300 Subject: [PATCH] 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 --- src/BluetoothStatus.h | 4 +- .../Drivers/Backlight/LatchingBacklight.cpp | 8 +- src/graphics/niche/Drivers/EInk/EInk.cpp | 2 +- src/graphics/niche/Drivers/EInk/EInk.h | 6 +- .../niche/Drivers/EInk/GDEY0154D67.cpp | 2 +- .../niche/Drivers/EInk/LCMEN2R13EFC1.cpp | 8 +- .../niche/Drivers/EInk/LCMEN2R13EFC1.h | 19 +- src/graphics/niche/Drivers/EInk/README.md | 11 +- src/graphics/niche/Drivers/EInk/SSD16XX.cpp | 9 +- src/graphics/niche/Drivers/EInk/SSD16XX.h | 19 +- src/graphics/niche/Drivers/README.md | 2 +- src/graphics/niche/FlashData.h | 2 +- .../niche/Fonts/FreeSans6pt8bCyrillic.h | 2 +- src/graphics/niche/InkHUD/Applet.cpp | 211 ++- src/graphics/niche/InkHUD/Applet.h | 200 +-- src/graphics/niche/InkHUD/AppletFont.cpp | 21 +- src/graphics/niche/InkHUD/AppletFont.h | 10 +- .../InkHUD/Applets/Bases/Map/MapApplet.cpp | 17 +- .../InkHUD/Applets/Bases/Map/MapApplet.h | 9 +- .../Applets/Bases/NodeList/NodeListApplet.cpp | 32 +- .../Applets/Bases/NodeList/NodeListApplet.h | 29 +- .../NewMsgExample/NewMsgExampleApplet.cpp | 2 - .../NewMsgExample/NewMsgExampleApplet.h | 2 +- .../System/BatteryIcon/BatteryIconApplet.cpp | 12 +- .../System/BatteryIcon/BatteryIconApplet.h | 12 +- .../InkHUD/Applets/System/Logo/LogoApplet.cpp | 68 +- .../InkHUD/Applets/System/Logo/LogoApplet.h | 13 +- .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 109 +- .../InkHUD/Applets/System/Menu/MenuApplet.h | 14 +- .../Notification/NotificationApplet.cpp | 56 +- .../System/Notification/NotificationApplet.h | 18 +- .../Applets/System/Pairing/PairingApplet.cpp | 37 +- .../Applets/System/Pairing/PairingApplet.h | 10 +- .../System/Placeholder/PlaceholderApplet.cpp | 8 - .../System/Placeholder/PlaceholderApplet.h | 7 +- .../InkHUD/Applets/System/Tips/TipsApplet.cpp | 107 +- .../InkHUD/Applets/System/Tips/TipsApplet.h | 5 +- .../User/AllMessage/AllMessageApplet.cpp | 8 +- .../niche/InkHUD/Applets/User/DM/DMApplet.cpp | 14 +- .../Applets/User/Positions/PositionsApplet.h | 12 +- .../User/RecentsList/RecentsListApplet.cpp | 4 +- .../ThreadedMessage/ThreadedMessageApplet.cpp | 2 - .../ThreadedMessage/ThreadedMessageApplet.h | 2 +- .../{UpdateMediator.cpp => DisplayHealth.cpp} | 79 +- .../{UpdateMediator.h => DisplayHealth.h} | 24 +- src/graphics/niche/InkHUD/Events.cpp | 179 +++ src/graphics/niche/InkHUD/Events.h | 63 + src/graphics/niche/InkHUD/InkHUD.cpp | 218 ++++ src/graphics/niche/InkHUD/InkHUD.h | 110 ++ src/graphics/niche/InkHUD/MessageStore.h | 2 +- src/graphics/niche/InkHUD/Persistence.cpp | 50 +- src/graphics/niche/InkHUD/Persistence.h | 171 +-- .../niche/InkHUD/PlatformioConfig.ini | 4 +- src/graphics/niche/InkHUD/Renderer.cpp | 412 ++++++ src/graphics/niche/InkHUD/Renderer.h | 96 ++ src/graphics/niche/InkHUD/SystemApplet.h | 41 + src/graphics/niche/InkHUD/Tile.cpp | 48 +- src/graphics/niche/InkHUD/Tile.h | 39 +- src/graphics/niche/InkHUD/Types.h | 62 - src/graphics/niche/InkHUD/WindowManager.cpp | 1157 ++++------------- src/graphics/niche/InkHUD/WindowManager.h | 179 +-- src/graphics/niche/Inputs/TwoButton.cpp | 16 +- .../heltec_vision_master_e213/nicheGraphics.h | 53 +- .../heltec_vision_master_e213/platformio.ini | 6 +- .../heltec_vision_master_e290/nicheGraphics.h | 63 +- .../heltec_vision_master_e290/platformio.ini | 6 +- .../heltec_wireless_paper/nicheGraphics.h | 49 +- variants/heltec_wireless_paper/platformio.ini | 6 +- variants/t-echo/nicheGraphics.h | 56 +- variants/t-echo/platformio.ini | 2 +- 70 files changed, 2381 insertions(+), 1955 deletions(-) rename src/graphics/niche/InkHUD/{UpdateMediator.cpp => DisplayHealth.cpp} (61%) rename src/graphics/niche/InkHUD/{UpdateMediator.h => DisplayHealth.h} (56%) create mode 100644 src/graphics/niche/InkHUD/Events.cpp create mode 100644 src/graphics/niche/InkHUD/Events.h create mode 100644 src/graphics/niche/InkHUD/InkHUD.cpp create mode 100644 src/graphics/niche/InkHUD/InkHUD.h create mode 100644 src/graphics/niche/InkHUD/Renderer.cpp create mode 100644 src/graphics/niche/InkHUD/Renderer.h create mode 100644 src/graphics/niche/InkHUD/SystemApplet.h delete mode 100644 src/graphics/niche/InkHUD/Types.h diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index e29139001..526b6f243 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -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; diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp index 7e4f0b709..6d9b709b1 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp @@ -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 diff --git a/src/graphics/niche/Drivers/EInk/EInk.cpp b/src/graphics/niche/Drivers/EInk/EInk.cpp index 0abe20bf9..043788b13 100644 --- a/src/graphics/niche/Drivers/EInk/EInk.cpp +++ b/src/graphics/niche/Drivers/EInk/EInk.cpp @@ -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. diff --git a/src/graphics/niche/Drivers/EInk/EInk.h b/src/graphics/niche/Drivers/EInk/EInk.h index 1fbc25a14..facb8ce72 100644 --- a/src/graphics/niche/Drivers/EInk/EInk.h +++ b/src/graphics/niche/Drivers/EInk/EInk.h @@ -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 diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp index bfc5ac681..2cab179b9 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp @@ -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" diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp index c54769fc2..c843c4694 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp @@ -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); } diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h index 5c801c014..f9da202aa 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h @@ -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); }; diff --git a/src/graphics/niche/Drivers/EInk/README.md b/src/graphics/niche/Drivers/EInk/README.md index ffe21e507..04a23a31f 100644 --- a/src/graphics/niche/Drivers/EInk/README.md +++ b/src/graphics/niche/Drivers/EInk/README.md @@ -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()` diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp index d58e5b37a..07d02a2ae 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp @@ -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); } diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.h b/src/graphics/niche/Drivers/EInk/SSD16XX.h index f9077f188..88fe4dc25 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.h +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.h @@ -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); }; diff --git a/src/graphics/niche/Drivers/README.md b/src/graphics/niche/Drivers/README.md index 566558658..14a9edd0b 100644 --- a/src/graphics/niche/Drivers/README.md +++ b/src/graphics/niche/Drivers/README.md @@ -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 diff --git a/src/graphics/niche/FlashData.h b/src/graphics/niche/FlashData.h index 4a436d387..8a63c6108 100644 --- a/src/graphics/niche/FlashData.h +++ b/src/graphics/niche/FlashData.h @@ -119,7 +119,7 @@ template 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(); diff --git a/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h b/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h index 49f03d4e1..d222cd1c3 100644 --- a/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h +++ b/src/graphics/niche/Fonts/FreeSans6pt8bCyrillic.h @@ -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. */ diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index ebd0acc78..9fda9a87e 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -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); } diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 30c1bdcdc..028b24f9c 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -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 +#include // 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 diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index bee9d33e6..25597c9b9 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -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"); diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 89f901c94..504bd12b3 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -15,7 +15,7 @@ #include "configuration.h" -#include +#include // 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 substitutions; + std::vector substitutions; // List of all character substitutions to run, prior to printing a string }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index 21f404349..ea7b74262 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -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 )); diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h index fd5245631..f45a36071 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h @@ -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); diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 5d60e6800..8ede40780 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index 670dd9e9a..0abcad824 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -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 cards; // Derived applet places cards here, for this base applet to render + std::deque 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 }; diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp index e0b2a4238..e31f534ac 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h index edfb211d7..f280afcda 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index e4432a7c2..4f99d99ee 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h index 765ca073f..e5b4172be 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h @@ -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 powerStatusObserver = CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index cc24417ab..24c2d88a4 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h index aa1bf8b2c..b55d4a2d9 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index d24ae59a5..7397f7e9f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -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); diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index f2e9b3947..fe72d826b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -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 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 diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index 886be84b5..aa702c032 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -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); diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index c4d36a4fd..66df784b4 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index 457fa0f3f..81de05b30 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -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(); } diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h index ce420e68b..b89783a25 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h @@ -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(this, &PairingApplet::onBluetoothStatusUpdate); std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros - - WindowManager *windowManager = nullptr; // For convenience. Set in constructor. }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp index 4f66593b9..99cdeb0ac 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h index e5106105c..78ba5cd89 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index e6b5b5dc9..1abf3ccfa 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index 29bcdfa8b..e7bb7bedc 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 1ae313d8a..f7e2a8e9d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -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) { diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index 526b86901..7a1d14f32 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h index 5bcec339d..28a53cb0f 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp index 54e67efef..02aa4a721 100644 --- a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index d81dd020c..d7d2e79c8 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -23,8 +23,6 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : cha void InkHUD::ThreadedMessageApplet::onRender() { - setFont(fontSmall); - // ============= // Draw a header // ============= diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 5bb8bf96e..3e11a25f2 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -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; diff --git a/src/graphics/niche/InkHUD/UpdateMediator.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp similarity index 61% rename from src/graphics/niche/InkHUD/UpdateMediator.cpp rename to src/graphics/niche/InkHUD/DisplayHealth.cpp index 16fc21cef..e8849b72e 100644 --- a/src/graphics/niche/InkHUD/UpdateMediator.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -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(); } diff --git a/src/graphics/niche/InkHUD/UpdateMediator.h b/src/graphics/niche/InkHUD/DisplayHealth.h similarity index 56% rename from src/graphics/niche/InkHUD/UpdateMediator.h rename to src/graphics/niche/InkHUD/DisplayHealth.h index e4c7c6786..2bd887f9d 100644 --- a/src/graphics/niche/InkHUD/UpdateMediator.h +++ b/src/graphics/niche/InkHUD/DisplayHealth.h @@ -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 }; diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp new file mode 100644 index 000000000..10072b302 --- /dev/null +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h new file mode 100644 index 000000000..6a6e9d7a2 --- /dev/null +++ b/src/graphics/niche/InkHUD/Events.h @@ -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 deepSleepObserver = CallbackObserver(this, &Events::beforeDeepSleep); + + // Get notified when the system is rebooting + CallbackObserver rebootObserver = CallbackObserver(this, &Events::beforeReboot); + + // Cache *incoming* text messages, for use by applets + CallbackObserver textMessageObserver = + CallbackObserver(this, &Events::onReceiveTextMessage); + +#ifdef ARCH_ESP32 + // Get notified when the system is entering light sleep + CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); +#endif +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp new file mode 100644 index 000000000..90b6718e0 --- /dev/null +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -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::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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h new file mode 100644 index 000000000..13839ea22 --- /dev/null +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -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 + +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 getEmptyTiles(); // From WindowManager + + // Applets + + SystemApplet *getSystemApplet(const char *name); + std::vector userApplets; + std::vector 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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h index 3cf7d0f68..745c3b2eb 100644 --- a/src/graphics/niche/InkHUD/MessageStore.h +++ b/src/graphics/niche/InkHUD/MessageStore.h @@ -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(); diff --git a/src/graphics/niche/InkHUD/Persistence.cpp b/src/graphics/niche/InkHUD/Persistence.cpp index 6e8ac0458..20909f2dc 100644 --- a/src/graphics/niche/InkHUD/Persistence.cpp +++ b/src/graphics/niche/InkHUD/Persistence.cpp @@ -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::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::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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index e2daa02d9..28841d4d9 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index 0a70f1d0e..cab0ea7bc 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -1,5 +1,7 @@ [inkhud] -build_src_filter = +<../variants/$PIOENV> +; Include nicheGraphics.h +build_src_filter = + +; 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) diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp new file mode 100644 index 000000000..c058c4126 --- /dev/null +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -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 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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Renderer.h b/src/graphics/niche/InkHUD/Renderer.h new file mode 100644 index 000000000..b6cf9e215 --- /dev/null +++ b/src/graphics/niche/InkHUD/Renderer.h @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/SystemApplet.h b/src/graphics/niche/InkHUD/SystemApplet.h new file mode 100644 index 000000000..0f8ceedc7 --- /dev/null +++ b/src/graphics/niche/InkHUD/SystemApplet.h @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Tile.cpp b/src/graphics/niche/InkHUD/Tile.cpp index e65835801..5e548de74 100644 --- a/src/graphics/niche/InkHUD/Tile.cpp +++ b/src/graphics/niche/InkHUD/Tile.cpp @@ -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 diff --git a/src/graphics/niche/InkHUD/Tile.h b/src/graphics/niche/InkHUD/Tile.h index e41536e53..0f5444f17 100644 --- a/src/graphics/niche/InkHUD/Tile.h +++ b/src/graphics/niche/InkHUD/Tile.h @@ -14,47 +14,44 @@ #include "configuration.h" #include "./Applet.h" -#include "./Types.h" -#include "./WindowManager.h" -#include +#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 diff --git a/src/graphics/niche/InkHUD/Types.h b/src/graphics/niche/InkHUD/Types.h deleted file mode 100644 index f4ab9ed4e..000000000 --- a/src/graphics/niche/InkHUD/Types.h +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index f987a3646..c883e9a29 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -2,11 +2,6 @@ #include "./WindowManager.h" -#include "RTC.h" -#include "mesh/NodeDB.h" - -// System applets -// Must be defined in .cpp to prevent a circular dependency with Applet base class #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" @@ -14,479 +9,108 @@ #include "./Applets/System/Pairing/PairingApplet.h" #include "./Applets/System/Placeholder/PlaceholderApplet.h" #include "./Applets/System/Tips/TipsApplet.h" +#include "./SystemApplet.h" using namespace NicheGraphics; -InkHUD::WindowManager::WindowManager() : concurrency::OSThread("InkHUD WM") +InkHUD::WindowManager::WindowManager() { - // Nothing for the timer to do just yet - OSThread::disable(); + // Convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } -// Get or create the WindowManager singleton -InkHUD::WindowManager *InkHUD::WindowManager::getInstance() -{ - // Create the singleton instance of our class, if not yet done - static InkHUD::WindowManager *instance = new InkHUD::WindowManager(); - return instance; -} - -// Connect the driver, which is created independently is setupNicheGraphics() -void InkHUD::WindowManager::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]; -} - -// Sets the ideal ratio of FAST updates to FULL updates -// We want as many FAST updates as possible, without causing gradual degradation of the display -// If explicitly requested, number of FAST updates may exceed fastPerFull value. -// In this case, the stressMultiplier is applied, causing the "FULL update debt" to increase by more than normal -// The stressMultplier helps the display recover from particularly taxing periods of use -// (Default arguments of 5,2 are very conservative values) -void InkHUD::WindowManager::setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0) -{ - mediator.fastPerFull = fastPerFull; - mediator.stressMultiplier = stressMultiplier; -} - -// Register a user applet with the WindowManager +// Register a user applet with InkHUD // This is called in setupNicheGraphics() // This should be the only time that specific user applets are mentioned in the code // If a user applet is not added with this method, its code should not be built +// Call before begin void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { - userApplets.push_back(a); + inkhud->userApplets.push_back(a); // If requested, mark in settings that this applet should be active by default // This means that it will be available for the user to cycle to with short-press of the button - // This is the default state only: user can activate or deactive applets through the menu. + // This is the default state only: user can activate or deactivate applets through the menu. // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present if (defaultActive) - settings.userApplets.active[userApplets.size() - 1] = true; + settings->userApplets.active[inkhud->userApplets.size() - 1] = true; // If requested, mark in settings that this applet should "autoshow" by default // This means that the applet will be automatically brought to foreground when it has new data to show // This is the default state only: user can select which applets have this behavior through the menu // User's selection is stored in settings, and will be honored instead of these defaults, if present if (defaultAutoshow) - settings.userApplets.autoshow[userApplets.size() - 1] = true; + settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true; // If specified, mark this as the default applet for a given tile index // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile if (onTile != (uint8_t)-1) - settings.userTiles.displayedUserApplet[onTile] = userApplets.size() - 1; + settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1; // The label that will be show in the applet selection menu, on the device a->name = name; } -// Perform initial setup, and begin responding to incoming events -// First task once init is to show the boot screen +// Initial configuration at startup void InkHUD::WindowManager::begin() { - // Make sure we have set a driver - if (!this->driver) { - LOG_ERROR("Driver not set"); - delay(2000); // Wait for native serial.. - assert(false); - } - - loadDataFromFlash(); + assert(inkhud); createSystemApplets(); - createSystemTiles(); placeSystemTiles(); - assignSystemAppletsToTiles(); createUserApplets(); createUserTiles(); placeUserTiles(); assignUserAppletsToTiles(); refocusTile(); - - logoApplet->showBootScreen(); - forceUpdate(Drivers::EInk::FULL, false); // Update now, and wait here until complete - - deepSleepObserver.observe(¬ifyDeepSleep); - rebootObserver.observe(¬ifyReboot); - textMessageObserver.observe(textMessageModule); -#ifdef ARCH_ESP32 - lightSleepObserver.observe(¬ifyLightSleep); -#endif } -// Set-up special "system applets" -// These handle things like bootscreen, pop-up notifications etc -// They are processed separately from the user applets, because they might need to do "weird things" -// They also won't be activated or deactivated -void InkHUD::WindowManager::createSystemApplets() +// Focus on a different tile +// The "focused tile" is the one which cycles applets on user button press, +// and the one where the menu will be displayed +void InkHUD::WindowManager::nextTile() { - logoApplet = new LogoApplet; - pairingApplet = new PairingApplet; - tipsApplet = new TipsApplet; - notificationApplet = new NotificationApplet; - batteryIconApplet = new BatteryIconApplet; - menuApplet = new MenuApplet; - placeholderApplet = new PlaceholderApplet; - - // System applets are always active - logoApplet->activate(); - pairingApplet->activate(); - tipsApplet->activate(); - notificationApplet->activate(); - batteryIconApplet->activate(); - menuApplet->activate(); - placeholderApplet->activate(); - - // Add to the systemApplets vector - // Although system applets often need special handling, sometimes we can process them en-masse with this vector - // e.g. rendering, raising events - // Order of these entries determines Z-Index when rendering - systemApplets.push_back(logoApplet); - systemApplets.push_back(pairingApplet); - systemApplets.push_back(tipsApplet); - systemApplets.push_back(batteryIconApplet); - systemApplets.push_back(menuApplet); - systemApplets.push_back(notificationApplet); - // Note: placeholder applet is technically a system applet, but it renders in WindowManager::renderPlaceholders -} - -void InkHUD::WindowManager::createSystemTiles() -{ - fullscreenTile = new Tile; - notificationTile = new Tile; - batteryIconTile = new Tile; -} - -void InkHUD::WindowManager::placeSystemTiles() -{ - fullscreenTile->placeSystemTile(0, 0, getWidth(), getHeight()); - notificationTile->placeSystemTile(0, 0, getWidth(), 20); // Testing only: constant value - - // Todo: appropriate sizing for the battery icon - const uint16_t batteryIconHeight = Applet::getHeaderHeight() - (2 * 2); - uint16_t batteryIconWidth = batteryIconHeight * 1.8; - - batteryIconTile->placeSystemTile(getWidth() - batteryIconWidth, 2, batteryIconWidth, batteryIconHeight); -} - -// Assign a system applet to the fullscreen tile -// Rendering of user tiles is suspended when the fullscreen tile is occupied -void InkHUD::WindowManager::claimFullscreen(InkHUD::Applet *a) -{ - // Make sure that only system applets use the fullscreen tile - bool isSystemApplet = false; - for (Applet *sa : systemApplets) { - if (sa == a) { - isSystemApplet = true; - break; - } - } - assert(isSystemApplet); - - fullscreenTile->assignApplet(a); -} - -// Clear the fullscreen tile, unlinking whichever system applet is assigned -// This allows the normal rendering of user tiles to resume -void InkHUD::WindowManager::releaseFullscreen() -{ - // Make sure the applet is ready to release the tile - assert(!fullscreenTile->getAssignedApplet()->isForeground()); - - // Break the link between the applet and the fullscreen tile - fullscreenTile->assignApplet(nullptr); -} - -// Some system applets can be assigned to a tile at boot -// These are applets which do have their own tile, and whose assignment never changes -// Applets which: -// - share the fullscreen tile (e.g. logoApplet, pairingApplet), -// - render on user tiles (e.g. menuApplet, placeholderApplet), -// are assigned to the tile only when needed -void InkHUD::WindowManager::assignSystemAppletsToTiles() -{ - notificationTile->assignApplet(notificationApplet); - batteryIconTile->assignApplet(batteryIconApplet); -} - -// Activate or deactivate user applets, to match settings -// Called at boot, or after run-time config changes via menu -// Note: this method does not instantiate the applets; -// this is done in setupNicheGraphics, with WindowManager::addApplet -void InkHUD::WindowManager::createUserApplets() -{ - // Deactivate and remove any no-longer-needed applets - for (uint8_t i = 0; i < userApplets.size(); i++) { - Applet *a = userApplets.at(i); - - // If the applet is active, but settings say it shouldn't be: - // - run applet's custom deactivation code - // - mark applet as inactive (internally) - if (a->isActive() && !settings.userApplets.active[i]) - a->deactivate(); + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; } - // Activate and add any new applets - for (uint8_t i = 0; i < userApplets.size() && i < MAX_USERAPPLETS_GLOBAL; i++) { + // Swap to next tile + settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count; - // If not activated, but it now should be: - // - run applet's custom activation code - // - mark applet as active (internally) - if (!userApplets.at(i)->isActive() && settings.userApplets.active[i]) - userApplets.at(i)->activate(); - } + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); + + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); + + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); } -void InkHUD::WindowManager::createUserTiles() +// Show the menu (on the the focused tile) +// The applet previously displayed there will be restored once the menu closes +void InkHUD::WindowManager::openMenu() { - // Delete any tiles which currently exist - for (Tile *t : userTiles) - delete t; - userTiles.clear(); - - // Create new tiles - for (uint8_t i = 0; i < settings.userTiles.count; i++) { - Tile *t = new Tile; - userTiles.push_back(t); - } -} - -void InkHUD::WindowManager::placeUserTiles() -{ - // Calculate the display region occupied by each tile - // This determines how pixels are translated from applet-space to windowmanager-space - for (uint8_t i = 0; i < userTiles.size(); i++) - userTiles.at(i)->placeUserTile(settings.userTiles.count, i); -} - -void InkHUD::WindowManager::assignUserAppletsToTiles() -{ - // Set "assignedApplet" property - // Which applet should be initially shown on a tile? - // This is preserved between reboots, but the value needs validating at startup - for (uint8_t i = 0; i < userTiles.size(); i++) { - Tile *t = userTiles.at(i); - - // Check whether tile can display the previously shown applet again - uint8_t oldIndex = settings.userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets - bool canRestore = true; - if (oldIndex > userApplets.size() - 1) // Check if old index is now out of bounds - canRestore = false; - else if (!settings.userApplets.active[oldIndex]) // Check that old applet is still activated - canRestore = false; - else { // Check that the old applet isn't now shown already on a different tile - for (uint8_t i2 = 0; i2 < i; i2++) { - if (settings.userTiles.displayedUserApplet[i2] == oldIndex) { - canRestore = false; - break; - } - } - } - - // Restore previously shown applet if possible, - // otherwise assign nullptr, which will render specially using placeholderApplet - if (canRestore) { - Applet *a = userApplets.at(oldIndex); - t->assignApplet(a); - a->bringToForeground(); - } else { - t->assignApplet(nullptr); - settings.userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet - } - } -} - -void InkHUD::WindowManager::refocusTile() -{ - // Validate "focused tile" setting - // - info: focused tile responds to button presses: applet cycling, menu, etc - // - if number of tiles changed, might now be out of index - if (settings.userTiles.focused >= userTiles.size()) - settings.userTiles.focused = 0; - - // Give "focused tile" a valid applet - // - scan for another valid applet, which we can addSubstitution - // - reason: nextApplet() won't cycle if no applet is assigned - Tile *focusedTile = userTiles.at(settings.userTiles.focused); - if (!focusedTile->getAssignedApplet()) { - // Search for available applets - for (uint8_t i = 0; i < userApplets.size(); i++) { - Applet *a = userApplets.at(i); - if (a->isActive() && !a->isForeground()) { - // Found a suitable applet - // Assign it to the focused tile - focusedTile->assignApplet(a); - a->bringToForeground(); - settings.userTiles.displayedUserApplet[settings.userTiles.focused] = i; // Record change: persist after reboot - break; - } - } - } -} - -// Callback for deepSleepObserver -// Returns 0 to signal that we agree to sleep now -int InkHUD::WindowManager::beforeDeepSleep(void *unused) -{ - // Notify all applets that we're shutting down - for (Applet *ua : userApplets) { - ua->onDeactivate(); - ua->onShutdown(); - } - for (Applet *sa : userApplets) { - // Note: no onDeactivate. System applets are always active. - sa->onShutdown(); - } - - // User has successfull executed a safe shutdown - // We don't need to nag at boot anymore - settings.tips.safeShutdownSeen = true; - - saveDataToFlash(); - - // Display the shutdown screen, and wait here until the update is complete - logoApplet->showShutdownScreen(); - 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::WindowManager::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 : userApplets) { - a->onDeactivate(); - a->onShutdown(); - } - for (Applet *sa : userApplets) { - // Note: no onDeactivate. System applets are always active. - sa->onShutdown(); - } - - saveDataToFlash(); - - return 0; // No special status to report. Ignored anyway by this Observable -} - -#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::WindowManager::beforeLightSleep(void *unused) -{ - if (driver->busy()) { - LOG_INFO("Waiting for display"); - driver->await(); // Wait here for update to complete - } - - return 0; // No special status to report. Ignored anyway by this Observable -} -#endif - -// 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::WindowManager::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 implemetation 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 - latestMessage.wasBroadcast = isBroadcast(packet->to); - - // Pick the appropriate variable to store the message in - MessageStore::Message *storedMessage = latestMessage.wasBroadcast ? &latestMessage.broadcast : &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) -} - -// Triggered by an input source when a short-press fires -// The input source is a separate component; not part of InkHUD -// It is connected in setupNicheGraphics() -void InkHUD::WindowManager::handleButtonShort() -{ - // If notification is open: close it - if (notificationApplet->isForeground()) { - notificationApplet->dismiss(); - forceUpdate(EInk::UpdateTypes::FULL); // Redraw everything, to clear the notification - } - - // If window manager is locked: lock owner handles button - else if (lockOwner) - lockOwner->onButtonShortPress(); - - // Normally: next applet - else - nextApplet(); -} - -// Triggered by an input source when a long-press fires -// The input source is a separate component; not part of InkHUD -// It is connected in setupNicheGraphics() -// Note: input source should raise this while button still held -void InkHUD::WindowManager::handleButtonLong() -{ - if (lockOwner) - lockOwner->onButtonLongPress(); - - else - menuApplet->show(userTiles.at(settings.userTiles.focused)); + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + menu->show(userTiles.at(settings->userTiles.focused)); } // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::nextApplet() { - Tile *t = userTiles.at(settings.userTiles.focused); + Tile *t = userTiles.at(settings->userTiles.focused); // Abort if zero applets available // nullptr means WindowManager::refocusTile determined that there were no available applets @@ -495,8 +119,8 @@ void InkHUD::WindowManager::nextApplet() // Find the index of the applet currently shown on the tile uint8_t appletIndex = -1; - for (uint8_t i = 0; i < userApplets.size(); i++) { - if (userApplets.at(i) == t->getAssignedApplet()) { + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { appletIndex = i; break; } @@ -507,15 +131,14 @@ void InkHUD::WindowManager::nextApplet() // Iterate forward through the WindowManager::applets, looking for the next valid applet Applet *nextValidApplet = nullptr; - // for (uint8_t i = (appletIndex + 1) % applets.size(); i != appletIndex; i = (i + 1) % applets.size()) { - for (uint8_t i = 1; i < userApplets.size(); i++) { - uint8_t newAppletIndex = (appletIndex + i) % userApplets.size(); - Applet *a = userApplets.at(newAppletIndex); + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size(); + Applet *a = inkhud->userApplets.at(newAppletIndex); // Looking for an applet which is active (enabled by user), but currently in background if (a->isActive() && !a->isForeground()) { nextValidApplet = a; - settings.userTiles.displayedUserApplet[settings.userTiles.focused] = + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! break; } @@ -529,35 +152,33 @@ void InkHUD::WindowManager::nextApplet() t->getAssignedApplet()->sendToBackground(); t->assignApplet(nextValidApplet); nextValidApplet->bringToForeground(); - forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } -// Focus on a different tile -// The "focused tile" is the one which cycles applets on user button press, -// and the one where the menu will be displayed -// Note: this method is only used by an aux button -// The menuApplet manually performs a subset of these actions, to avoid disturbing the stale image on adjacent tiles -void InkHUD::WindowManager::nextTile() +// Rotate the display image by 90 degrees +void InkHUD::WindowManager::rotate() { - // Close the menu applet if open - // We done *really* want to do this, but it simplifies handling *a lot* - if (menuApplet->isForeground()) - menuApplet->sendToBackground(); - - // Seems like some system applet other than menu is open. Pairing? Booting? - if (!canRequestUpdate()) - return; - - // Swap to next tile - settings.userTiles.focused = (settings.userTiles.focused + 1) % settings.userTiles.count; - - // Make sure that we don't get stuck on the placeholder tile - // changeLayout reassigns applets to tiles + settings->rotation = (settings->rotation + 1) % 4; changeLayout(); +} - // Ask the tile to draw an indicator showing which tile is now focused - // Requests a render - userTiles.at(settings.userTiles.focused)->requestHighlight(); +// Change whether the battery icon is displayed (top right corner) +// Don't toggle the OptionalFeatures value before calling this, our method handles it internally +void InkHUD::WindowManager::toggleBatteryIcon() +{ + BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon"); + + settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots + + // Show or hide the applet + if (settings->optionalFeatures.batteryIcon) + batteryIcon->bringToForeground(); + else + batteryIcon->sendToBackground(); + + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time @@ -589,21 +210,24 @@ void InkHUD::WindowManager::changeLayout() // Restore menu // - its tile was just destroyed and recreated (createUserTiles) // - its assignment was cleared (assignUserAppletsToTiles) - if (menuApplet->isForeground()) { - Tile *ft = userTiles.at(settings.userTiles.focused); - menuApplet->show(ft); + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + if (menu->isForeground()) { + Tile *ft = userTiles.at(settings->userTiles.focused); + menu->show(ft); } // Force-render // - redraw all applets - forceUpdate(EInk::UpdateTypes::FAST); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user activates or deactivates applets at run-time // Call after changing settings.userApplets.active void InkHUD::WindowManager::changeActivatedApplets() { - assert(menuApplet->isForeground()); + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + + assert(menu->isForeground()); // Activate or deactivate applets // - to match value of settings.userApplets.active @@ -621,223 +245,41 @@ void InkHUD::WindowManager::changeActivatedApplets() // Restore menu // - its assignment was cleared (assignUserAppletsToTiles) - if (menuApplet->isForeground()) { - Tile *ft = userTiles.at(settings.userTiles.focused); - menuApplet->show(ft); + if (menu->isForeground()) { + Tile *ft = userTiles.at(settings->userTiles.focused); + menu->show(ft); } // Force-render // - redraw all applets - forceUpdate(EInk::UpdateTypes::FAST); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } -// Change whether the battery icon is displayed (top left corner) -// Don't toggle the OptionalFeatures value before calling this, our method handles it internally -void InkHUD::WindowManager::toggleBatteryIcon() -{ - assert(batteryIconApplet->isActive()); - settings.optionalFeatures.batteryIcon = !settings.optionalFeatures.batteryIcon; // Preserve the change between boots - - // Show or hide the applet - if (settings.optionalFeatures.batteryIcon) - batteryIconApplet->bringToForeground(); - else - batteryIconApplet->sendToBackground(); - - // Force-render - // - redraw all applets - forceUpdate(EInk::UpdateTypes::FAST); -} - -// Allow applets to suppress notifications -// Applets will be asked whether they approve, 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::WindowManager::approveNotification(InkHUD::Notification &n) -{ - // Ask all currently displayed applets - for (Tile *ut : userTiles) { - Applet *ua = ut->getAssignedApplet(); - if (ua && !ua->approveNotification(n)) - return false; - } - - // Nobody objected - return true; -} - -// 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::WindowManager::requestUpdate() -{ - requestingUpdate = 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 absense of any sort of applet event -// Display health, for example. -// In these situations, we use forceUpdate -void InkHUD::WindowManager::forceUpdate(EInk::UpdateTypes type, bool async) -{ - requestingUpdate = true; - forcingUpdate = true; - forcedUpdateType = 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); -} - -// Receives rendered image data from an Applet, via a tile -// When applets render, they output pixel data relative to their own left / top edges -// They pass this pixel data to tile, which offsets the pixels, making them relative to the display left / top edges -// That data is then passed to this method, which applies any rotation, then places the pixels into the image buffer -// That image buffer is the fully-formatted data handed off to the driver -void InkHUD::WindowManager::handleTilePixel(int16_t x, int16_t y, Color c) -{ - rotatePixelCoords(&x, &y); - setBufferPixel(x, y, c); -} - -// Width of the display, relative to rotation -uint16_t InkHUD::WindowManager::getWidth() -{ - if (settings.rotation % 2) - return driver->height; - else - return driver->width; -} - -// Height of the display, relative to rotation -uint16_t InkHUD::WindowManager::getHeight() -{ - if (settings.rotation % 2) - return driver->width; - else - return driver->height; -} - -// How many user applets have been built? Includes applets which have been inactivated by user config -uint8_t InkHUD::WindowManager::getAppletCount() -{ - return userApplets.size(); -} - -// A tidy title for applets: used on-display in some situations -// Index is the order in the WindowManager::userApplets vector -// This is the same order that applets were added in setupNicheGraphics -const char *InkHUD::WindowManager::getAppletName(uint8_t index) -{ - return userApplets.at(index)->name; -} - -// Allows a system applet to prevent other applets from temporarily requesting updates -// All user applets will honor this. Some system applets might not, although they probably should -// WindowManager::forceUpdate will ignore this lock -void InkHUD::WindowManager::lock(Applet *owner) -{ - // Only one system applet may lock render at once - assert(!lockOwner); - - // Only system applets may lock rendering - for (Applet *a : userApplets) - assert(owner != a); - - lockOwner = owner; -} - -// Remove a lock placed by a system applet, which prevents other applets from rendering -void InkHUD::WindowManager::unlock(Applet *owner) -{ - assert(lockOwner = owner); - lockOwner = nullptr; - - // Raise this as an event (system applets only) - // - in case applet waiting for lock - // - in case applet relinquished its lock earlier, and wants it back - for (Applet *sa : systemApplets) { - // Don't raise event for the applet which is calling unlock - // - avoid loop of unlock->lock (some implementations of Applet::onLockAvailable) - if (sa != owner) - sa->onLockAvailable(); - } -} - -// Is an applet blocked from requesting update by a current lock? -// Applets are allowed to request updates if there is no lock, or if they are the owner of the lock -// If a == nullptr, checks permission "for everyone and anyone" -bool InkHUD::WindowManager::canRequestUpdate(Applet *a) -{ - if (!lockOwner) - return true; - else if (lockOwner == a) - return true; - else - return false; -} - -// Get the applet which is currently locking rendering -// We might be able to convince it release its lock, if we want it instead -InkHUD::Applet *InkHUD::WindowManager::whoLocked() -{ - return WindowManager::lockOwner; -} - -// Runs at regular intervals -// WindowManager's uses of this include: -// - 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::WindowManager::runOnce() -{ - // If an applet asked to render, and hardware is able, lets try now - if (requestingUpdate && !driver->busy()) { - render(); - } - - // If our render() call failed, try again shortly - // otherwise, stop our thread until next update due - if (requestingUpdate) - return 250UL; - else - return OSThread::disable(); -} - -// Some applets may be permitted to bring themselved to foreground, to show new data +// Some applets may be permitted to bring themselves to foreground, to show new data // User selects which applets have this permission via on-screen menu // Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics // We will only autoshow one applet void InkHUD::WindowManager::autoshow() { - for (uint8_t i = 0; i < userApplets.size(); i++) { - Applet *a = userApplets.at(i); - if (a->wantsToAutoshow() // Applet wants to become foreground - && !a->isForeground() // Not yet foreground - && settings.userApplets.autoshow[i] // User permits this applet to autoshow - && canRequestUpdate()) // Updates not currently blocked by system applet + // Don't perform autoshow if a system applet has exclusive use of the display right now + // Note: lockRequests prevents autoshow attempting to hide menuApplet + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->lockRendering || sa->lockRequests) + return; + } + + NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification"); + + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a->wantsToAutoshow() // Applet wants to become foreground + && !a->isForeground() // Not yet foreground + && settings->userApplets.autoshow[i]) // User permits this applet to autoshow { - Tile *t = userTiles.at(settings.userTiles.focused); // Get focused tile - t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile - t->assignApplet(a); // Assign our new applet to tile - a->bringToForeground(); // Foreground our new applet + Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile + t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile + t->assignApplet(a); // Assign our new applet to tile + a->bringToForeground(); // Foreground our new applet // Check if autoshown applet shows the same information as notification intended to // In this case, we can dismiss the notification before it is shown @@ -850,257 +292,220 @@ void InkHUD::WindowManager::autoshow() } } -// Check whether an update is justified -// We usually require that a foreground applet requested the update, -// but forceUpdate call will bypass these checks. -// Abstraction for WindowManager::render only -bool InkHUD::WindowManager::shouldUpdate() +// 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::WindowManager::getEmptyTiles() { - bool should = false; + std::vector empty; - // via forceUpdate - should |= forcingUpdate; - - // via user applet - for (Tile *ut : userTiles) { - Applet *ua = ut->getAssignedApplet(); - if (ua // Tile has valid applet - && ua->wantsToRender() // This applet requested display update - && ua->isForeground() // This applet is currently shown - && canRequestUpdate()) // Requests are not currently locked - { - should = true; - break; - } + for (Tile *t : userTiles) { + Applet *a = t->getAssignedApplet(); + if (!a || !a->isActive()) + empty.push_back(t); } - // via system applet - for (Applet *sa : systemApplets) { - if (sa->wantsToRender() // This applet requested - && sa->isForeground() // This applet is currently shown - && canRequestUpdate(sa)) // Requests are not currently locked, or this applet owns the lock - { - should = true; - break; - } - } - - return should; + return empty; } -// 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. -// Abstraction for WindowManager::render only -Drivers::EInk::UpdateTypes InkHUD::WindowManager::selectUpdateType() +// Complete the configuration of one newly instantiated system applet +// - link it with its tile +// Unlike user applets, most system applets have their own unique tile; +// the only reference to this tile is held by the system applet itself. +// - give it a name +// A system applet's name is its unique identifier. +// The name is our only reference to specific system applets, via InkHUD->getSystemApplet +// - add it to the list of system applets + +void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile) { - // Ask applets which update type they would prefer - // Some update types take priority over others - EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED; - if (forcingUpdate) { - // Update type was manually specified via forceUpdate - type = forcedUpdateType; - } else { - // User applets - for (Tile *ut : userTiles) { - Applet *ua = ut->getAssignedApplet(); - if (ua && ua->isForeground() && canRequestUpdate()) - type = mediator.prioritize(type, ua->wantsUpdateType()); - } - // System Applets - for (Applet *sa : systemApplets) { - if (sa->isForeground() && canRequestUpdate(sa)) - type = mediator.prioritize(type, sa->wantsUpdateType()); - } - } + // Some system applets might not have their own tile (e.g. menu, placeholder) + if (tile) + tile->assignApplet(applet); - // Tell the mediator what update type the applets deciced on, - // find out what update type the mediator will actually allow us to have - type = mediator.evaluate(type); - - return type; + applet->name = name; + inkhud->systemApplets.push_back(applet); } -// 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 -// Abstraction for WindowManager::render only -void InkHUD::WindowManager::renderUserApplets() +// Create the "system applets" +// These handle things like bootscreen, pop-up notifications etc +// They are processed separately from the user applets, because they might need to do "weird things" +void InkHUD::WindowManager::createSystemApplets() { - // Don't render any user applets if the screen is covered by a system applet using the fullscreen tile - if (fullscreenTile->getAssignedApplet()) - return; + addSystemApplet("Logo", new LogoApplet, new Tile); + addSystemApplet("Pairing", new PairingApplet, new Tile); + addSystemApplet("Tips", new TipsApplet, new Tile); - // For each tile - for (Tile *ut : userTiles) { - Applet *ua = ut->getAssignedApplet(); // Get the applet on the tile + addSystemApplet("Menu", new MenuApplet, nullptr); - // Don't render if tile has no applet. Handled in renderPlaceholders - if (!ua) - continue; + // Battery and notifications *behind* the menu + addSystemApplet("Notification", new NotificationApplet, new Tile); + addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); - // Don't render the menu applet, Handled by renderSystemApplets - if (ua == menuApplet) - continue; + // Special handling only, via Rendering::renderPlaceholders + addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); - uint32_t start = millis(); - ua->render(); // Draw! - uint32_t stop = millis(); - LOG_DEBUG("%s took %dms to render", ua->name, stop - start); + // System applets are always active + for (SystemApplet *sa : inkhud->systemApplets) + sa->activate(); +} + +// Set the position and size of most system applets +// Most system applets have their own tile. We manually set the region this tile occupies +void InkHUD::WindowManager::placeSystemTiles() +{ + inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + + inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); + + const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; + const uint16_t batteryIconWidth = batteryIconHeight * 1.8; + inkhud->getSystemApplet("BatteryIcon") + ->getTile() + ->setRegion(inkhud->width() - batteryIconWidth, // x + 2, // y + batteryIconWidth, // width + batteryIconHeight); // height + + // Note: the tiles of placeholder and menu applets are manipulated specially + // - menuApplet borrows user tiles + // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles +} + +// Activate or deactivate user applets, to match settings +// Called at boot, or after run-time config changes via menu +// Note: this method does not instantiate the applets; +// this is done in setupNicheGraphics, when passing to InkHUD::addApplet +void InkHUD::WindowManager::createUserApplets() +{ + // Deactivate and remove any no-longer-needed applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + + // If the applet is active, but settings say it shouldn't be: + // - run applet's custom deactivation code + // - mark applet as inactive (internally) + if (a->isActive() && !settings->userApplets.active[i]) + a->deactivate(); + } + + // Activate and add any new applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + + // If not activated, but it now should be: + // - run applet's custom activation code + // - mark applet as active (internally) + if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i]) + inkhud->userApplets.at(i)->activate(); } } -// 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 -// Abstraction for WindowManager::render only -void InkHUD::WindowManager::renderSystemApplets() +// Creates the tiles which will host user applets +// The amount of these is controlled by the user, via "layout" option in the InkHUD menu +void InkHUD::WindowManager::createUserTiles() { - // Each system applet - for (Applet *sa : systemApplets) { - // Skip if not shown - if (!sa->isForeground()) - continue; + // Delete any tiles which currently exist + for (Tile *t : userTiles) + delete t; + userTiles.clear(); - // Don't draw the battery overtop the menu - // Todo: smarter way to handle this - if (sa == batteryIconApplet && menuApplet->isForeground()) - continue; - - // Skip applet if fullscreen tile is in use, but not used by this applet - // Applet is "obscured" - if (fullscreenTile->getAssignedApplet() && fullscreenTile->getAssignedApplet() != sa) - continue; - - // uint32_t start = millis(); // Debugging only: runtime - sa->render(); // Draw! - // uint32_t stop = millis(); // Debugging only: runtime - // LOG_DEBUG("%s (system) took %dms to render", (sa->name == nullptr) ? "Unnamed" : sa->name, stop - start); + // Create new tiles + for (uint8_t i = 0; i < settings->userTiles.count; i++) { + Tile *t = new Tile; + userTiles.push_back(t); } } -// 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::WindowManager::renderPlaceholders() +// Calculate the display region occupied by each tile +// This determines how pixels are translated from "relative" applet-space to "absolute" windowmanager-space +// The size and position depend on the amount of tiles the user prefers, set by the "layout" option +void InkHUD::WindowManager::placeUserTiles() { - // Don't draw if obscured by the fullscreen tile - if (fullscreenTile->getAssignedApplet()) - return; + for (uint8_t i = 0; i < userTiles.size(); i++) + userTiles.at(i)->setRegion(settings->userTiles.count, i); +} - for (Tile *ut : userTiles) { - // If no applet assigned - if (!ut->getAssignedApplet()) { - ut->assignApplet(placeholderApplet); - placeholderApplet->render(); - ut->assignApplet(nullptr); +// Link "foreground" user applets with tiles +// Which applet should be *initially* shown on a tile? +// This initial state changes once WindowManager::nextApplet is called. +// Performed at startup, or during certain run-time reconfigurations (e.g number of tiles) +// This state of "which applets are foreground" is preserved between reboots, but the value needs validating at startup. +void InkHUD::WindowManager::assignUserAppletsToTiles() +{ + // Each user tile + for (uint8_t i = 0; i < userTiles.size(); i++) { + Tile *t = userTiles.at(i); + + // Check whether tile can display the previously shown applet again + uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets + bool canRestore = true; + if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds + canRestore = false; + else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated + canRestore = false; + else { // Check that the old applet isn't now shown already on a different tile + for (uint8_t i2 = 0; i2 < i; i2++) { + if (settings->userTiles.displayedUserApplet[i2] == oldIndex) { + canRestore = false; + break; + } + } + } + + // Restore previously shown applet if possible, + // otherwise assign nullptr, which will render specially using placeholderApplet + if (canRestore) { + Applet *a = inkhud->userApplets.at(oldIndex); + t->assignApplet(a); + a->bringToForeground(); + } else { + t->assignApplet(nullptr); + settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet } } } -// 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::WindowManager::render(bool async) +// During layout changes, our focused tile setting can become invalid +// This method identifies that situation and corrects for it +void InkHUD::WindowManager::refocusTile() { - // 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(); + // Validate "focused tile" setting + // - info: focused tile responds to button presses: applet cycling, menu, etc + // - if number of tiles changed, might now be out of index + if (settings->userTiles.focused >= userTiles.size()) + settings->userTiles.focused = 0; + + // Give "focused tile" a valid applet + // - scan for another valid applet, which we can addSubstitution + // - reason: nextApplet() won't cycle if no applet is assigned + Tile *focusedTile = userTiles.at(settings->userTiles.focused); + if (!focusedTile->getAssignedApplet()) { + // Search for available applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a->isActive() && !a->isForeground()) { + // Found a suitable applet + // Assign it to the focused tile + focusedTile->assignApplet(a); + a->bringToForeground(); + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot + break; + } + } } - - // (Potentially) change applet to display new info, - // then check if this newly displayed applet makes a pending notification redundant - 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 - EInk::UpdateTypes updateType = selectUpdateType(); - - // Render the new image - clearBuffer(); - renderUserApplets(); - renderSystemApplets(); - renderPlaceholders(); - - // 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(); - } else - LOG_DEBUG("Not updating display"); - - // 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 - requestingUpdate = false; - forcingUpdate = false; - forcedUpdateType = EInk::UpdateTypes::UNSPECIFIED; -} - -// 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::WindowManager::setBufferPixel(int16_t x, int16_t y, Color c) -{ - 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); -} - -// 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::WindowManager::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; -} - -// Manually fill the image buffer with WHITE -// Clears any old drawing -void InkHUD::WindowManager::clearBuffer() -{ - memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } // Seach for any applets which believe they are foreground, but no longer have a valid tile // Tidies up after layout changes at runtime void InkHUD::WindowManager::findOrphanApplets() { - for (uint8_t ia = 0; ia < userApplets.size(); ia++) { - Applet *a = userApplets.at(ia); + for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) { + Applet *a = inkhud->userApplets.at(ia); // Applet doesn't believe it is displayed: not orphaned if (!a->isForeground()) diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index f701233e2..4d1aedf1b 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -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 - -#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 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 deepSleepObserver = - CallbackObserver(this, &WindowManager::beforeDeepSleep); + std::vector userTiles; // Tiles which can host user applets - // Get notified when the system is rebooting - CallbackObserver rebootObserver = - CallbackObserver(this, &WindowManager::beforeReboot); - - // Cache *incoming* text messages, for use by applets - CallbackObserver textMessageObserver = - CallbackObserver(this, &WindowManager::onReceiveTextMessage); - -#ifdef ARCH_ESP32 - // Get notified when the system is entering light sleep - CallbackObserver lightSleepObserver = - CallbackObserver(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 userApplets; - std::vector userTiles; - - // System Applets - std::vector 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 \ No newline at end of file diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index e478364cc..10d89ef41 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -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 diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h index f7a37fc61..b14c72896 100644 --- a/variants/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -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(); } diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini index 00fffdfd9..6ba597200 100644 --- a/variants/heltec_vision_master_e213/platformio.ini +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -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 \ No newline at end of file +upload_speed = 921600 \ No newline at end of file diff --git a/variants/heltec_vision_master_e290/nicheGraphics.h b/variants/heltec_vision_master_e290/nicheGraphics.h index c55a84ec0..c14ee76ec 100644 --- a/variants/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/heltec_vision_master_e290/nicheGraphics.h @@ -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(); } diff --git a/variants/heltec_vision_master_e290/platformio.ini b/variants/heltec_vision_master_e290/platformio.ini index 232b13559..cfea81a7e 100644 --- a/variants/heltec_vision_master_e290/platformio.ini +++ b/variants/heltec_vision_master_e290/platformio.ini @@ -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 \ No newline at end of file +upload_speed = 921600 \ No newline at end of file diff --git a/variants/heltec_wireless_paper/nicheGraphics.h b/variants/heltec_wireless_paper/nicheGraphics.h index 0c26f453c..44405b8f6 100644 --- a/variants/heltec_wireless_paper/nicheGraphics.h +++ b/variants/heltec_wireless_paper/nicheGraphics.h @@ -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 diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index b32b60dd5..9979e1c1d 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -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 \ No newline at end of file +upload_speed = 921600 \ No newline at end of file diff --git a/variants/t-echo/nicheGraphics.h b/variants/t-echo/nicheGraphics.h index 44d8ef4c3..f0ffe4108 100644 --- a/variants/t-echo/nicheGraphics.h +++ b/variants/t-echo/nicheGraphics.h @@ -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(); }); diff --git a/variants/t-echo/platformio.ini b/variants/t-echo/platformio.ini index bca760453..e01befb45 100644 --- a/variants/t-echo/platformio.ini +++ b/variants/t-echo/platformio.ini @@ -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 \ No newline at end of file