diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index d715d398b8..d790e30c18 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -87,9 +87,9 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) #ifndef EINK_NO_HIBERNATE // Only hibernate if controller IC will preserve image memory // Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display) adafruitDisplay->hibernate(); - LOG_DEBUG("done\n"); #endif + LOG_DEBUG("done\n"); return true; } diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index f40747f266..3693781320 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -13,6 +13,8 @@ /** * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * + * Note: EInkDynamicDisplay derives from this class. + * * Remaining TODO: * optimize display() to only draw changed pixels (see other OLED subclasses for examples) * implement displayOn/displayOff to turn off the TFT device (and backlight) @@ -41,7 +43,7 @@ class EInkDisplay : public OLEDDisplay * * @return true if we did draw the screen */ - bool forceDisplay(uint32_t msecLimit = 1000); + virtual bool forceDisplay(uint32_t msecLimit = 1000); /** * shim to make the abstraction happy diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp new file mode 100644 index 0000000000..ae1e30fe1e --- /dev/null +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -0,0 +1,384 @@ +#include "configuration.h" + +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) +#include "EInkDynamicDisplay.h" + +// Constructor +EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) + : EInkDisplay(address, sda, scl, geometry, i2cBus) +{ + // If tracking ghost pixels, grab memory +#ifdef EINK_LIMIT_GHOSTING_PX + dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros +#endif +} + +// Destructor +EInkDynamicDisplay::~EInkDynamicDisplay() +{ + // If we were tracking ghost pixels, free the memory +#ifdef EINK_LIMIT_GHOSTING_PX + delete[] dirtyPixels; +#endif +} + +// Screen requests a BACKGROUND frame +void EInkDynamicDisplay::display() +{ + setFrameFlag(BACKGROUND); + update(); +} + +// Screen requests a RESPONSIVE frame +bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit) +{ + setFrameFlag(RESPONSIVE); + return update(); // (Unutilized) Base class promises to return true if update ran +} + +// Add flag for the next frame +void EInkDynamicDisplay::setFrameFlag(frameFlagTypes flag) +{ + // OR the new flag into the existing flags + this->frameFlags = (frameFlagTypes)(this->frameFlags | flag); +} + +// GxEPD2 code to set fast refresh +void EInkDynamicDisplay::configForFastRefresh() +{ + // Variant-specific code can go here +#if defined(PRIVATE_HW) +#else + // Otherwise: + adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); +#endif +} + +// GxEPD2 code to set full refresh +void EInkDynamicDisplay::configForFullRefresh() +{ + // Variant-specific code can go here +#if defined(PRIVATE_HW) +#else + // Otherwise: + adafruitDisplay->setFullWindow(); +#endif +} + +// Run any relevant GxEPD2 code, so next update will use correct refresh type +void EInkDynamicDisplay::applyRefreshMode() +{ + // Change from FULL to FAST + if (currentConfig == FULL && refresh == FAST) { + configForFastRefresh(); + currentConfig = FAST; + } + + // Change from FAST back to FULL + else if (currentConfig == FAST && refresh == FULL) { + configForFullRefresh(); + currentConfig = FULL; + } +} + +// Update fastRefreshCount +void EInkDynamicDisplay::adjustRefreshCounters() +{ + if (refresh == FAST) + fastRefreshCount++; + + else if (refresh == FULL) + fastRefreshCount = 0; +} + +// Trigger the display update by calling base class +bool EInkDynamicDisplay::update() +{ + bool refreshApproved = determineMode(); + if (refreshApproved) + EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system + return refreshApproved; // (Unutilized) Base class promises to return true if update ran +} + +// Assess situation, pick a refresh type +bool EInkDynamicDisplay::determineMode() +{ + checkWasFlooded(); + checkRateLimiting(); + + // If too soon for a new time, abort here + if (refresh == SKIPPED) { + storeAndReset(); + return false; // No refresh + } + + // -- New frame is due -- + + resetRateLimiting(); // Once determineMode() ends, will have to wait again + hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check + LOG_DEBUG("EInkDynamicDisplay: "); // Begin log entry + + // Once mode determined, any remaining checks will bypass + checkCosmetic(); + checkDemandingFast(); + checkConsecutiveFastRefreshes(); +#ifdef EINK_LIMIT_GHOSTING_PX + checkExcessiveGhosting(); +#endif + checkFrameMatchesPrevious(); + checkFastRequested(); + + if (refresh == UNSPECIFIED) + LOG_WARN("There was a flaw in the determineMode() logic.\n"); + + // -- Decision has been reached -- + applyRefreshMode(); + adjustRefreshCounters(); + +#ifdef EINK_LIMIT_GHOSTING_PX + // Full refresh clears any ghosting + if (refresh == FULL) + resetGhostPixelTracking(); +#endif + + // Return - call a refresh or not? + if (refresh == SKIPPED) { + storeAndReset(); + return false; // Don't trigger a refresh + } else { + storeAndReset(); + return true; // Do trigger a refresh + } +} + +// Did RESPONSIVE frames previously exceed the rate-limit for fast refresh? +void EInkDynamicDisplay::checkWasFlooded() +{ + if (previousReason == EXCEEDED_RATELIMIT_FAST) { + // If so, allow a BACKGROUND frame to draw as RESPONSIVE + // Because we DID want a RESPONSIVE frame last time, we just didn't get it + setFrameFlag(RESPONSIVE); + } +} + +// Is it too soon for another frame of this type? +void EInkDynamicDisplay::checkRateLimiting() +{ + uint32_t now = millis(); + + // Sanity check: millis() overflow - just let the update run.. + if (previousRunMs > now) + return; + + // Skip update: too soon for BACKGROUND + if (frameFlags == BACKGROUND) { + if (now - previousRunMs < EINK_LIMIT_RATE_BACKGROUND_SEC * 1000) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FULL; + return; + } + } + + // No rate-limit for these special cases + if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST) + return; + + // Skip update: too soon for RESPONSIVE + if (frameFlags & RESPONSIVE) { + if (now - previousRunMs < EINK_LIMIT_RATE_RESPONSIVE_SEC * 1000) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FAST; + return; + } + } +} + +// Is this frame COSMETIC (splash screens?) +void EInkDynamicDisplay::checkCosmetic() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; + + // A full refresh is requested for cosmetic purposes: we have a decision + if (frameFlags & COSMETIC) { + refresh = FULL; + reason = FLAGGED_COSMETIC; + LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC\n"); + } +} + +// Is this a one-off special circumstance, where we REALLY want a fast refresh? +void EInkDynamicDisplay::checkDemandingFast() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; + + // A fast refresh is demanded: we have a decision + if (frameFlags & DEMAND_FAST) { + refresh = FAST; + reason = FLAGGED_DEMAND_FAST; + LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST\n"); + } +} + +// Have too many fast-refreshes occured consecutively, since last full refresh? +void EInkDynamicDisplay::checkConsecutiveFastRefreshes() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; + + // If too many FAST refreshes consecutively - force a FULL refresh + if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) { + refresh = FULL; + reason = EXCEEDED_LIMIT_FASTREFRESH; + LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH\n"); + } +} + +// Does the new frame match the currently displayed image? +void EInkDynamicDisplay::checkFrameMatchesPrevious() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; + + // If frame is *not* a duplicate, abort the check + if (imageHash != previousImageHash) + return; + +#if !defined(EINK_BACKGROUND_USES_FAST) + // If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality) + if (frameFlags == BACKGROUND && fastRefreshCount > 0) { + refresh = FULL; + reason = REDRAW_WITH_FULL; + LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL\n"); + return; + } +#endif + + // Not redrawn, not COSMETIC, not DEMAND_FAST + refresh = SKIPPED; + reason = FRAME_MATCHED_PREVIOUS; + LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS\n"); +} + +// No objections, we can perform fast-refresh, if desired +void EInkDynamicDisplay::checkFastRequested() +{ + if (refresh != UNSPECIFIED) + return; + + if (frameFlags == BACKGROUND) { +#ifdef EINK_BACKGROUND_USES_FAST + // If we want BACKGROUND to use fast. (FULL only when a limit is hit) + refresh = FAST; + reason = BACKGROUND_USES_FAST; + LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu\n", fastRefreshCount); +#else + // If we do want to use FULL for BACKGROUND updates + refresh = FULL; + reason = FLAGGED_BACKGROUND; + LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND\n"); +#endif + } + + // Sanity: confirm that we did ask for a RESPONSIVE frame. + if (frameFlags & RESPONSIVE) { + refresh = FAST; + reason = NO_OBJECTIONS; + LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu\n", fastRefreshCount); + } +} + +// Reset the timer used for rate-limiting +void EInkDynamicDisplay::resetRateLimiting() +{ + previousRunMs = millis(); +} + +// Generate a hash of this frame, to compare against previous update +void EInkDynamicDisplay::hashImage() +{ + imageHash = 0; + + // Sum all bytes of the image buffer together + for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { + imageHash += buffer[b]; + } +} + +// Store the results of determineMode() for future use, and reset for next call +void EInkDynamicDisplay::storeAndReset() +{ + previousRefresh = refresh; + previousReason = reason; + + // Only store image hash if the display will update + if (refresh != SKIPPED) { + previousImageHash = imageHash; + } + + frameFlags = BACKGROUND; + refresh = UNSPECIFIED; +} + +#ifdef EINK_LIMIT_GHOSTING_PX +// Count how many ghost pixels the new image will display +void EInkDynamicDisplay::countGhostPixels() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; + + // Start a new count + ghostPixelCount = 0; + + // Check new image, bit by bit, for any white pixels at locations marked "dirty" + for (uint16_t i = 0; i < displayBufferSize; i++) { + for (uint8_t bit = 0; bit < 7; bit++) { + + const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh? + const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image? + + // If pixel is (or has been) black since last full-refresh, and now is white: ghosting + if (dirty && shouldBeBlank) + ghostPixelCount++; + + // Update the dirty status for this pixel - will this location become a ghost if set white in future? + if (!dirty && !shouldBeBlank) + dirtyPixels[i] |= (1 << bit); + } + } + + LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount); +} + +// Check if ghost pixel count exceeds the defined limit +void EInkDynamicDisplay::checkExcessiveGhosting() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; + + countGhostPixels(); + + // If too many ghost pixels, select full refresh + if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) { + refresh = FULL; + reason = EXCEEDED_GHOSTINGLIMIT; + LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT\n"); + } +} + +// Clear the dirty pixels array. Call when full-refresh cleans the display. +void EInkDynamicDisplay::resetGhostPixelTracking() +{ + // Copy the current frame into dirtyPixels[] from the display buffer + memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); +} +#endif // EINK_LIMIT_GHOSTING_PX + +#endif // USE_EINK_DYNAMICDISPLAY \ No newline at end of file diff --git a/src/graphics/EInkDynamicDisplay.h b/src/graphics/EInkDynamicDisplay.h new file mode 100644 index 0000000000..2880c716b0 --- /dev/null +++ b/src/graphics/EInkDynamicDisplay.h @@ -0,0 +1,104 @@ +#pragma once + +#include "configuration.h" + +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + +#include "EInkDisplay2.h" +#include "GxEPD2_BW.h" + +/* + Derives from the EInkDisplay adapter class. + Accepts suggestions from Screen class about frame type. + Determines which refresh type is most suitable. + (Full, Fast, Skip) +*/ + +class EInkDynamicDisplay : public EInkDisplay +{ + public: + // Constructor + // ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class ) + EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus); + ~EInkDynamicDisplay(); + + // What kind of frame is this + enum frameFlagTypes : uint8_t { + BACKGROUND = (1 << 0), // For frames via display() + RESPONSIVE = (1 << 1), // For frames via forceDisplay() + COSMETIC = (1 << 2), // For splashes + DEMAND_FAST = (1 << 3), // Special case only + }; + void setFrameFlag(frameFlagTypes flag); + + // Set the correct frame flag, then call universal "update()" method + void display() override; + bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused. + + protected: + enum refreshTypes : uint8_t { // Which refresh operation will be used + UNSPECIFIED, + FULL, + FAST, + SKIPPED, + }; + enum reasonTypes : uint8_t { // How was the decision reached + NO_OBJECTIONS, + EXCEEDED_RATELIMIT_FAST, + EXCEEDED_RATELIMIT_FULL, + FLAGGED_COSMETIC, + FLAGGED_DEMAND_FAST, + EXCEEDED_LIMIT_FASTREFRESH, + EXCEEDED_GHOSTINGLIMIT, + FRAME_MATCHED_PREVIOUS, + BACKGROUND_USES_FAST, + FLAGGED_BACKGROUND, + REDRAW_WITH_FULL, + }; + + void configForFastRefresh(); // GxEPD2 code to set fast-refresh + void configForFullRefresh(); // GxEPD2 code to set full-refresh + bool determineMode(); // Assess situation, pick a refresh type + void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type + void adjustRefreshCounters(); // Update fastRefreshCount + bool update(); // Trigger the display update - determine mode, then call base class + + // Checks as part of determineMode() + void checkWasFlooded(); // Was the previous frame skipped for exceeding EINK_LIMIT_RATE_RESPONSIVE_SEC? + void checkRateLimiting(); // Is this frame too soon? + void checkCosmetic(); // Was the COSMETIC flag set? + void checkDemandingFast(); // Was the DEMAND_FAST flag set? + void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively? + void checkFrameMatchesPrevious(); // Does the new frame match the existing display image? + void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND? + + void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting + void hashImage(); // Generate a hashed version of this frame, to compare against previous update + void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call + + // What we are determining for this frame + frameFlagTypes frameFlags = BACKGROUND; // Frame type(s) - determineMode() input + refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output + reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used + + // What happened last time determineMode() ran + refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome + reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason + + uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting) + uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed! + uint32_t previousImageHash = 0; // Hash of the previous update's frame + uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh? + refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for + + // Optional - track ghosting, pixel by pixel +#ifdef EINK_LIMIT_GHOSTING_PX + void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh + void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit + void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. + uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) + uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use +#endif +}; + +#endif \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c92877d41e..33df78462a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -898,9 +898,12 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) || defined(RAK14014) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_EINK) +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index baee4b1400..69e858dd2a 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -47,6 +47,7 @@ class Screen #endif #include "EInkDisplay2.h" +#include "EInkDynamicDisplay.h" #include "TFTDisplay.h" #include "TypedQueue.h" #include "commands.h" diff --git a/variants/heltec_wireless_paper_v1/platformio.ini b/variants/heltec_wireless_paper_v1/platformio.ini index 01b68f5f08..4e5e291e08 100644 --- a/variants/heltec_wireless_paper_v1/platformio.ini +++ b/variants/heltec_wireless_paper_v1/platformio.ini @@ -8,6 +8,12 @@ build_flags = -D EINK_DISPLAY_MODEL=GxEPD2_213_BN -D EINK_WIDTH=250 -D EINK_HEIGHT=122 + -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted + -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + ;-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2/