Skip to content

Commit

Permalink
E-Ink Screensaver (#3477)
Browse files Browse the repository at this point in the history
* fix Wireless Paper double-clear screen at boot

* log when flooded with "responsive" frames

* show the "resuming" screen when waking from deep-sleep

* rename drawDeepSleepScreen
avoid future confusion with "Screen Paused" screen

* show a screensaver frame when screen off
The frame shown during deep sleep is now also passed through showScreensaverFrames()

* Add macros for E-Ink color values.
OLEDDISPLAY_COLOR is inverted. Result of light-mode on E-Ink vs dark-mode on OLED?

* adapt drawDeepSleepScreen to new screensaver convention

* Mark Wireless Paper V1.1 as having problems with ghosting
Any other issues can be marked in a similar way, then handled in code where relevant

* Change screensaver from fullscreen logo to overlay

* identify "quirks" rather than "problems"

* move async refresh polling from display() to a NotifiedWorkerThread

* Prevent skipping of deep-sleep screen
(Hopefully)

* Redesign screensaver overlay
Now displays short name

* Optimize refresh for different displays

* Support older EInkDisplay class

* Don't assume text alignment

* fix spelling of a quirk macro
(No impact to code, but avoids future issues)

* Handle impossibly unlikely millis() overflow error
Should have just let it go, but here we are..

---------

Co-authored-by: Ben Meadors <[email protected]>
  • Loading branch information
todd-herbert and thebentern authored Mar 28, 2024
1 parent daa4d38 commit 8187fa7
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 43 deletions.
1 change: 0 additions & 1 deletion src/graphics/EInkDisplay2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ 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
62 changes: 46 additions & 16 deletions src/graphics/EInkDynamicDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

// Constructor
EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
: EInkDisplay(address, sda, scl, geometry, i2cBus)
: EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay")
{
// If tracking ghost pixels, grab memory
#ifdef EINK_LIMIT_GHOSTING_PX
Expand Down Expand Up @@ -112,12 +112,15 @@ 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.
asyncRefreshRunning = true; // Set the flag - checked in determineMode(); cleared by onNotify()

if (previousFrameFlags & BLOCKING)
awaitRefresh();
else
LOG_DEBUG("Async full-refresh begins\n");
else {
// Async begins
LOG_DEBUG("Async full-refresh begins (dropping frames)\n");
notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread
}
}

// Fast Refresh
Expand All @@ -141,7 +144,7 @@ bool EInkDynamicDisplay::determineMode()
checkInitialized();
checkForPromotion();
#if defined(HAS_EINK_ASYNCFULL)
checkAsyncFullRefresh();
checkBusyAsyncRefresh();
#endif
checkRateLimiting();

Expand Down Expand Up @@ -252,6 +255,7 @@ void EInkDynamicDisplay::checkRateLimiting()
if (now - previousRunMs < EINK_LIMIT_RATE_RESPONSIVE_SEC * 1000) {
refresh = SKIPPED;
reason = EXCEEDED_RATELIMIT_FAST;
LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x\n", frameFlags);
return;
}
}
Expand Down Expand Up @@ -447,9 +451,44 @@ void EInkDynamicDisplay::resetGhostPixelTracking()
}
#endif // EINK_LIMIT_GHOSTING_PX

// Handle any asyc tasks
void EInkDynamicDisplay::onNotify(uint32_t notification)
{
// Which task
switch (notification) {
case DUE_POLL_ASYNCREFRESH:
pollAsyncRefresh();
break;
}
}

#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()
// Run the post-update code if the hardware is ready
void EInkDynamicDisplay::pollAsyncRefresh()
{
// We shouldn't be here..
if (!asyncRefreshRunning)
return;

// Still running, check back later
if (adafruitDisplay->epd2.isBusy()) {
// Schedule next call of pollAsyncRefresh()
NotifiedWorkerThread::notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true);
return;
}

// If 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()
}

// Check the status of "async full-refresh"; skip if running
void EInkDynamicDisplay::checkBusyAsyncRefresh()
{
// No refresh taking place, continue with determineMode()
if (!asyncRefreshRunning)
Expand All @@ -472,15 +511,6 @@ void EInkDynamicDisplay::checkAsyncFullRefresh()

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
Expand Down
35 changes: 23 additions & 12 deletions src/graphics/EInkDynamicDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "EInkDisplay2.h"
#include "GxEPD2_BW.h"
#include "concurrency/NotifiedWorkerThread.h"

/*
Derives from the EInkDisplay adapter class.
Expand All @@ -14,7 +15,7 @@
(Full, Fast, Skip)
*/

class EInkDynamicDisplay : public EInkDisplay
class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWorkerThread
{
public:
// Constructor
Expand Down Expand Up @@ -61,13 +62,20 @@ class EInkDynamicDisplay : public EInkDisplay
REDRAW_WITH_FULL,
};

void configForFastRefresh(); // GxEPD2 code to set fast-refresh
void configForFullRefresh(); // GxEPD2 code to set full-refresh
bool determineMode(); // Assess situation, pick a refresh type
void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type
void adjustRefreshCounters(); // Update fastRefreshCount
bool update(); // Trigger the display update - determine mode, then call base class
void endOrDetach(); // Run the post-update code, or delegate it off to checkAsyncFullRefresh()
enum notificationTypes : uint8_t { // What was onNotify() called for
NONE = 0, // This behavior (NONE=0) is fixed by NotifiedWorkerThread class
DUE_POLL_ASYNCREFRESH = 1,
};
const uint32_t intervalPollAsyncRefresh = 100;

void onNotify(uint32_t notification) override; // Handle any async tasks - overrides NotifiedWorkerThread
void configForFastRefresh(); // GxEPD2 code to set fast-refresh
void configForFullRefresh(); // GxEPD2 code to set full-refresh
bool determineMode(); // Assess situation, pick a refresh type
void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type
void adjustRefreshCounters(); // Update fastRefreshCount
bool update(); // Trigger the display update - determine mode, then call base class
void endOrDetach(); // Run the post-update code, or delegate it off to checkBusyAsyncRefresh()

// Checks as part of determineMode()
void checkInitialized(); // Is this the very first frame?
Expand Down Expand Up @@ -111,10 +119,13 @@ class EInkDynamicDisplay : public EInkDisplay

// 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 awaitRefresh(); // Hold control while an async refresh runs
void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay()
bool asyncRefreshRunning = false; // Flag, checked by checkAsyncFullRefresh()
void pollAsyncRefresh(); // Run the post-update code if the hardware is ready
void checkBusyAsyncRefresh(); // Check if display is busy running an async full-refresh (rejecting new frames)
void awaitRefresh(); // Hold control while an async refresh runs
void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay()
bool asyncRefreshRunning = false; // Flag, checked by checkBusyAsyncRefresh()
#else
void pollAsyncRefresh() {} // Dummy method. In theory, not reachable
#endif
};

Expand Down
125 changes: 117 additions & 8 deletions src/graphics/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,65 @@ static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i

#ifdef USE_EINK
/// Used on eink displays while in deep sleep
static void drawSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
static void drawDeepSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Next frame should use full-refresh, and block while running, else device will sleep before async callback
EINK_ADD_FRAMEFLAG(display, COSMETIC);
EINK_ADD_FRAMEFLAG(display, BLOCKING);

LOG_DEBUG("Drawing deep sleep screen\n");
drawIconScreen("Sleeping...", display, state, x, y);
}

/// Used on eink displays when screen updates are paused
static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
LOG_DEBUG("Drawing screensaver overlay\n");

EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh

// Config
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused";
const char *idText = owner.short_name;
constexpr uint16_t padding = 5;
constexpr uint8_t dividerGap = 1;
constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in.

// Dimensions
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText));
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
const uint16_t boxWidth = padding + idTextWidth + padding + padding + pauseTextWidth + padding;
const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding;

// Position
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1);
// const int16_t boxRight = boxLeft + boxWidth - 1;
const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1));
const int16_t boxBottom = boxTop + boxHeight - 1;
const int16_t idTextLeft = boxLeft + padding;
const int16_t idTextTop = boxTop + padding;
const int16_t pauseTextLeft = boxLeft + padding + idTextWidth + padding + padding;
const int16_t pauseTextTop = boxTop + padding;
const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
const int16_t dividerTop = boxTop + 1 + dividerGap;
const int16_t dividerBottom = boxBottom - 1 - dividerGap;

// Draw: box
display->setColor(EINK_WHITE);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box
display->setColor(EINK_BLACK);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);

// Draw: Text
display->drawString(idTextLeft, idTextTop, idText);
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold

// Draw: divider
display->drawLine(dividerX, dividerTop, dividerX, dividerBottom);
}
#endif

static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
Expand Down Expand Up @@ -948,18 +999,17 @@ Screen::~Screen()
void Screen::doDeepSleep()
{
#ifdef USE_EINK
static FrameCallback sleepFrames[] = {drawSleepScreen};
static const int sleepFrameCount = sizeof(sleepFrames) / sizeof(sleepFrames[0]);
ui->setFrames(sleepFrames, sleepFrameCount);
ui->update();
setOn(false, drawDeepSleepScreen);
#ifdef PIN_EINK_EN
digitalWrite(PIN_EINK_EN, LOW); // power off backlight
#endif
#endif
#else
// Without E-Ink display:
setOn(false);
#endif
}

void Screen::handleSetOn(bool on)
void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
{
if (!useDisplay)
return;
Expand All @@ -978,6 +1028,10 @@ void Screen::handleSetOn(bool on)
setInterval(0); // Draw ASAP
runASAP = true;
} else {
#ifdef USE_EINK
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
setScreensaverFrames(einkScreensaver);
#endif
LOG_INFO("Turning off screen\n");
dispdev->displayOff();
#ifdef T_WATCH_S3
Expand Down Expand Up @@ -1028,6 +1082,7 @@ void Screen::setup()
logo_timeout *= 2;

// Add frames.
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST);
static FrameCallback bootFrames[] = {drawBootScreen};
static const int bootFrameCount = sizeof(bootFrames) / sizeof(bootFrames[0]);
ui->setFrames(bootFrames, bootFrameCount);
Expand Down Expand Up @@ -1283,6 +1338,58 @@ void Screen::setWelcomeFrames()
}
}

#ifdef USE_EINK
/// Determine which screensaver frame to use, then set the FrameCallback
void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
{
// Remember current frame, restore position at power-on
uint8_t frameNumber = ui->getUiState()->currentFrame;

// Retain specified frame / overlay callback beyond scope of this method
static FrameCallback screensaverFrame;
static OverlayCallback screensaverOverlay;

// If: one-off screensaver frame passed as argument. Handles doDeepSleep()
if (einkScreensaver != NULL) {
screensaverFrame = einkScreensaver;
ui->setFrames(&screensaverFrame, 1);
}

// Else, display the usual "overlay" screensaver
else {
screensaverOverlay = drawScreensaverOverlay;
ui->setOverlays(&screensaverOverlay, 1);
}

// Request new frame, ASAP
setFastFramerate();
uint64_t startUpdate;
do {
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
delay(1);
ui->update();
} while (ui->getUiState()->lastUpdate < startUpdate);

#ifndef USE_EINK_DYNAMICDISPLAY
// Retrofit to EInkDisplay class
delay(10);
screen->forceDisplay();
#endif

// Prepare now for next frame, shown when display wakes
ui->setOverlays(NULL, 0); // Clear overlay
setFrames(); // Return to normal display updates
ui->switchToFrame(frameNumber); // Attempt to return to same frame after power-on

// Pick a refresh method, for when display wakes
#ifdef EINK_HASQUIRK_GHOSTING
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused"
#else
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
#endif
}
#endif

// restore our regular frame list
void Screen::setFrames()
{
Expand Down Expand Up @@ -1383,14 +1490,16 @@ void Screen::handleShutdownScreen()
{
LOG_DEBUG("showing shutdown screen\n");
showingNormalScreen = false;
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Use fast-refresh for next frame, no skip please
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update

auto frame = [](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
drawFrameText(display, state, x, y, "Shutting down...");
};
static FrameCallback frames[] = {frame};

setFrameImmediateDraw(frames);
forceDisplay();
}

void Screen::handleRebootScreen()
Expand Down
Loading

0 comments on commit 8187fa7

Please sign in to comment.