From 00d742e0fb1fac474dbc35cb7bb7c99e36437caa Mon Sep 17 00:00:00 2001 From: thealhu Date: Sun, 3 Mar 2024 13:28:56 +0100 Subject: [PATCH] Revert "Reimplement "Dynamic E-Ink" as a derived class (#3316)" --- src/graphics/EInkDisplay2.cpp | 2 +- src/graphics/EInkDisplay2.h | 4 +- src/graphics/EInkDynamicDisplay.cpp | 384 ------------------ src/graphics/EInkDynamicDisplay.h | 104 ----- src/graphics/Screen.cpp | 5 +- src/graphics/Screen.h | 1 - .../heltec_wireless_paper_v1/platformio.ini | 6 - 7 files changed, 3 insertions(+), 503 deletions(-) delete mode 100644 src/graphics/EInkDynamicDisplay.cpp delete mode 100644 src/graphics/EInkDynamicDisplay.h diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index d790e30c18..d715d398b8 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 3693781320..f40747f266 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -13,8 +13,6 @@ /** * 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) @@ -43,7 +41,7 @@ class EInkDisplay : public OLEDDisplay * * @return true if we did draw the screen */ - virtual bool forceDisplay(uint32_t msecLimit = 1000); + bool forceDisplay(uint32_t msecLimit = 1000); /** * shim to make the abstraction happy diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp deleted file mode 100644 index ae1e30fe1e..0000000000 --- a/src/graphics/EInkDynamicDisplay.cpp +++ /dev/null @@ -1,384 +0,0 @@ -#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 deleted file mode 100644 index 2880c716b0..0000000000 --- a/src/graphics/EInkDynamicDisplay.h +++ /dev/null @@ -1,104 +0,0 @@ -#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 bc66094405..06af4318b2 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -898,12 +898,9 @@ 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) && !defined(USE_EINK_DYNAMICDISPLAY) +#elif defined(USE_EINK) 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 69e858dd2a..baee4b1400 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -47,7 +47,6 @@ 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 4e5e291e08..01b68f5f08 100644 --- a/variants/heltec_wireless_paper_v1/platformio.ini +++ b/variants/heltec_wireless_paper_v1/platformio.ini @@ -8,12 +8,6 @@ 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/