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

E-Ink Screensaver #3477

Merged
merged 26 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f0b83e4
fix Wireless Paper double-clear screen at boot
todd-herbert Mar 22, 2024
b14bae8
log when flooded with "responsive" frames
todd-herbert Mar 22, 2024
c323c7d
show the "resuming" screen when waking from deep-sleep
todd-herbert Mar 22, 2024
e9c02ec
rename drawDeepSleepScreen
todd-herbert Mar 22, 2024
dbb8874
show a screensaver frame when screen off
todd-herbert Mar 23, 2024
ba8f856
Merge branch 'master' into eink-screensaver
thebentern Mar 24, 2024
dce4b5a
Merge branch 'meshtastic:master' into eink-screensaver
todd-herbert Mar 25, 2024
20521e1
Add macros for E-Ink color values.
todd-herbert Mar 25, 2024
719026d
adapt drawDeepSleepScreen to new screensaver convention
todd-herbert Mar 25, 2024
03905e7
Merge branch 'master' of https://github.com/meshtastic/firmware into …
todd-herbert Mar 26, 2024
dd243ea
Mark Wireless Paper V1.1 as having problems with ghosting
todd-herbert Mar 26, 2024
8be20ba
Change screensaver from fullscreen logo to overlay
todd-herbert Mar 26, 2024
a0bd2f7
Merge branch 'master' of meshtastic/firmware into eink-screensaver
todd-herbert Mar 26, 2024
5130351
Merge branch 'master' into eink-screensaver
thebentern Mar 27, 2024
1857614
identify "quirks" rather than "problems"
todd-herbert Mar 27, 2024
a67ccc8
move async refresh polling from display() to a NotifiedWorkerThread
todd-herbert Mar 27, 2024
62a4d0a
Merge branch 'eink-screensaver' of https://github.com/todd-herbert/me…
todd-herbert Mar 27, 2024
35c81d1
Prevent skipping of deep-sleep screen
todd-herbert Mar 28, 2024
52794b4
Redesign screensaver overlay
todd-herbert Mar 28, 2024
f1fc2cf
Optimize refresh for different displays
todd-herbert Mar 28, 2024
933a920
Support older EInkDisplay class
todd-herbert Mar 28, 2024
53745db
Don't assume text alignment
todd-herbert Mar 28, 2024
b2465e8
fix spelling of a quirk macro
todd-herbert Mar 28, 2024
ae5b49d
Merge branch 'master' into eink-screensaver
thebentern Mar 28, 2024
ffc1658
Merge branch 'master' into eink-screensaver
todd-herbert Mar 28, 2024
8c5afa0
Handle impossibly unlikely millis() overflow error
todd-herbert Mar 28, 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
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
Loading