mirror of
https://github.com/meshtastic/firmware.git
synced 2025-08-06 05:34:45 +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:
parent
ce8673b6dc
commit
36cf9b9ef4
@ -125,11 +125,22 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
|||||||
// No need to grab this lock because we are on our own SPI bus
|
// No need to grab this lock because we are on our own SPI bus
|
||||||
// concurrency::LockGuard g(spiLock);
|
// 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 now = millis();
|
||||||
uint32_t sinceLast = now - lastDrawMsec;
|
uint32_t sinceLast = now - lastDrawMsec;
|
||||||
|
|
||||||
if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) {
|
if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0))
|
||||||
lastDrawMsec = now;
|
lastDrawMsec = now;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
// FIXME - only draw bits have changed (use backbuf similar to the other displays)
|
// FIXME - only draw bits have changed (use backbuf similar to the other displays)
|
||||||
// tft.drawBitmap(0, 0, buffer, 128, 64, TFT_YELLOW, TFT_BLACK);
|
// tft.drawBitmap(0, 0, buffer, 128, 64, TFT_YELLOW, TFT_BLACK);
|
||||||
@ -176,10 +187,6 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
|||||||
LOG_DEBUG("done\n");
|
LOG_DEBUG("done\n");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
// LOG_DEBUG("Skipping eink display\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the buffer to the display memory
|
// 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
|
// 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
|
// at least one forceDisplay() keyframe. This prevents flashing when we should the critical
|
||||||
// bootscreen (that we want to look nice)
|
// 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
|
forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a command to the display (low level function)
|
// Send a command to the display (low level function)
|
||||||
@ -329,4 +344,130 @@ bool EInkDisplay::connect()
|
|||||||
return true;
|
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
|
#endif
|
@ -54,4 +54,68 @@ class EInkDisplay : public OLEDDisplay
|
|||||||
|
|
||||||
// Connect to the display
|
// Connect to the display
|
||||||
virtual bool connect() override;
|
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
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,14 @@
|
|||||||
#define I2C_SCL SCL
|
#define I2C_SCL SCL
|
||||||
|
|
||||||
#define USE_EINK
|
#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
|
* eink display pins
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user