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

Handle "special-frames" with EInkDynamicDisplay #3356

Merged
merged 25 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5d4d91f
Move Wireless Paper V1.1 custom hibernate behavior to GxEPD2
todd-herbert Mar 5, 2024
07da130
Async full-refresh for EInkDynamicDisplay
todd-herbert Mar 6, 2024
ac89bb3
initial config for T-Echo
todd-herbert Mar 8, 2024
7275c21
formatting
todd-herbert Mar 8, 2024
2392621
increase fast-refresh limit for T-Echo
todd-herbert Mar 8, 2024
42286ed
Merge branch 'master' into eink-async
thebentern Mar 9, 2024
aaa5d61
Merge branch 'master' into eink-async
thebentern Mar 9, 2024
d5c11d1
change dependency from private repo to meshtastic/GxEPD2
todd-herbert Mar 9, 2024
576f582
rename setFrameFlag() method
todd-herbert Mar 9, 2024
efd818f
move storeAndReset() to end of update()
todd-herbert Mar 9, 2024
95b6f27
change order of determineMode() checks
todd-herbert Mar 9, 2024
94794ed
add init code as a determineMode() check
todd-herbert Mar 9, 2024
e232e34
add BLOCKING modifier to frameFlagTypes
todd-herbert Mar 9, 2024
a9c07a4
add frameFlags to LOG_DEBUG() messages
todd-herbert Mar 9, 2024
94eb837
function macro for tidier addFramFlag() calls
todd-herbert Mar 9, 2024
7b70324
handle special frames in Screen.cpp
todd-herbert Mar 9, 2024
dfbb4cd
Merge branch 'master' into eink-special-frames
thebentern Mar 9, 2024
3daae24
fix fallback behavior for unmodified GxEPD2
todd-herbert Mar 10, 2024
c0a3b20
while drafting, build from todd-herbert/meshtastic-GxEPD2#async
todd-herbert Mar 10, 2024
1032e16
reorder determineMode() checks
todd-herbert Mar 10, 2024
4b4bd07
Merge branch 'master' into eink-special-frames
caveman99 Mar 11, 2024
1d31be9
Swap Wireless Paper V1.0 dependency to meshtastic/GxEPD2
todd-herbert Mar 11, 2024
1f766a0
purge unused enum val
todd-herbert Mar 11, 2024
95967a0
Merge branch 'master' into eink-special-frames
thebentern Mar 11, 2024
2d5a6c1
Merge branch 'master' into eink-special-frames
thebentern Mar 11, 2024
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
24 changes: 10 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
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
204 changes: 160 additions & 44 deletions src/graphics/EInkDynamicDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ EInkDynamicDisplay::~EInkDynamicDisplay()
// Screen requests a BACKGROUND frame
void EInkDynamicDisplay::display()
{
setFrameFlag(BACKGROUND);
addFrameFlag(BACKGROUND);
update();
}

// Screen requests a RESPONSIVE frame
bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit)
{
setFrameFlag(RESPONSIVE);
addFrameFlag(RESPONSIVE);
return update(); // (Unutilized) Base class promises to return true if update ran
}

// Add flag for the next frame
void EInkDynamicDisplay::setFrameFlag(frameFlagTypes flag)
void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag)
{
// OR the new flag into the existing flags
this->frameFlags = (frameFlagTypes)(this->frameFlags | flag);
Expand Down Expand Up @@ -94,38 +94,75 @@ 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)
if (refreshApproved) {
EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system
return refreshApproved; // (Unutilized) Base class promises to return true if update ran
storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach()
endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL)
} else
storeAndReset(); // No update, no post-update code, just store the results

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

// Figure out who runs the post-update code
void EInkDynamicDisplay::endOrDetach()
{
// If the GxEPD2 version reports that it has the async modifications
#ifdef HAS_EINK_ASYNCFULL
if (previousRefresh == FULL) {
asyncRefreshRunning = true; // Set the flag - picked up at start of determineMode(), next loop.

if (previousFrameFlags & BLOCKING)
awaitRefresh();
else
LOG_DEBUG("Async full-refresh begins\n");
}

// Fast Refresh
else if (previousRefresh == FAST)
EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves.

// Fallback - If using an unmodified version of GxEPD2 for some reason
#else
if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..)
LOG_WARN(
"GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update lib_deps in "
"variant's platformio.ini file\n");
EInkDisplay::endUpdate();
}
#endif
}

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

// If too soon for a new time, abort here
if (refresh == SKIPPED) {
storeAndReset();
// If too soon for a new frame, or display busy, abort early
if (refresh == SKIPPED)
return false; // No refresh
}

// -- New frame is due --

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();
checkDemandingFast();
checkFrameMatchesPrevious();
checkConsecutiveFastRefreshes();
#ifdef EINK_LIMIT_GHOSTING_PX
checkExcessiveGhosting();
#endif
checkFrameMatchesPrevious();
checkFastRequested();

if (refresh == UNSPECIFIED)
Expand All @@ -142,22 +179,49 @@ bool EInkDynamicDisplay::determineMode()
#endif

// Return - call a refresh or not?
if (refresh == SKIPPED) {
storeAndReset();
if (refresh == SKIPPED)
return false; // Don't trigger a refresh
} else {
storeAndReset();
else
return true; // Do trigger a refresh
}

// Is this the very first frame?
void EInkDynamicDisplay::checkInitialized()
{
if (!initialized) {
// Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect()
configForFullRefresh();

// Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write
adafruitDisplay->clearScreen();

LOG_DEBUG("initialized, ");
initialized = true;

// Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep
addFrameFlag(DEMAND_FAST);
}
}

// 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
setFrameFlag(RESPONSIVE);
// 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:
addFrameFlag(DEMAND_FAST);
break;
case ASYNC_REFRESH_BLOCKED_COSMETIC:
addFrameFlag(COSMETIC);
break;
case ASYNC_REFRESH_BLOCKED_RESPONSIVE:
case EXCEEDED_RATELIMIT_FAST:
addFrameFlag(RESPONSIVE);
break;
default:
break;
}
}

Expand Down Expand Up @@ -204,7 +268,7 @@ void EInkDynamicDisplay::checkCosmetic()
if (frameFlags & COSMETIC) {
refresh = FULL;
reason = FLAGGED_COSMETIC;
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC\n");
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x\n", frameFlags);
}
}

Expand All @@ -219,22 +283,7 @@ void EInkDynamicDisplay::checkDemandingFast()
if (frameFlags & DEMAND_FAST) {
refresh = FAST;
reason = FLAGGED_DEMAND_FAST;
LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST\n");
}
}

// Have too many fast-refreshes occured consecutively, since last full refresh?
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;

// If too many FAST refreshes consecutively - force a FULL refresh
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
refresh = FULL;
reason = EXCEEDED_LIMIT_FASTREFRESH;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH\n");
LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x\n", frameFlags);
}
}

Expand All @@ -254,15 +303,30 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
if (frameFlags == BACKGROUND && fastRefreshCount > 0) {
refresh = FULL;
reason = REDRAW_WITH_FULL;
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL\n");
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x\n", frameFlags);
return;
}
#endif

// Not redrawn, not COSMETIC, not DEMAND_FAST
refresh = SKIPPED;
reason = FRAME_MATCHED_PREVIOUS;
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS\n");
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x\n", frameFlags);
}

// Have too many fast-refreshes occured consecutively, since last full refresh?
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;

// If too many FAST refreshes consecutively - force a FULL refresh
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
refresh = FULL;
reason = EXCEEDED_LIMIT_FASTREFRESH;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x\n", frameFlags);
}
}

// No objections, we can perform fast-refresh, if desired
Expand All @@ -276,7 +340,8 @@ void EInkDynamicDisplay::checkFastRequested()
// If we want BACKGROUND to use fast. (FULL only when a limit is hit)
refresh = FAST;
reason = BACKGROUND_USES_FAST;
LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu\n", fastRefreshCount);
LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x\n", fastRefreshCount,
frameFlags);
#else
// If we do want to use FULL for BACKGROUND updates
refresh = FULL;
Expand All @@ -289,7 +354,7 @@ void EInkDynamicDisplay::checkFastRequested()
if (frameFlags & RESPONSIVE) {
refresh = FAST;
reason = NO_OBJECTIONS;
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu\n", fastRefreshCount);
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x\n", fastRefreshCount, frameFlags);
}
}

Expand All @@ -313,6 +378,7 @@ void EInkDynamicDisplay::hashImage()
// Store the results of determineMode() for future use, and reset for next call
void EInkDynamicDisplay::storeAndReset()
{
previousFrameFlags = frameFlags;
previousRefresh = refresh;
previousReason = reason;

Expand Down Expand Up @@ -369,7 +435,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) {
refresh = FULL;
reason = EXCEEDED_GHOSTINGLIMIT;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT\n");
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x\n", frameFlags);
}
}

Expand All @@ -381,4 +447,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()
}

// Hold control while an async refresh runs
void EInkDynamicDisplay::awaitRefresh()
{
// Continually poll the BUSY pin
while (adafruitDisplay->epd2.isBusy())
yield();

// End the full-refresh process
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
}
#endif // HAS_EINK_ASYNCFULL

#endif // USE_EINK_DYNAMICDISPLAY
Loading
Loading