mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-20 09:43:03 +00:00
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 <benmmeadors@gmail.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
// 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... ");
|
||||
|
||||
#if defined(TTGO_T_ECHO)
|
||||
adafruitDisplay->nextPage();
|
||||
#elif defined(RAK4630) || defined(MAKERPYTHON)
|
||||
|
||||
// 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
|
||||
|
||||
// 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();
|
||||
#elif defined(HELTEC_WIRELESS_PAPER_V1_0)
|
||||
adafruitDisplay->nextPage();
|
||||
#elif defined(HELTEC_WIRELESS_PAPER)
|
||||
adafruitDisplay->nextPage();
|
||||
#elif defined(PRIVATE_HW) || defined(my)
|
||||
adafruitDisplay->nextPage();
|
||||
else
|
||||
return false;
|
||||
|
||||
#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");
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// LOG_DEBUG("Skipping eink display\n");
|
||||
return false;
|
||||
// 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... ");
|
||||
|
||||
#if defined(TTGO_T_ECHO)
|
||||
adafruitDisplay->nextPage();
|
||||
#elif defined(RAK4630) || defined(MAKERPYTHON)
|
||||
|
||||
// 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
|
||||
|
||||
// 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();
|
||||
#elif defined(HELTEC_WIRELESS_PAPER_V1_0)
|
||||
adafruitDisplay->nextPage();
|
||||
#elif defined(HELTEC_WIRELESS_PAPER)
|
||||
adafruitDisplay->nextPage();
|
||||
#elif defined(PRIVATE_HW) || defined(my)
|
||||
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");
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user