diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 839d495833..cfc2b2bf92 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -132,6 +132,10 @@ void halt() { bool wifi_enabled = false; // set at boot from prefs; false also when the effective SSID is blank #endif +#if defined(DISPLAY_CLASS) && defined(EINK_REFRESH_LOOP) +static void loopDuringDisplayRefresh(); +#endif + void setup() { Serial.begin(115200); board.begin(); @@ -284,15 +288,36 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved the_mesh.setListener(&ui_task); +#ifdef EINK_REFRESH_LOOP + if (disp != NULL) disp->setBusyHook(loopDuringDisplayRefresh); +#endif #endif board.onBootComplete(); } -void loop() { +// Work that must keep running even while the UI is blocked (e.g. by an e-ink refresh). +// Called from loop() and from the display busy hook, so both always run the same set. +static void loopCore() { the_mesh.loop(); interface_manager.loop(); sensors.loop(); +} + +#if defined(DISPLAY_CLASS) && defined(EINK_REFRESH_LOOP) +// Runs from inside the e-ink busy wait, so radio/BLE/sensors keep going during a panel refresh. +// Must not touch the display: UI listener callbacks only update state and schedule the next frame. +static void loopDuringDisplayRefresh() { + static bool active = false; + if (active) return; + active = true; + loopCore(); + active = false; +} +#endif + +void loop() { + loopCore(); #ifdef DISPLAY_CLASS ui_task.loop(); #endif diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 0e41dab5e4..2fd2297723 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -24,6 +24,7 @@ class DisplayDriver { virtual bool isOn() = 0; virtual bool isEink() { return false; } // default to non-eink, override in eink drivers virtual void forceFullRefresh() {} // next refresh will be full for eink + virtual void setBusyHook(void (*hook)()) {} // eink: hook is called repeatedly while waiting for panel refresh virtual void turnOn() = 0; virtual void turnOn(int max_partial) { turnOn(); } virtual void turnOff() = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index d7f9562bb3..209d1b46bf 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -88,7 +88,9 @@ void GxEPDDisplay::drawSleepIcon() { display.fillRect((x - 5) * scale_x - 1, y * scale_y, (W+5) * scale_x + 2, (H-2) * scale_y + 1, GxEPD_WHITE); _curr_color = GxEPD_BLACK; drawXbm(x, y, sleep_icon_24x24, W, H); + if (_busy_hook) display.epd2.setBusyCallback(busyCallback, this); display.display(true); + display.epd2.setBusyCallback(NULL); } void GxEPDDisplay::turnOff(bool blank) { @@ -104,8 +106,10 @@ void GxEPDDisplay::turnOff(bool blank) { void GxEPDDisplay::blankScreen() { // do full refresh before powering off to clear screen // no full refresh needed at wakeup + if (_busy_hook) display.epd2.setBusyCallback(busyCallback, this); display.clearScreen(0xFF); // Clears microcontroller side RAM display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + display.epd2.setBusyCallback(NULL); _partial_refresh_count = 0; last_display_crc_value = 0; // we can hibernate as screen has been cleaned @@ -126,8 +130,10 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { || (_max_partial_refresh > 0 && _partial_refresh_count >= _max_partial_refresh)) { // forces a full wipe of the screen ... + if (_busy_hook) display.epd2.setBusyCallback(busyCallback, this); display.clearScreen(0xFF); // Clears microcontroller side RAM display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + display.epd2.setBusyCallback(NULL); // we'll need a partial refresh after that (whatever crc value is) last_display_crc_value = 0; _partial_refresh_count = 0; @@ -239,7 +245,10 @@ void GxEPDDisplay::endFrame() { if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { + // keep the rest of the firmware running while the panel refreshes (~0.5s partial) + if (_busy_hook) display.epd2.setBusyCallback(busyCallback, this); display.display(true); + display.epd2.setBusyCallback(NULL); _partial_refresh_count++; } last_display_crc_value = crc; diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 9d91844303..0aeaeca994 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -39,6 +39,12 @@ class GxEPDDisplay : public DisplayDriver { bool _force_full_refresh = false; int _partial_refresh_count = 0; int _max_partial_refresh = 0; + void (*_busy_hook)() = NULL; + + static void busyCallback(const void* self) { + ((const GxEPDDisplay*)self)->_busy_hook(); + delay(1); // same as GxEPD2 does without a callback + } public: #if defined(EINK_DISPLAY_MODEL) @@ -53,6 +59,7 @@ class GxEPDDisplay : public DisplayDriver { bool isEink() override { return true; } void forceFullRefresh() override { _force_full_refresh = true; } void turnOn(int max_partial) override; + void setBusyHook(void (*hook)()) override { _busy_hook = hook; } void turnOn() override; void turnOff(bool blank) override; void turnOff() override { turnOff(false); } diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 896e42a656..2c2a3e4304 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -93,6 +93,7 @@ build_flags = ${LilyGo_T-Echo.build_flags} -I src/helpers/ui -I examples/companion_radio/ui-new + -D EINK_REFRESH_LOOP -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D QSPIFLASH=1 @@ -120,6 +121,7 @@ build_flags = ${LilyGo_T-Echo.build_flags} -I src/helpers/ui -I examples/companion_radio/ui-new + -D EINK_REFRESH_LOOP -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256