kopia lustrzana https://github.com/meshtastic/firmware
				
				
				
			feat: E-Ink "Dynamic Partial" (#3193)
Use a mixture of full refresh, partial refresh, and skipped updates, balancing urgency and display health. Co-authored-by: Ben Meadors <benmmeadors@gmail.com>pull/3209/head
							rodzic
							
								
									ce8673b6dc
								
							
						
					
					
						commit
						36cf9b9ef4
					
				|  | @ -125,61 +125,68 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) | |||
|     // No need to grab this lock because we are on our own SPI bus
 | ||||
|     // concurrency::LockGuard g(spiLock);
 | ||||
| 
 | ||||
| #if defined(USE_EINK_DYNAMIC_PARTIAL) | ||||
|     // Decide if update is partial or full
 | ||||
|     bool continueUpdate = determineRefreshMode(); | ||||
|     if (!continueUpdate) | ||||
|         return false; | ||||
| #else | ||||
| 
 | ||||
|     uint32_t now = millis(); | ||||
|     uint32_t sinceLast = now - lastDrawMsec; | ||||
| 
 | ||||
|     if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) { | ||||
|     if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) | ||||
|         lastDrawMsec = now; | ||||
| 
 | ||||
|         // FIXME - only draw bits have changed (use backbuf similar to the other displays)
 | ||||
|         // tft.drawBitmap(0, 0, buffer, 128, 64, TFT_YELLOW, TFT_BLACK);
 | ||||
|         for (uint32_t y = 0; y < displayHeight; y++) { | ||||
|             for (uint32_t x = 0; x < displayWidth; x++) { | ||||
|                 // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient
 | ||||
|                 auto b = buffer[x + (y / 8) * displayWidth]; | ||||
|                 auto isset = b & (1 << (y & 7)); | ||||
|                 adafruitDisplay->drawPixel(x, y, isset ? COLORED : UNCOLORED); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         LOG_DEBUG("Updating E-Paper... "); | ||||
| 
 | ||||
| #if defined(TTGO_T_ECHO) | ||||
|         adafruitDisplay->nextPage(); | ||||
| #elif defined(RAK4630) || defined(MAKERPYTHON) | ||||
| 
 | ||||
|         // RAK14000 2.13 inch b/w 250x122 actually now does support partial updates
 | ||||
| 
 | ||||
|         // Full update mode (slow)
 | ||||
|         // adafruitDisplay->display(false); // FIXME, use partial update mode
 | ||||
| 
 | ||||
|         // Only enable for e-Paper with support for partial updates and comment out above adafruitDisplay->display(false);
 | ||||
|         //  1.54 inch 200x200 - GxEPD2_154_M09
 | ||||
|         //  2.13 inch 250x122 - GxEPD2_213_BN
 | ||||
|         //  2.9 inch 296x128 - GxEPD2_290_T5D
 | ||||
|         //  4.2 inch 300x400 - GxEPD2_420_M01
 | ||||
|         adafruitDisplay->nextPage(); | ||||
| 
 | ||||
| #elif defined(PCA10059) || defined(M5_COREINK) | ||||
|         adafruitDisplay->nextPage(); | ||||
| #elif defined(HELTEC_WIRELESS_PAPER_V1_0) | ||||
|         adafruitDisplay->nextPage(); | ||||
| #elif defined(HELTEC_WIRELESS_PAPER) | ||||
|         adafruitDisplay->nextPage(); | ||||
| #elif defined(PRIVATE_HW) || defined(my) | ||||
|         adafruitDisplay->nextPage(); | ||||
|     else | ||||
|         return false; | ||||
| 
 | ||||
| #endif | ||||
| 
 | ||||
|         // Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display)
 | ||||
|         adafruitDisplay->hibernate(); | ||||
|         LOG_DEBUG("done\n"); | ||||
| 
 | ||||
|         return true; | ||||
|     } else { | ||||
|         // LOG_DEBUG("Skipping eink display\n");
 | ||||
|         return false; | ||||
|     // FIXME - only draw bits have changed (use backbuf similar to the other displays)
 | ||||
|     // tft.drawBitmap(0, 0, buffer, 128, 64, TFT_YELLOW, TFT_BLACK);
 | ||||
|     for (uint32_t y = 0; y < displayHeight; y++) { | ||||
|         for (uint32_t x = 0; x < displayWidth; x++) { | ||||
|             // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient
 | ||||
|             auto b = buffer[x + (y / 8) * displayWidth]; | ||||
|             auto isset = b & (1 << (y & 7)); | ||||
|             adafruitDisplay->drawPixel(x, y, isset ? COLORED : UNCOLORED); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     LOG_DEBUG("Updating E-Paper... "); | ||||
| 
 | ||||
| #if defined(TTGO_T_ECHO) | ||||
|     adafruitDisplay->nextPage(); | ||||
| #elif defined(RAK4630) || defined(MAKERPYTHON) | ||||
| 
 | ||||
|     // RAK14000 2.13 inch b/w 250x122 actually now does support partial updates
 | ||||
| 
 | ||||
|     // Full update mode (slow)
 | ||||
|     // adafruitDisplay->display(false); // FIXME, use partial update mode
 | ||||
| 
 | ||||
|     // Only enable for e-Paper with support for partial updates and comment out above adafruitDisplay->display(false);
 | ||||
|     //  1.54 inch 200x200 - GxEPD2_154_M09
 | ||||
|     //  2.13 inch 250x122 - GxEPD2_213_BN
 | ||||
|     //  2.9 inch 296x128 - GxEPD2_290_T5D
 | ||||
|     //  4.2 inch 300x400 - GxEPD2_420_M01
 | ||||
|     adafruitDisplay->nextPage(); | ||||
| 
 | ||||
| #elif defined(PCA10059) || defined(M5_COREINK) | ||||
|     adafruitDisplay->nextPage(); | ||||
| #elif defined(HELTEC_WIRELESS_PAPER_V1_0) | ||||
|     adafruitDisplay->nextPage(); | ||||
| #elif defined(HELTEC_WIRELESS_PAPER) | ||||
|     adafruitDisplay->nextPage(); | ||||
| #elif defined(PRIVATE_HW) || defined(my) | ||||
|     adafruitDisplay->nextPage(); | ||||
| 
 | ||||
| #endif | ||||
| 
 | ||||
|     // Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display)
 | ||||
|     adafruitDisplay->hibernate(); | ||||
|     LOG_DEBUG("done\n"); | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| // Write the buffer to the display memory
 | ||||
|  | @ -188,8 +195,16 @@ void EInkDisplay::display(void) | |||
|     // We don't allow regular 'dumb' display() calls to draw on eink until we've shown
 | ||||
|     // at least one forceDisplay() keyframe.  This prevents flashing when we should the critical
 | ||||
|     // bootscreen (that we want to look nice)
 | ||||
|     if (lastDrawMsec) | ||||
| 
 | ||||
| #ifdef USE_EINK_DYNAMIC_PARTIAL | ||||
|     lowPriority(); | ||||
|     forceDisplay(); | ||||
|     highPriority(); | ||||
| #else | ||||
|     if (lastDrawMsec) { | ||||
|         forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower
 | ||||
|     } | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| // Send a command to the display (low level function)
 | ||||
|  | @ -329,4 +344,130 @@ bool EInkDisplay::connect() | |||
|     return true; | ||||
| } | ||||
| 
 | ||||
| // Use a mix of full and partial refreshes, to preserve display health
 | ||||
| #if defined(USE_EINK_DYNAMIC_PARTIAL) | ||||
| 
 | ||||
| // Suggest that subsequent updates should use partial-refresh
 | ||||
| void EInkDisplay::highPriority() | ||||
| { | ||||
|     isHighPriority = true; | ||||
| } | ||||
| 
 | ||||
| // Suggest that subsequent updates should use full-refresh
 | ||||
| void EInkDisplay::lowPriority() | ||||
| { | ||||
|     isHighPriority = false; | ||||
| } | ||||
| 
 | ||||
| // configure display for partial-refresh
 | ||||
| void EInkDisplay::configForPartialRefresh() | ||||
| { | ||||
|     // Display-specific code can go here
 | ||||
| #if defined(PRIVATE_HW) | ||||
| #else | ||||
|     // Otherwise:
 | ||||
|     adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| // Configure display for full-refresh
 | ||||
| void EInkDisplay::configForFullRefresh() | ||||
| { | ||||
|     // Display-specific code can go here
 | ||||
| #if defined(PRIVATE_HW) | ||||
| #else | ||||
|     // Otherwise:
 | ||||
|     adafruitDisplay->setFullWindow(); | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| bool EInkDisplay::newImageMatchesOld() | ||||
| { | ||||
|     uint32_t newImageHash = 0; | ||||
| 
 | ||||
|     // Generate hash: sum all bytes in the image buffer
 | ||||
|     for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { | ||||
|         newImageHash += buffer[b]; | ||||
|     } | ||||
| 
 | ||||
|     // Compare hashes
 | ||||
|     bool hashMatches = (newImageHash == prevImageHash); | ||||
| 
 | ||||
|     // Update the cached hash
 | ||||
|     prevImageHash = newImageHash; | ||||
| 
 | ||||
|     // Return the comparison result
 | ||||
|     return hashMatches; | ||||
| } | ||||
| 
 | ||||
| // Change between partial and full refresh config, or skip update, balancing urgency and display health.
 | ||||
| bool EInkDisplay::determineRefreshMode() | ||||
| { | ||||
|     uint32_t now = millis(); | ||||
|     uint32_t sinceLast = now - lastUpdateMsec; | ||||
| 
 | ||||
|     // If rate-limiting dropped a high-priority update:
 | ||||
|     // promote this update, so it runs ASAP
 | ||||
|     if (missedHighPriorityUpdate) { | ||||
|         isHighPriority = true; | ||||
|         missedHighPriorityUpdate = false; | ||||
|     } | ||||
| 
 | ||||
|     // Abort: if too soon for a new frame
 | ||||
|     if (isHighPriority && partialRefreshCount > 0 && sinceLast < highPriorityLimitMsec) { | ||||
|         LOG_DEBUG("Update skipped: exceeded EINK_HIGHPRIORITY_LIMIT_SECONDS\n"); | ||||
|         missedHighPriorityUpdate = true; | ||||
|         return false; | ||||
|     } | ||||
|     if (!isHighPriority && sinceLast < lowPriorityLimitMsec) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // Check if old image (partial) should be redrawn (as full), for image quality
 | ||||
|     if (partialRefreshCount > 0 && !isHighPriority) | ||||
|         needsFull = true; | ||||
| 
 | ||||
|     // If too many partials, require a full-refresh (display health)
 | ||||
|     if (partialRefreshCount >= partialRefreshLimit) | ||||
|         needsFull = true; | ||||
| 
 | ||||
|     // If image matches
 | ||||
|     if (newImageMatchesOld()) { | ||||
|         // If low priority: limit rate
 | ||||
|         // otherwise, every loop() will run the hash method
 | ||||
|         if (!isHighPriority) | ||||
|             lastUpdateMsec = now; | ||||
| 
 | ||||
|         // If update is *not* for display health or image quality, skip it
 | ||||
|         if (!needsFull) | ||||
|             return false; | ||||
|     } | ||||
| 
 | ||||
|     // Conditions assessed - not skipping - load the appropriate config
 | ||||
| 
 | ||||
|     // If options require a full refresh
 | ||||
|     if (!isHighPriority || needsFull) { | ||||
|         if (partialRefreshCount > 0) | ||||
|             configForFullRefresh(); | ||||
| 
 | ||||
|         LOG_DEBUG("Conditions met for full-refresh\n"); | ||||
|         partialRefreshCount = 0; | ||||
|         needsFull = false; | ||||
|     } | ||||
| 
 | ||||
|     // If options allow a partial refresh
 | ||||
|     else { | ||||
|         if (partialRefreshCount == 0) | ||||
|             configForPartialRefresh(); | ||||
| 
 | ||||
|         LOG_DEBUG("Conditions met for partial-refresh\n"); | ||||
|         partialRefreshCount++; | ||||
|     } | ||||
| 
 | ||||
|     lastUpdateMsec = now; // Mark time for rate limiting
 | ||||
|     return true;          // Instruct calling method to continue with update
 | ||||
| } | ||||
| 
 | ||||
| #endif // End USE_EINK_DYNAMIC_PARTIAL
 | ||||
| 
 | ||||
| #endif | ||||
|  | @ -54,4 +54,68 @@ class EInkDisplay : public OLEDDisplay | |||
| 
 | ||||
|     // Connect to the display
 | ||||
|     virtual bool connect() override; | ||||
| 
 | ||||
| #if defined(USE_EINK_DYNAMIC_PARTIAL) | ||||
|     // Full, partial, or skip: balance urgency with display health
 | ||||
| 
 | ||||
|     // Use partial refresh if EITHER:
 | ||||
|     // * highPriority() was set
 | ||||
|     // * a highPriority() update was previously skipped, for rate-limiting - (EINK_HIGHPRIORITY_LIMIT_SECONDS)
 | ||||
| 
 | ||||
|     // Use full refresh if EITHER:
 | ||||
|     // * lowPriority() was set
 | ||||
|     // * too many partial updates in a row: protect display - (EINK_PARTIAL_REPEAT_LIMIT)
 | ||||
|     // * no recent updates, and last update was partial: redraw for image quality (EINK_LOWPRIORITY_LIMIT_SECONDS)
 | ||||
| 
 | ||||
|     // Rate limit if:
 | ||||
|     // * lowPriority() - (EINK_LOWPRIORITY_LIMIT_SECONDS)
 | ||||
|     // * highPriority(), if multiple partials have run back-to-back - (EINK_HIGHPRIORITY_LIMIT_SECONDS)
 | ||||
| 
 | ||||
|     // Skip update entirely if ALL criteria met:
 | ||||
|     // * new image matches old image
 | ||||
|     // * lowPriority()
 | ||||
|     // * not redrawing for image quality
 | ||||
|     // * not refreshing for display health
 | ||||
| 
 | ||||
|     // ------------------------------------
 | ||||
| 
 | ||||
|     // To implement for your E-Ink display:
 | ||||
|     // * edit configForPartialRefresh()
 | ||||
|     // * edit configForFullRefresh()
 | ||||
|     // * add macros to variant.h, and adjust to taste:
 | ||||
| 
 | ||||
|     /*
 | ||||
|         #define USE_EINK_DYNAMIC_PARTIAL | ||||
|         #define EINK_LOWPRIORITY_LIMIT_SECONDS 30 | ||||
|         #define EINK_HIGHPRIORITY_LIMIT_SECONDS 1 | ||||
|         #define EINK_PARTIAL_REPEAT_LIMIT 5 | ||||
|     */ | ||||
| 
 | ||||
|   public: | ||||
|     void highPriority(); // Suggest partial refresh
 | ||||
|     void lowPriority();  // Suggest full refresh
 | ||||
| 
 | ||||
|   protected: | ||||
|     void configForPartialRefresh(); // Display specific code to select partial refresh mode
 | ||||
|     void configForFullRefresh();    // Display specific code to return to full refresh mode
 | ||||
|     bool newImageMatchesOld();      // Is the new update actually different to the last image?
 | ||||
|     bool determineRefreshMode();    // Called immediately before data written to display - choose refresh mode, or abort update
 | ||||
| 
 | ||||
|     bool isHighPriority = true;            // Does the method calling update believe that this is urgent?
 | ||||
|     bool needsFull = false;                // Is a full refresh forced? (display health)
 | ||||
|     bool missedHighPriorityUpdate = false; // Was a high priority update skipped for rate-limiting?
 | ||||
|     uint16_t partialRefreshCount = 0;      // How many partials have occurred since last full refresh?
 | ||||
|     uint32_t lastUpdateMsec = 0;           // When did the last update occur?
 | ||||
|     uint32_t prevImageHash = 0;            // Used to check if update will change screen image (skippable or not)
 | ||||
| 
 | ||||
|     // Set in variant.h
 | ||||
|     const uint32_t lowPriorityLimitMsec = (uint32_t)1000 * EINK_LOWPRIORITY_LIMIT_SECONDS;   // Max rate for partial refreshes
 | ||||
|     const uint32_t highPriorityLimitMsec = (uint32_t)1000 * EINK_HIGHPRIORITY_LIMIT_SECONDS; // Max rate for full refreshes
 | ||||
|     const uint32_t partialRefreshLimit = EINK_PARTIAL_REPEAT_LIMIT; // Max consecutive partials, before full is triggered
 | ||||
| 
 | ||||
| #else // !USE_EINK_DYNAMIC_PARTIAL
 | ||||
|     // Tolerate calls to these methods anywhere, just to be safe
 | ||||
|     void highPriority() {} | ||||
|     void lowPriority() {} | ||||
| #endif | ||||
| }; | ||||
|  |  | |||
|  | @ -5,6 +5,14 @@ | |||
| #define I2C_SCL SCL | ||||
| 
 | ||||
| #define USE_EINK | ||||
| 
 | ||||
| // Settings for Dynamic Partial mode
 | ||||
| // Change between partial and full refresh config, or skip update, balancing urgency and display health.
 | ||||
| #define USE_EINK_DYNAMIC_PARTIAL | ||||
| #define EINK_LOWPRIORITY_LIMIT_SECONDS 30 | ||||
| #define EINK_HIGHPRIORITY_LIMIT_SECONDS 1 | ||||
| #define EINK_PARTIAL_REPEAT_LIMIT 5 | ||||
| 
 | ||||
| /*
 | ||||
|  * eink display pins | ||||
|  */ | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 todd-herbert
						todd-herbert