Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E-Ink "Dynamic Partial" #3193

Merged
merged 2 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
markbirss marked this conversation as resolved.
Show resolved Hide resolved
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
Loading