Skip to content

Commit

Permalink
feat: E-Ink "Dynamic Partial" (meshtastic#3193)
Browse files Browse the repository at this point in the history
Use a mixture of full refresh, partial refresh, and skipped updates, balancing urgency and display health.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
  • Loading branch information
2 people authored and GUVWAF committed Feb 11, 2024
1 parent 2ac6f41 commit baeefee
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 34 deletions.
209 changes: 175 additions & 34 deletions src/graphics/EInkDisplay2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
64 changes: 64 additions & 0 deletions src/graphics/EInkDisplay2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
8 changes: 8 additions & 0 deletions variants/heltec_wireless_paper_v1/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down

0 comments on commit baeefee

Please sign in to comment.