Skip to content

Commit

Permalink
Async full-refresh for EInkDynamicDisplay (#3339)
Browse files Browse the repository at this point in the history
* Move Wireless Paper V1.1 custom hibernate behavior to GxEPD2

* Async full-refresh for EInkDynamicDisplay

* initial config for T-Echo

* formatting
responds to #3339 (comment)

* increase fast-refresh limit for T-Echo
#3339 (comment)

* change dependency from private repo to meshtastic/GxEPD2

---------

Co-authored-by: Ben Meadors <[email protected]>
  • Loading branch information
todd-herbert and thebentern authored Mar 11, 2024
1 parent 892223a commit cf4753f
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 27 deletions.
25 changes: 11 additions & 14 deletions src/graphics/EInkDisplay2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,24 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
}
}

// Trigger the refresh in GxEPD2
LOG_DEBUG("Updating E-Paper... ");

#if false
// Currently unused; rescued from commented-out line during a refactor
// Use a meaningful macro here if variant doesn't want fast refresh

// Full update mode (slow)
adafruitDisplay->display(false)
#else
// Fast update mode
adafruitDisplay->nextPage();
#endif

#ifndef EINK_NO_HIBERNATE // Only hibernate if controller IC will preserve image memory
// Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display)
adafruitDisplay->hibernate();
#endif
// End the update process
endUpdate();

LOG_DEBUG("done\n");
return true;
}

// End the update process - virtual method, overriden in derived class
void EInkDisplay::endUpdate()
{
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
adafruitDisplay->hibernate();
}

// Write the buffer to the display memory
void EInkDisplay::display(void)
{
Expand Down Expand Up @@ -188,6 +184,7 @@ bool EInkDisplay::connect()
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
adafruitDisplay->clearScreen(); // Clearing now, so the boot logo will draw nice and smoothe (fast refresh)
}
#elif defined(PCA10059)
{
Expand Down
7 changes: 7 additions & 0 deletions src/graphics/EInkDisplay2.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ class EInkDisplay : public OLEDDisplay
*/
virtual bool forceDisplay(uint32_t msecLimit = 1000);

/**
* Run any code needed to complete an update, after the physical refresh has completed.
* Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class.
*
*/
virtual void endUpdate();

/**
* shim to make the abstraction happy
*
Expand Down
90 changes: 81 additions & 9 deletions src/graphics/EInkDynamicDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,29 @@ void EInkDynamicDisplay::adjustRefreshCounters()
// Trigger the display update by calling base class
bool EInkDynamicDisplay::update()
{
// Detemine the refresh mode to use, and start the update
bool refreshApproved = determineMode();
if (refreshApproved)
EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system
return refreshApproved; // (Unutilized) Base class promises to return true if update ran

#if defined(HAS_EINK_ASYNCFULL)
if (refreshApproved)
endOrDetach(); // Either endUpdate() right now (fast refresh), or set the async flag (full refresh)
#endif

return refreshApproved; // (Unutilized) Base class promises to return true if update ran
}

// Assess situation, pick a refresh type
bool EInkDynamicDisplay::determineMode()
{
checkWasFlooded();
checkForPromotion();
#if defined(HAS_EINK_ASYNCFULL)
checkAsyncFullRefresh();
#endif
checkRateLimiting();

// If too soon for a new time, abort here
// If too soon for a new frame, or display busy, abort early
if (refresh == SKIPPED) {
storeAndReset();
return false; // No refresh
Expand All @@ -116,7 +126,7 @@ bool EInkDynamicDisplay::determineMode()

resetRateLimiting(); // Once determineMode() ends, will have to wait again
hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check
LOG_DEBUG("EInkDynamicDisplay: "); // Begin log entry
LOG_DEBUG("determineMode(): "); // Begin log entry

// Once mode determined, any remaining checks will bypass
checkCosmetic();
Expand Down Expand Up @@ -151,13 +161,25 @@ bool EInkDynamicDisplay::determineMode()
}
}

// Did RESPONSIVE frames previously exceed the rate-limit for fast refresh?
void EInkDynamicDisplay::checkWasFlooded()
// Was a frame skipped (rate, display busy) that should have been a FAST refresh?
void EInkDynamicDisplay::checkForPromotion()
{
if (previousReason == EXCEEDED_RATELIMIT_FAST) {
// If so, allow a BACKGROUND frame to draw as RESPONSIVE
// Because we DID want a RESPONSIVE frame last time, we just didn't get it
// If a frame was skipped (rate, display busy), then promote a BACKGROUND frame
// Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it

switch (previousReason) {
case ASYNC_REFRESH_BLOCKED_DEMANDFAST:
setFrameFlag(DEMAND_FAST);
break;
case ASYNC_REFRESH_BLOCKED_COSMETIC:
setFrameFlag(COSMETIC);
break;
case ASYNC_REFRESH_BLOCKED_RESPONSIVE:
case EXCEEDED_RATELIMIT_FAST:
setFrameFlag(RESPONSIVE);
break;
default:
break;
}
}

Expand Down Expand Up @@ -381,4 +403,54 @@ void EInkDynamicDisplay::resetGhostPixelTracking()
}
#endif // EINK_LIMIT_GHOSTING_PX

#ifdef HAS_EINK_ASYNCFULL
// Check the status of an "async full-refresh", and run the finish-up code if the hardware is ready
void EInkDynamicDisplay::checkAsyncFullRefresh()
{
// No refresh taking place, continue with determineMode()
if (!asyncRefreshRunning)
return;

// Full refresh still running
if (adafruitDisplay->epd2.isBusy()) {
// No refresh
refresh = SKIPPED;

// Set the reason, marking what type of frame we're skipping
if (frameFlags & DEMAND_FAST)
reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST;
else if (frameFlags & COSMETIC)
reason = ASYNC_REFRESH_BLOCKED_COSMETIC;
else if (frameFlags & RESPONSIVE)
reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE;
else
reason = ASYNC_REFRESH_BLOCKED_BACKGROUND;

return;
}

// If we asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done
adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code
EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override)
asyncRefreshRunning = false; // Unset the flag
LOG_DEBUG("Async full-refresh complete\n");

// Note: this code only works because of a modification to meshtastic/GxEPD2.
// It is only equipped to intercept calls to nextPage()
}

// Figure out who runs the post-update code
void EInkDynamicDisplay::endOrDetach()
{
if (previousRefresh == FULL) { // Note: previousRefresh is the refresh from this loop.
asyncRefreshRunning = true; // Set the flag - picked up at start of determineMode(), next loop.
LOG_DEBUG("Async full-refresh begins\n");
}

// Fast Refresh
else
EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves.
}
#endif // HAS_EINK_ASYNCFULL

#endif // USE_EINK_DYNAMICDISPLAY
15 changes: 14 additions & 1 deletion src/graphics/EInkDynamicDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class EInkDynamicDisplay : public EInkDisplay
};
enum reasonTypes : uint8_t { // How was the decision reached
NO_OBJECTIONS,
ASYNC_REFRESH_BLOCKED_DEMANDFAST,
ASYNC_REFRESH_BLOCKED_COSMETIC,
ASYNC_REFRESH_BLOCKED_RESPONSIVE,
ASYNC_REFRESH_BLOCKED_BACKGROUND,
DISPLAY_NOT_READY_FOR_FULL,
EXCEEDED_RATELIMIT_FAST,
EXCEEDED_RATELIMIT_FULL,
FLAGGED_COSMETIC,
Expand All @@ -64,7 +69,7 @@ class EInkDynamicDisplay : public EInkDisplay
bool update(); // Trigger the display update - determine mode, then call base class

// Checks as part of determineMode()
void checkWasFlooded(); // Was the previous frame skipped for exceeding EINK_LIMIT_RATE_RESPONSIVE_SEC?
void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh?
void checkRateLimiting(); // Is this frame too soon?
void checkCosmetic(); // Was the COSMETIC flag set?
void checkDemandingFast(); // Was the DEMAND_FAST flag set?
Expand Down Expand Up @@ -99,6 +104,14 @@ class EInkDynamicDisplay : public EInkDisplay
uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
#endif

// Conditional - async full refresh - only with modified meshtastic/GxEPD2
#if defined(HAS_EINK_ASYNCFULL)
void checkAsyncFullRefresh(); // Check the status of "async full-refresh"; run the post-update code if the hardware is ready
void endOrDetach(); // Run the post-update code, or delegate it off to checkAsyncFullRefresh()
void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay()
bool asyncRefreshRunning = false; // Flag, checked by checkAsyncFullRefresh()
#endif
};

#endif
2 changes: 1 addition & 1 deletion variants/heltec_wireless_paper/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ build_flags =
-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
lib_deps =
${esp32s3_base.lib_deps}
https://github.com/meshtastic/GxEPD2
https://github.com/meshtastic/GxEPD2/
adafruit/Adafruit BusIO@^1.13.2
lewisxhe/PCF8563_Library@^1.0.1
upload_speed = 115200
1 change: 0 additions & 1 deletion variants/heltec_wireless_paper/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#define I2C_SCL SCL

#define USE_EINK
#define EINK_NO_HIBERNATE

/*
* eink display pins
Expand Down
8 changes: 7 additions & 1 deletion variants/t-echo/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/t-echo
-DEINK_DISPLAY_MODEL=GxEPD2_154_D67
-DEINK_WIDTH=200
-DEINK_HEIGHT=200
-DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk
-DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted
-DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates
-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
-DEINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/t-echo>
lib_deps =
${nrf52840_base.lib_deps}
https://github.com/meshtastic/GxEPD2#afce87a97dda1ac31d8a28dc8fa7c6f55dc96a61
https://github.com/meshtastic/GxEPD2
adafruit/Adafruit BusIO@^1.13.2
lewisxhe/PCF8563_Library@^1.0.1
;upload_protocol = fs

0 comments on commit cf4753f

Please sign in to comment.