From 05423e516afca8effbf8c7191d7d1ec4522f98c6 Mon Sep 17 00:00:00 2001 From: akalinin-softvelum <142931037+akalinin-softvelum@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:52:54 +0000 Subject: [PATCH 1/2] companion: keep mesh/BLE running during e-ink panel refresh Use GxEPD2's busy callback to run the mesh, interface and sensor loops (loopCore(), shared with loop()) while the panel refreshes, instead of blocking the main loop. Covers the partial refresh in endFrame(), the full wipe in startFrame(), and the sleep icon / blank refresh on turnOff(). --- examples/companion_radio/main.cpp | 25 ++++++++++++++++++++++++- src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/GxEPDDisplay.cpp | 9 +++++++++ src/helpers/ui/GxEPDDisplay.h | 7 +++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 839d4958336..9ed3bdcd7d8 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 +#ifdef DISPLAY_CLASS +static void loopDuringDisplayRefresh(); +#endif + void setup() { Serial.begin(115200); board.begin(); @@ -284,15 +288,34 @@ 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); + if (disp != NULL) disp->setBusyHook(loopDuringDisplayRefresh); #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(); +} + +#ifdef DISPLAY_CLASS +// 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 0e41dab5e4d..2fd22977239 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 d7f9562bb3a..209d1b46bf9 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 9d91844303a..0aeaeca9947 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); } From 1df6262d58969610a02adf9e32f57716a63cdeaa Mon Sep 17 00:00:00 2001 From: akalinin-softvelum <142931037+akalinin-softvelum@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:21:11 +0000 Subject: [PATCH 2/2] companion: enable e-ink refresh loop only with EINK_REFRESH_LOOP Only builds that define EINK_REFRESH_LOOP install the busy hook; for now the T-Echo companion BLE/USB envs. --- examples/companion_radio/main.cpp | 6 ++++-- variants/lilygo_techo/platformio.ini | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9ed3bdcd7d8..cfc2b2bf925 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -132,7 +132,7 @@ void halt() { bool wifi_enabled = false; // set at boot from prefs; false also when the effective SSID is blank #endif -#ifdef DISPLAY_CLASS +#if defined(DISPLAY_CLASS) && defined(EINK_REFRESH_LOOP) static void loopDuringDisplayRefresh(); #endif @@ -288,7 +288,9 @@ 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(); @@ -302,7 +304,7 @@ static void loopCore() { sensors.loop(); } -#ifdef DISPLAY_CLASS +#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() { diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 896e42a656f..2c2a3e4304a 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