mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-08 14:12:05 +00:00
Reimplement "Dynamic E-Ink" as a derived class (#3316)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
parent
905718e2ac
commit
c659292836
@ -87,9 +87,9 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
|||||||
#ifndef EINK_NO_HIBERNATE // Only hibernate if controller IC will preserve image memory
|
#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)
|
// Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display)
|
||||||
adafruitDisplay->hibernate();
|
adafruitDisplay->hibernate();
|
||||||
LOG_DEBUG("done\n");
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
LOG_DEBUG("done\n");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
/**
|
/**
|
||||||
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
||||||
*
|
*
|
||||||
|
* Note: EInkDynamicDisplay derives from this class.
|
||||||
|
*
|
||||||
* Remaining TODO:
|
* Remaining TODO:
|
||||||
* optimize display() to only draw changed pixels (see other OLED subclasses for examples)
|
* optimize display() to only draw changed pixels (see other OLED subclasses for examples)
|
||||||
* implement displayOn/displayOff to turn off the TFT device (and backlight)
|
* implement displayOn/displayOff to turn off the TFT device (and backlight)
|
||||||
@ -41,7 +43,7 @@ class EInkDisplay : public OLEDDisplay
|
|||||||
*
|
*
|
||||||
* @return true if we did draw the screen
|
* @return true if we did draw the screen
|
||||||
*/
|
*/
|
||||||
bool forceDisplay(uint32_t msecLimit = 1000);
|
virtual bool forceDisplay(uint32_t msecLimit = 1000);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shim to make the abstraction happy
|
* shim to make the abstraction happy
|
||||||
|
384
src/graphics/EInkDynamicDisplay.cpp
Normal file
384
src/graphics/EInkDynamicDisplay.cpp
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||||
|
#include "EInkDynamicDisplay.h"
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
|
||||||
|
: EInkDisplay(address, sda, scl, geometry, i2cBus)
|
||||||
|
{
|
||||||
|
// If tracking ghost pixels, grab memory
|
||||||
|
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||||
|
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
EInkDynamicDisplay::~EInkDynamicDisplay()
|
||||||
|
{
|
||||||
|
// If we were tracking ghost pixels, free the memory
|
||||||
|
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||||
|
delete[] dirtyPixels;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen requests a BACKGROUND frame
|
||||||
|
void EInkDynamicDisplay::display()
|
||||||
|
{
|
||||||
|
setFrameFlag(BACKGROUND);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen requests a RESPONSIVE frame
|
||||||
|
bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit)
|
||||||
|
{
|
||||||
|
setFrameFlag(RESPONSIVE);
|
||||||
|
return update(); // (Unutilized) Base class promises to return true if update ran
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flag for the next frame
|
||||||
|
void EInkDynamicDisplay::setFrameFlag(frameFlagTypes flag)
|
||||||
|
{
|
||||||
|
// OR the new flag into the existing flags
|
||||||
|
this->frameFlags = (frameFlagTypes)(this->frameFlags | flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GxEPD2 code to set fast refresh
|
||||||
|
void EInkDynamicDisplay::configForFastRefresh()
|
||||||
|
{
|
||||||
|
// Variant-specific code can go here
|
||||||
|
#if defined(PRIVATE_HW)
|
||||||
|
#else
|
||||||
|
// Otherwise:
|
||||||
|
adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// GxEPD2 code to set full refresh
|
||||||
|
void EInkDynamicDisplay::configForFullRefresh()
|
||||||
|
{
|
||||||
|
// Variant-specific code can go here
|
||||||
|
#if defined(PRIVATE_HW)
|
||||||
|
#else
|
||||||
|
// Otherwise:
|
||||||
|
adafruitDisplay->setFullWindow();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run any relevant GxEPD2 code, so next update will use correct refresh type
|
||||||
|
void EInkDynamicDisplay::applyRefreshMode()
|
||||||
|
{
|
||||||
|
// Change from FULL to FAST
|
||||||
|
if (currentConfig == FULL && refresh == FAST) {
|
||||||
|
configForFastRefresh();
|
||||||
|
currentConfig = FAST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change from FAST back to FULL
|
||||||
|
else if (currentConfig == FAST && refresh == FULL) {
|
||||||
|
configForFullRefresh();
|
||||||
|
currentConfig = FULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fastRefreshCount
|
||||||
|
void EInkDynamicDisplay::adjustRefreshCounters()
|
||||||
|
{
|
||||||
|
if (refresh == FAST)
|
||||||
|
fastRefreshCount++;
|
||||||
|
|
||||||
|
else if (refresh == FULL)
|
||||||
|
fastRefreshCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the display update by calling base class
|
||||||
|
bool EInkDynamicDisplay::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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assess situation, pick a refresh type
|
||||||
|
bool EInkDynamicDisplay::determineMode()
|
||||||
|
{
|
||||||
|
checkWasFlooded();
|
||||||
|
checkRateLimiting();
|
||||||
|
|
||||||
|
// If too soon for a new time, abort here
|
||||||
|
if (refresh == SKIPPED) {
|
||||||
|
storeAndReset();
|
||||||
|
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
|
||||||
|
|
||||||
|
// Once mode determined, any remaining checks will bypass
|
||||||
|
checkCosmetic();
|
||||||
|
checkDemandingFast();
|
||||||
|
checkConsecutiveFastRefreshes();
|
||||||
|
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||||
|
checkExcessiveGhosting();
|
||||||
|
#endif
|
||||||
|
checkFrameMatchesPrevious();
|
||||||
|
checkFastRequested();
|
||||||
|
|
||||||
|
if (refresh == UNSPECIFIED)
|
||||||
|
LOG_WARN("There was a flaw in the determineMode() logic.\n");
|
||||||
|
|
||||||
|
// -- Decision has been reached --
|
||||||
|
applyRefreshMode();
|
||||||
|
adjustRefreshCounters();
|
||||||
|
|
||||||
|
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||||
|
// Full refresh clears any ghosting
|
||||||
|
if (refresh == FULL)
|
||||||
|
resetGhostPixelTracking();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Return - call a refresh or not?
|
||||||
|
if (refresh == SKIPPED) {
|
||||||
|
storeAndReset();
|
||||||
|
return false; // Don't trigger a refresh
|
||||||
|
} else {
|
||||||
|
storeAndReset();
|
||||||
|
return true; // Do trigger a refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did RESPONSIVE frames previously exceed the rate-limit for fast refresh?
|
||||||
|
void EInkDynamicDisplay::checkWasFlooded()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it too soon for another frame of this type?
|
||||||
|
void EInkDynamicDisplay::checkRateLimiting()
|
||||||
|
{
|
||||||
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
// Sanity check: millis() overflow - just let the update run..
|
||||||
|
if (previousRunMs > now)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Skip update: too soon for BACKGROUND
|
||||||
|
if (frameFlags == BACKGROUND) {
|
||||||
|
if (now - previousRunMs < EINK_LIMIT_RATE_BACKGROUND_SEC * 1000) {
|
||||||
|
refresh = SKIPPED;
|
||||||
|
reason = EXCEEDED_RATELIMIT_FULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No rate-limit for these special cases
|
||||||
|
if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Skip update: too soon for RESPONSIVE
|
||||||
|
if (frameFlags & RESPONSIVE) {
|
||||||
|
if (now - previousRunMs < EINK_LIMIT_RATE_RESPONSIVE_SEC * 1000) {
|
||||||
|
refresh = SKIPPED;
|
||||||
|
reason = EXCEEDED_RATELIMIT_FAST;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this frame COSMETIC (splash screens?)
|
||||||
|
void EInkDynamicDisplay::checkCosmetic()
|
||||||
|
{
|
||||||
|
// If a decision was already reached, don't run the check
|
||||||
|
if (refresh != UNSPECIFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// A full refresh is requested for cosmetic purposes: we have a decision
|
||||||
|
if (frameFlags & COSMETIC) {
|
||||||
|
refresh = FULL;
|
||||||
|
reason = FLAGGED_COSMETIC;
|
||||||
|
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a one-off special circumstance, where we REALLY want a fast refresh?
|
||||||
|
void EInkDynamicDisplay::checkDemandingFast()
|
||||||
|
{
|
||||||
|
// If a decision was already reached, don't run the check
|
||||||
|
if (refresh != UNSPECIFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// A fast refresh is demanded: we have a decision
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the new frame match the currently displayed image?
|
||||||
|
void EInkDynamicDisplay::checkFrameMatchesPrevious()
|
||||||
|
{
|
||||||
|
// If a decision was already reached, don't run the check
|
||||||
|
if (refresh != UNSPECIFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If frame is *not* a duplicate, abort the check
|
||||||
|
if (imageHash != previousImageHash)
|
||||||
|
return;
|
||||||
|
|
||||||
|
#if !defined(EINK_BACKGROUND_USES_FAST)
|
||||||
|
// If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality)
|
||||||
|
if (frameFlags == BACKGROUND && fastRefreshCount > 0) {
|
||||||
|
refresh = FULL;
|
||||||
|
reason = REDRAW_WITH_FULL;
|
||||||
|
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Not redrawn, not COSMETIC, not DEMAND_FAST
|
||||||
|
refresh = SKIPPED;
|
||||||
|
reason = FRAME_MATCHED_PREVIOUS;
|
||||||
|
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No objections, we can perform fast-refresh, if desired
|
||||||
|
void EInkDynamicDisplay::checkFastRequested()
|
||||||
|
{
|
||||||
|
if (refresh != UNSPECIFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (frameFlags == BACKGROUND) {
|
||||||
|
#ifdef EINK_BACKGROUND_USES_FAST
|
||||||
|
// 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);
|
||||||
|
#else
|
||||||
|
// If we do want to use FULL for BACKGROUND updates
|
||||||
|
refresh = FULL;
|
||||||
|
reason = FLAGGED_BACKGROUND;
|
||||||
|
LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity: confirm that we did ask for a RESPONSIVE frame.
|
||||||
|
if (frameFlags & RESPONSIVE) {
|
||||||
|
refresh = FAST;
|
||||||
|
reason = NO_OBJECTIONS;
|
||||||
|
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu\n", fastRefreshCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the timer used for rate-limiting
|
||||||
|
void EInkDynamicDisplay::resetRateLimiting()
|
||||||
|
{
|
||||||
|
previousRunMs = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a hash of this frame, to compare against previous update
|
||||||
|
void EInkDynamicDisplay::hashImage()
|
||||||
|
{
|
||||||
|
imageHash = 0;
|
||||||
|
|
||||||
|
// Sum all bytes of the image buffer together
|
||||||
|
for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) {
|
||||||
|
imageHash += buffer[b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the results of determineMode() for future use, and reset for next call
|
||||||
|
void EInkDynamicDisplay::storeAndReset()
|
||||||
|
{
|
||||||
|
previousRefresh = refresh;
|
||||||
|
previousReason = reason;
|
||||||
|
|
||||||
|
// Only store image hash if the display will update
|
||||||
|
if (refresh != SKIPPED) {
|
||||||
|
previousImageHash = imageHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
frameFlags = BACKGROUND;
|
||||||
|
refresh = UNSPECIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||||
|
// Count how many ghost pixels the new image will display
|
||||||
|
void EInkDynamicDisplay::countGhostPixels()
|
||||||
|
{
|
||||||
|
// If a decision was already reached, don't run the check
|
||||||
|
if (refresh != UNSPECIFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Start a new count
|
||||||
|
ghostPixelCount = 0;
|
||||||
|
|
||||||
|
// Check new image, bit by bit, for any white pixels at locations marked "dirty"
|
||||||
|
for (uint16_t i = 0; i < displayBufferSize; i++) {
|
||||||
|
for (uint8_t bit = 0; bit < 7; bit++) {
|
||||||
|
|
||||||
|
const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh?
|
||||||
|
const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image?
|
||||||
|
|
||||||
|
// If pixel is (or has been) black since last full-refresh, and now is white: ghosting
|
||||||
|
if (dirty && shouldBeBlank)
|
||||||
|
ghostPixelCount++;
|
||||||
|
|
||||||
|
// Update the dirty status for this pixel - will this location become a ghost if set white in future?
|
||||||
|
if (!dirty && !shouldBeBlank)
|
||||||
|
dirtyPixels[i] |= (1 << bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ghost pixel count exceeds the defined limit
|
||||||
|
void EInkDynamicDisplay::checkExcessiveGhosting()
|
||||||
|
{
|
||||||
|
// If a decision was already reached, don't run the check
|
||||||
|
if (refresh != UNSPECIFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
countGhostPixels();
|
||||||
|
|
||||||
|
// If too many ghost pixels, select full refresh
|
||||||
|
if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) {
|
||||||
|
refresh = FULL;
|
||||||
|
reason = EXCEEDED_GHOSTINGLIMIT;
|
||||||
|
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the dirty pixels array. Call when full-refresh cleans the display.
|
||||||
|
void EInkDynamicDisplay::resetGhostPixelTracking()
|
||||||
|
{
|
||||||
|
// Copy the current frame into dirtyPixels[] from the display buffer
|
||||||
|
memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize);
|
||||||
|
}
|
||||||
|
#endif // EINK_LIMIT_GHOSTING_PX
|
||||||
|
|
||||||
|
#endif // USE_EINK_DYNAMICDISPLAY
|
104
src/graphics/EInkDynamicDisplay.h
Normal file
104
src/graphics/EInkDynamicDisplay.h
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||||
|
|
||||||
|
#include "EInkDisplay2.h"
|
||||||
|
#include "GxEPD2_BW.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Derives from the EInkDisplay adapter class.
|
||||||
|
Accepts suggestions from Screen class about frame type.
|
||||||
|
Determines which refresh type is most suitable.
|
||||||
|
(Full, Fast, Skip)
|
||||||
|
*/
|
||||||
|
|
||||||
|
class EInkDynamicDisplay : public EInkDisplay
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
// ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class )
|
||||||
|
EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
|
||||||
|
~EInkDynamicDisplay();
|
||||||
|
|
||||||
|
// What kind of frame is this
|
||||||
|
enum frameFlagTypes : uint8_t {
|
||||||
|
BACKGROUND = (1 << 0), // For frames via display()
|
||||||
|
RESPONSIVE = (1 << 1), // For frames via forceDisplay()
|
||||||
|
COSMETIC = (1 << 2), // For splashes
|
||||||
|
DEMAND_FAST = (1 << 3), // Special case only
|
||||||
|
};
|
||||||
|
void setFrameFlag(frameFlagTypes flag);
|
||||||
|
|
||||||
|
// Set the correct frame flag, then call universal "update()" method
|
||||||
|
void display() override;
|
||||||
|
bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused.
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum refreshTypes : uint8_t { // Which refresh operation will be used
|
||||||
|
UNSPECIFIED,
|
||||||
|
FULL,
|
||||||
|
FAST,
|
||||||
|
SKIPPED,
|
||||||
|
};
|
||||||
|
enum reasonTypes : uint8_t { // How was the decision reached
|
||||||
|
NO_OBJECTIONS,
|
||||||
|
EXCEEDED_RATELIMIT_FAST,
|
||||||
|
EXCEEDED_RATELIMIT_FULL,
|
||||||
|
FLAGGED_COSMETIC,
|
||||||
|
FLAGGED_DEMAND_FAST,
|
||||||
|
EXCEEDED_LIMIT_FASTREFRESH,
|
||||||
|
EXCEEDED_GHOSTINGLIMIT,
|
||||||
|
FRAME_MATCHED_PREVIOUS,
|
||||||
|
BACKGROUND_USES_FAST,
|
||||||
|
FLAGGED_BACKGROUND,
|
||||||
|
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
|
||||||
|
|
||||||
|
// Checks as part of determineMode()
|
||||||
|
void checkWasFlooded(); // Was the previous frame skipped for exceeding EINK_LIMIT_RATE_RESPONSIVE_SEC?
|
||||||
|
void checkRateLimiting(); // Is this frame too soon?
|
||||||
|
void checkCosmetic(); // Was the COSMETIC flag set?
|
||||||
|
void checkDemandingFast(); // Was the DEMAND_FAST flag set?
|
||||||
|
void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively?
|
||||||
|
void checkFrameMatchesPrevious(); // Does the new frame match the existing display image?
|
||||||
|
void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND?
|
||||||
|
|
||||||
|
void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting
|
||||||
|
void hashImage(); // Generate a hashed version of this frame, to compare against previous update
|
||||||
|
void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call
|
||||||
|
|
||||||
|
// What we are determining for this frame
|
||||||
|
frameFlagTypes frameFlags = BACKGROUND; // Frame type(s) - determineMode() input
|
||||||
|
refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output
|
||||||
|
reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used
|
||||||
|
|
||||||
|
// What happened last time determineMode() ran
|
||||||
|
refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome
|
||||||
|
reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason
|
||||||
|
|
||||||
|
uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting)
|
||||||
|
uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed!
|
||||||
|
uint32_t previousImageHash = 0; // Hash of the previous update's frame
|
||||||
|
uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh?
|
||||||
|
refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for
|
||||||
|
|
||||||
|
// Optional - track ghosting, pixel by pixel
|
||||||
|
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||||
|
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
|
||||||
|
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
|
||||||
|
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -898,9 +898,12 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) || defined(RAK14014)
|
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) || defined(RAK14014)
|
||||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
#elif defined(USE_EINK)
|
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
|
||||||
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
|
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
|
#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||||
|
dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry,
|
||||||
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
#elif defined(USE_ST7567)
|
#elif defined(USE_ST7567)
|
||||||
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
|
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
|
@ -47,6 +47,7 @@ class Screen
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "EInkDisplay2.h"
|
#include "EInkDisplay2.h"
|
||||||
|
#include "EInkDynamicDisplay.h"
|
||||||
#include "TFTDisplay.h"
|
#include "TFTDisplay.h"
|
||||||
#include "TypedQueue.h"
|
#include "TypedQueue.h"
|
||||||
#include "commands.h"
|
#include "commands.h"
|
||||||
|
@ -8,6 +8,12 @@ build_flags =
|
|||||||
-D EINK_DISPLAY_MODEL=GxEPD2_213_BN
|
-D EINK_DISPLAY_MODEL=GxEPD2_213_BN
|
||||||
-D EINK_WIDTH=250
|
-D EINK_WIDTH=250
|
||||||
-D EINK_HEIGHT=122
|
-D EINK_HEIGHT=122
|
||||||
|
-D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk
|
||||||
|
-D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted
|
||||||
|
-D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates
|
||||||
|
-D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
|
||||||
|
-D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated
|
||||||
|
;-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
https://github.com/meshtastic/GxEPD2/
|
https://github.com/meshtastic/GxEPD2/
|
||||||
|
Loading…
Reference in New Issue
Block a user