From 36cf9b9ef4161a3b0721fe5d68a66193b886c5c3 Mon Sep 17 00:00:00 2001 From: todd-herbert Date: Mon, 12 Feb 2024 02:27:22 +1300 Subject: [PATCH 1/2] feat: E-Ink "Dynamic Partial" (#3193) Use a mixture of full refresh, partial refresh, and skipped updates, balancing urgency and display health. Co-authored-by: Ben Meadors --- src/graphics/EInkDisplay2.cpp | 209 ++++++++++++++++---- src/graphics/EInkDisplay2.h | 64 ++++++ variants/heltec_wireless_paper_v1/variant.h | 8 + 3 files changed, 247 insertions(+), 34 deletions(-) diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 09ea343e1b..51d7ac5f8f 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -125,61 +125,68 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) // No need to grab this lock because we are on our own SPI bus // concurrency::LockGuard g(spiLock); +#if defined(USE_EINK_DYNAMIC_PARTIAL) + // Decide if update is partial or full + bool continueUpdate = determineRefreshMode(); + if (!continueUpdate) + return false; +#else + uint32_t now = millis(); uint32_t sinceLast = now - lastDrawMsec; - if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) { + if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) lastDrawMsec = now; + else + return false; + +#endif - // FIXME - only draw bits have changed (use backbuf similar to the other displays) - // tft.drawBitmap(0, 0, buffer, 128, 64, TFT_YELLOW, TFT_BLACK); - for (uint32_t y = 0; y < displayHeight; y++) { - for (uint32_t x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient - auto b = buffer[x + (y / 8) * displayWidth]; - auto isset = b & (1 << (y & 7)); - adafruitDisplay->drawPixel(x, y, isset ? COLORED : UNCOLORED); - } + // FIXME - only draw bits have changed (use backbuf similar to the other displays) + // tft.drawBitmap(0, 0, buffer, 128, 64, TFT_YELLOW, TFT_BLACK); + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel(x, y, isset ? COLORED : UNCOLORED); } + } - LOG_DEBUG("Updating E-Paper... "); + LOG_DEBUG("Updating E-Paper... "); #if defined(TTGO_T_ECHO) - adafruitDisplay->nextPage(); + adafruitDisplay->nextPage(); #elif defined(RAK4630) || defined(MAKERPYTHON) - // RAK14000 2.13 inch b/w 250x122 actually now does support partial updates + // RAK14000 2.13 inch b/w 250x122 actually now does support partial updates - // Full update mode (slow) - // adafruitDisplay->display(false); // FIXME, use partial update mode + // Full update mode (slow) + // adafruitDisplay->display(false); // FIXME, use partial update mode - // Only enable for e-Paper with support for partial updates and comment out above adafruitDisplay->display(false); - // 1.54 inch 200x200 - GxEPD2_154_M09 - // 2.13 inch 250x122 - GxEPD2_213_BN - // 2.9 inch 296x128 - GxEPD2_290_T5D - // 4.2 inch 300x400 - GxEPD2_420_M01 - adafruitDisplay->nextPage(); + // Only enable for e-Paper with support for partial updates and comment out above adafruitDisplay->display(false); + // 1.54 inch 200x200 - GxEPD2_154_M09 + // 2.13 inch 250x122 - GxEPD2_213_BN + // 2.9 inch 296x128 - GxEPD2_290_T5D + // 4.2 inch 300x400 - GxEPD2_420_M01 + adafruitDisplay->nextPage(); #elif defined(PCA10059) || defined(M5_COREINK) - adafruitDisplay->nextPage(); + adafruitDisplay->nextPage(); #elif defined(HELTEC_WIRELESS_PAPER_V1_0) - adafruitDisplay->nextPage(); + adafruitDisplay->nextPage(); #elif defined(HELTEC_WIRELESS_PAPER) - adafruitDisplay->nextPage(); + adafruitDisplay->nextPage(); #elif defined(PRIVATE_HW) || defined(my) - adafruitDisplay->nextPage(); + adafruitDisplay->nextPage(); #endif - // Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display) - adafruitDisplay->hibernate(); - LOG_DEBUG("done\n"); + // Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display) + adafruitDisplay->hibernate(); + LOG_DEBUG("done\n"); - return true; - } else { - // LOG_DEBUG("Skipping eink display\n"); - return false; - } + return true; } // Write the buffer to the display memory @@ -188,8 +195,16 @@ void EInkDisplay::display(void) // We don't allow regular 'dumb' display() calls to draw on eink until we've shown // at least one forceDisplay() keyframe. This prevents flashing when we should the critical // bootscreen (that we want to look nice) - if (lastDrawMsec) + +#ifdef USE_EINK_DYNAMIC_PARTIAL + lowPriority(); + forceDisplay(); + highPriority(); +#else + if (lastDrawMsec) { forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower + } +#endif } // Send a command to the display (low level function) @@ -329,4 +344,130 @@ bool EInkDisplay::connect() return true; } +// Use a mix of full and partial refreshes, to preserve display health +#if defined(USE_EINK_DYNAMIC_PARTIAL) + +// Suggest that subsequent updates should use partial-refresh +void EInkDisplay::highPriority() +{ + isHighPriority = true; +} + +// Suggest that subsequent updates should use full-refresh +void EInkDisplay::lowPriority() +{ + isHighPriority = false; +} + +// configure display for partial-refresh +void EInkDisplay::configForPartialRefresh() +{ + // Display-specific code can go here +#if defined(PRIVATE_HW) +#else + // Otherwise: + adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); +#endif +} + +// Configure display for full-refresh +void EInkDisplay::configForFullRefresh() +{ + // Display-specific code can go here +#if defined(PRIVATE_HW) +#else + // Otherwise: + adafruitDisplay->setFullWindow(); +#endif +} + +bool EInkDisplay::newImageMatchesOld() +{ + uint32_t newImageHash = 0; + + // Generate hash: sum all bytes in the image buffer + for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { + newImageHash += buffer[b]; + } + + // Compare hashes + bool hashMatches = (newImageHash == prevImageHash); + + // Update the cached hash + prevImageHash = newImageHash; + + // Return the comparison result + return hashMatches; +} + +// Change between partial and full refresh config, or skip update, balancing urgency and display health. +bool EInkDisplay::determineRefreshMode() +{ + uint32_t now = millis(); + uint32_t sinceLast = now - lastUpdateMsec; + + // If rate-limiting dropped a high-priority update: + // promote this update, so it runs ASAP + if (missedHighPriorityUpdate) { + isHighPriority = true; + missedHighPriorityUpdate = false; + } + + // Abort: if too soon for a new frame + if (isHighPriority && partialRefreshCount > 0 && sinceLast < highPriorityLimitMsec) { + LOG_DEBUG("Update skipped: exceeded EINK_HIGHPRIORITY_LIMIT_SECONDS\n"); + missedHighPriorityUpdate = true; + return false; + } + if (!isHighPriority && sinceLast < lowPriorityLimitMsec) { + return false; + } + + // Check if old image (partial) should be redrawn (as full), for image quality + if (partialRefreshCount > 0 && !isHighPriority) + needsFull = true; + + // If too many partials, require a full-refresh (display health) + if (partialRefreshCount >= partialRefreshLimit) + needsFull = true; + + // If image matches + if (newImageMatchesOld()) { + // If low priority: limit rate + // otherwise, every loop() will run the hash method + if (!isHighPriority) + lastUpdateMsec = now; + + // If update is *not* for display health or image quality, skip it + if (!needsFull) + return false; + } + + // Conditions assessed - not skipping - load the appropriate config + + // If options require a full refresh + if (!isHighPriority || needsFull) { + if (partialRefreshCount > 0) + configForFullRefresh(); + + LOG_DEBUG("Conditions met for full-refresh\n"); + partialRefreshCount = 0; + needsFull = false; + } + + // If options allow a partial refresh + else { + if (partialRefreshCount == 0) + configForPartialRefresh(); + + LOG_DEBUG("Conditions met for partial-refresh\n"); + partialRefreshCount++; + } + + lastUpdateMsec = now; // Mark time for rate limiting + return true; // Instruct calling method to continue with update +} + +#endif // End USE_EINK_DYNAMIC_PARTIAL + #endif \ No newline at end of file diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 7bbf07069a..91261c865d 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -54,4 +54,68 @@ class EInkDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; + +#if defined(USE_EINK_DYNAMIC_PARTIAL) + // Full, partial, or skip: balance urgency with display health + + // Use partial refresh if EITHER: + // * highPriority() was set + // * a highPriority() update was previously skipped, for rate-limiting - (EINK_HIGHPRIORITY_LIMIT_SECONDS) + + // Use full refresh if EITHER: + // * lowPriority() was set + // * too many partial updates in a row: protect display - (EINK_PARTIAL_REPEAT_LIMIT) + // * no recent updates, and last update was partial: redraw for image quality (EINK_LOWPRIORITY_LIMIT_SECONDS) + + // Rate limit if: + // * lowPriority() - (EINK_LOWPRIORITY_LIMIT_SECONDS) + // * highPriority(), if multiple partials have run back-to-back - (EINK_HIGHPRIORITY_LIMIT_SECONDS) + + // Skip update entirely if ALL criteria met: + // * new image matches old image + // * lowPriority() + // * not redrawing for image quality + // * not refreshing for display health + + // ------------------------------------ + + // To implement for your E-Ink display: + // * edit configForPartialRefresh() + // * edit configForFullRefresh() + // * add macros to variant.h, and adjust to taste: + + /* + #define USE_EINK_DYNAMIC_PARTIAL + #define EINK_LOWPRIORITY_LIMIT_SECONDS 30 + #define EINK_HIGHPRIORITY_LIMIT_SECONDS 1 + #define EINK_PARTIAL_REPEAT_LIMIT 5 + */ + + public: + void highPriority(); // Suggest partial refresh + void lowPriority(); // Suggest full refresh + + protected: + void configForPartialRefresh(); // Display specific code to select partial refresh mode + void configForFullRefresh(); // Display specific code to return to full refresh mode + bool newImageMatchesOld(); // Is the new update actually different to the last image? + bool determineRefreshMode(); // Called immediately before data written to display - choose refresh mode, or abort update + + bool isHighPriority = true; // Does the method calling update believe that this is urgent? + bool needsFull = false; // Is a full refresh forced? (display health) + bool missedHighPriorityUpdate = false; // Was a high priority update skipped for rate-limiting? + uint16_t partialRefreshCount = 0; // How many partials have occurred since last full refresh? + uint32_t lastUpdateMsec = 0; // When did the last update occur? + uint32_t prevImageHash = 0; // Used to check if update will change screen image (skippable or not) + + // Set in variant.h + const uint32_t lowPriorityLimitMsec = (uint32_t)1000 * EINK_LOWPRIORITY_LIMIT_SECONDS; // Max rate for partial refreshes + const uint32_t highPriorityLimitMsec = (uint32_t)1000 * EINK_HIGHPRIORITY_LIMIT_SECONDS; // Max rate for full refreshes + const uint32_t partialRefreshLimit = EINK_PARTIAL_REPEAT_LIMIT; // Max consecutive partials, before full is triggered + +#else // !USE_EINK_DYNAMIC_PARTIAL + // Tolerate calls to these methods anywhere, just to be safe + void highPriority() {} + void lowPriority() {} +#endif }; diff --git a/variants/heltec_wireless_paper_v1/variant.h b/variants/heltec_wireless_paper_v1/variant.h index 4daf9a655f..7a4e54ca90 100644 --- a/variants/heltec_wireless_paper_v1/variant.h +++ b/variants/heltec_wireless_paper_v1/variant.h @@ -5,6 +5,14 @@ #define I2C_SCL SCL #define USE_EINK + +// Settings for Dynamic Partial mode +// Change between partial and full refresh config, or skip update, balancing urgency and display health. +#define USE_EINK_DYNAMIC_PARTIAL +#define EINK_LOWPRIORITY_LIMIT_SECONDS 30 +#define EINK_HIGHPRIORITY_LIMIT_SECONDS 1 +#define EINK_PARTIAL_REPEAT_LIMIT 5 + /* * eink display pins */ From 96bd898a382f84bed475190c68a80da234266373 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Feb 2024 07:43:07 -0600 Subject: [PATCH 2/2] [create-pull-request] automated change (#3209) Co-authored-by: thebentern --- protobufs | 2 +- src/mesh/generated/meshtastic/storeforward.pb.h | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 6cb18782b1..20f2783e19 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 6cb18782b1c446a4ca4797dcf5bb2da765b6e5a0 +Subproject commit 20f2783e196da1429de4b0fcf05c7ffea98d7901 diff --git a/src/mesh/generated/meshtastic/storeforward.pb.h b/src/mesh/generated/meshtastic/storeforward.pb.h index 151f6211b8..55ab0b5108 100644 --- a/src/mesh/generated/meshtastic/storeforward.pb.h +++ b/src/mesh/generated/meshtastic/storeforward.pb.h @@ -30,6 +30,10 @@ typedef enum _meshtastic_StoreAndForward_RequestResponse { meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY = 6, /* Router is responding to a request for stats. */ meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS = 7, + /* Router sends a text message from its history that was a direct message. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT = 8, + /* Router sends a text message from its history that was a broadcast. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST = 9, /* Client is an in error state. */ meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR = 64, /* Client has requested a replay from the router. */