diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 533faaf15..485acd204 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -350,8 +350,8 @@ class LGFX : public lgfx::LGFX_Device cfg.pclk_idle_high = 1; cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif #ifdef ST7262_HSYNC_POLARITY @@ -367,8 +367,8 @@ class LGFX : public lgfx::LGFX_Device cfg.pclk_idle_high = 1; cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif #ifdef SC7277_HSYNC_POLARITY @@ -384,8 +384,8 @@ class LGFX : public lgfx::LGFX_Device cfg.pclk_idle_high = 1; cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif _bus_instance.config(cfg); @@ -463,22 +463,22 @@ class LGFX : public lgfx::LGFX_Device // The following setting values ​​are general initial values ​​for each panel, so please comment out any // unknown items and try them. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) @@ -594,8 +594,8 @@ class TOUCH_CHSC6X : public ITouch return 0; }; - void wakeup(void) override{}; - void sleep(void) override{}; + void wakeup(void) override {}; + void sleep(void) override {}; private: chsc6x *chsc6xTouch = nullptr; @@ -633,7 +633,7 @@ class LGFX : public lgfx::LGFX_Device #ifdef SPI_3_WIRE cfg.spi_3wire = SPI_3_WIRE; #else - cfg.spi_3wire = true; // Set to true if reception is done on the MOSI pin + cfg.spi_3wire = true; // Set to true if reception is done on the MOSI pin #endif cfg.use_lock = true; // Set to true to use transaction locking cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / @@ -662,8 +662,8 @@ class LGFX : public lgfx::LGFX_Device cfg.memory_width = 240; cfg.memory_height = 320; cfg.offset_x = 0; - cfg.offset_y = 0; // No vertical shift needed - panel is top-aligned - cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout + cfg.offset_y = 0; // No vertical shift needed - panel is top-aligned + cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout #else cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC @@ -676,14 +676,14 @@ class LGFX : public lgfx::LGFX_Device #ifdef TFT_DUMMY_READ_PIXELS cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = - false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI #if defined(HAS_SDCARD) cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) #else @@ -794,20 +794,20 @@ class LGFX : public lgfx::LGFX_Device // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) @@ -1464,6 +1464,7 @@ static LGFX *tft = nullptr; #include "TFTColorRegions.h" #include "TFTDisplay.h" #include "TFTPalette.h" +#include "mesh/Throttle.h" #include #ifdef UNPHONE @@ -1849,14 +1850,23 @@ void TFTDisplay::sdlLoop() #endif } -#ifdef TFT_BLANK_ON_DISPLAY_OFF -// LovyanGFX exposes sleep in/out but not display on/off, so send the MIPI DCS opcodes directly. +#if defined(TFT_BLANK_ON_DISPLAY_OFF) || defined(TFT_SLEEP_WHEN_OFF) +// Neither LovyanGFX nor TFT_eSPI exposes display on/off, so send the MIPI DCS opcodes directly. static constexpr uint8_t kCmdDispOff = 0x28; static constexpr uint8_t kCmdDispOn = 0x29; // Quiet time the controller needs after sleep out before it will accept the next command. static constexpr uint32_t kSleepOutSettleMs = 120; #endif +#ifdef TFT_SLEEP_WHEN_OFF +// TFT_eSPI has no sleep()/wakeup() either. Frame memory survives sleep-in, so the last frame +// reappears on sleep-out and the dirty-window diff carries on. +static constexpr uint8_t kCmdSleepIn = 0x10; +static constexpr uint8_t kCmdSleepOut = 0x11; +static bool panelAsleep = false; +static uint32_t sleepInMs = 0; +#endif + // Send a command to the display (low level function) void TFTDisplay::sendCommand(uint8_t com) { @@ -1903,6 +1913,21 @@ void TFTDisplay::sendCommand(uint8_t com) tft->wakeup(); tft->powerSaveOff(); #endif +#elif defined(TFT_SLEEP_WHEN_OFF) + // Screen::handleSetOn() calls displayOn() twice per wake; only the first one has work to do. + if (panelAsleep) { +#ifdef VTFT_CTRL + digitalWrite(VTFT_CTRL, LOW); // rail up before the panel is addressed +#endif + // SLPOUT within 120 ms of SLPIN is ignored, e.g. a button press as the timeout fires. + // Wait out what is left of that window, not a fresh 120 ms on top of it. + if (uint32_t settleLeft = Throttle::remainingMs(sleepInMs, kSleepOutSettleMs)) + delay(settleLeft); + tft->writecommand(kCmdSleepOut); + delay(kSleepOutSettleMs); // datasheet minimum before the panel accepts DISPON + tft->writecommand(kCmdDispOn); + panelAsleep = false; + } #endif #if defined(TFT_NV3001B) || defined(TFT_BLANK_ON_DISPLAY_OFF) @@ -1955,6 +1980,14 @@ void TFTDisplay::sendCommand(uint8_t com) tft->sleep(); tft->powerSaveOn(); #endif +#elif defined(TFT_SLEEP_WHEN_OFF) + // Without this the LCD keeps driving the last frame unlit, which is what builds image retention. + if (!panelAsleep) { + tft->writecommand(kCmdDispOff); + tft->writecommand(kCmdSleepIn); + sleepInMs = millis(); + panelAsleep = true; + } #endif #ifdef VTFT_CTRL diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index 0c2e316da..39e6f4acf 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -38,6 +38,15 @@ bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) return (now - lastExecutionMs) < timeSpanMs; } +/// @brief How much of an interval is left since a stored event, 0 once the interval has passed +/// @param lastExecutionMs The last execution time in milliseconds +/// @param intervalMs The interval in milliseconds +uint32_t Throttle::remainingMs(uint32_t lastExecutionMs, uint32_t intervalMs) +{ + uint32_t elapsed = Time::getMillis() - lastExecutionMs; + return elapsed < intervalMs ? intervalMs - elapsed : 0; +} + /// @brief Check whether an absolute deadline has arrived, correctly across the millis() wrap /// @param deadlineMs The deadline, as a millis() value /// See the header for the range limit and the sentinel requirement. diff --git a/src/mesh/Throttle.h b/src/mesh/Throttle.h index 86740f577..ed322f83a 100644 --- a/src/mesh/Throttle.h +++ b/src/mesh/Throttle.h @@ -17,6 +17,11 @@ class Throttle return !isWithinTimespanMs(lastExecutionMs, intervalMs); } + /// What is left of intervalMs since lastExecutionMs, 0 once it has passed. One clock read, so a + /// caller waiting out the remainder cannot be preempted between the test and the subtraction. + /// Same sentinel rule as isWithinTimespanMs(): 0 is not treated as "never run". + static uint32_t remainingMs(uint32_t lastExecutionMs, uint32_t intervalMs); + /// True once an absolute deadline has arrived. Use this rather than comparing against millis() /// directly: that inverts while the deadline sits on the far side of the 32-bit wrap, so the /// action either fires immediately or blocks for about the interval it should have waited. diff --git a/test/test_throttle/test_main.cpp b/test/test_throttle/test_main.cpp index b0fe23a04..f00d091dd 100644 --- a/test/test_throttle/test_main.cpp +++ b/test/test_throttle/test_main.cpp @@ -55,6 +55,54 @@ void test_hasElapsed_boundary_is_inclusive() TEST_ASSERT_FALSE(Throttle::hasElapsed(9001, 1000)); // 999ms elapsed } +// --- remainingMs() --- + +// The point of the helper: what is left of the window, not the whole window. A caller that waits +// out the remainder must not pay again for time that has already gone by. +void test_remainingMs_returns_only_what_is_left() +{ + Time::setTestMillis(10000); + TEST_ASSERT_EQUAL_UINT32(500, Throttle::remainingMs(9500, 1000)); // 500ms elapsed of a 1000ms window + TEST_ASSERT_EQUAL_UINT32(1, Throttle::remainingMs(9001, 1000)); // 999ms elapsed + TEST_ASSERT_EQUAL_UINT32(1000, Throttle::remainingMs(10000, 1000)); // nothing elapsed yet +} + +// Saturates at 0 rather than underflowing to a ~49 day wait, which is what a bare +// intervalMs - elapsed would produce once the interval has passed. +void test_remainingMs_saturates_at_zero_once_elapsed() +{ + Time::setTestMillis(10000); + TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(9000, 1000)); // exactly the interval + TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(8000, 1000)); // well past it + TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(0, 1000)); +} + +// Nonzero exactly while isWithinTimespanMs() is true, so `if (remainingMs(...))` is a drop-in for +// the predicate at a call site that then waits out the rest. +void test_remainingMs_is_nonzero_exactly_within_the_window() +{ + Time::setTestMillis(10000); + const uint32_t cases[][2] = {{9500, 1000}, {8000, 1000}, {9000, 1000}, {9001, 1000}, {10000, 1}, {0, 5000}}; + for (auto &c : cases) { + TEST_ASSERT_EQUAL(Throttle::isWithinTimespanMs(c[0], c[1]), Throttle::remainingMs(c[0], c[1]) != 0); + } +} + +void test_remainingMs_survives_millis_wrap() +{ + const uint32_t lastRun = 0xFFFFFF00u; // 256ms before the wrap + Time::setTestMillis(lastRun); + + Time::advanceTestMillis(100); // still before the wrap + TEST_ASSERT_EQUAL_UINT32(900, Throttle::remainingMs(lastRun, 1000)); + + Time::advanceTestMillis(200); // wraps to 0x0000002C - 300ms elapsed in total + TEST_ASSERT_EQUAL_UINT32(700, Throttle::remainingMs(lastRun, 1000)); + + Time::advanceTestMillis(800); // 1100ms elapsed, past both the window and the wrap + TEST_ASSERT_EQUAL_UINT32(0, Throttle::remainingMs(lastRun, 1000)); +} + // --- rollover: the headline property --- // A window opened just before the 32-bit wrap must still close correctly after it. @@ -226,6 +274,10 @@ void setup() RUN_TEST(test_isWithinTimespan_boundary_is_exclusive); RUN_TEST(test_hasElapsed_is_complement_of_isWithinTimespan); RUN_TEST(test_hasElapsed_boundary_is_inclusive); + RUN_TEST(test_remainingMs_returns_only_what_is_left); + RUN_TEST(test_remainingMs_saturates_at_zero_once_elapsed); + RUN_TEST(test_remainingMs_is_nonzero_exactly_within_the_window); + RUN_TEST(test_remainingMs_survives_millis_wrap); RUN_TEST(test_isWithinTimespan_survives_millis_wrap); RUN_TEST(test_long_interval_survives_wrap); RUN_TEST(test_deadlinePassed_basic); diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.h b/variants/nrf52840/heltec_mesh_node_t096/variant.h index 3fa9dd86d..7159602f4 100644 --- a/variants/nrf52840/heltec_mesh_node_t096/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.h @@ -54,6 +54,7 @@ extern "C" { #define TFT_OFFSET_X 24 #define TFT_OFFSET_Y 0 #define TFT_INVERT false +#define TFT_SLEEP_WHEN_OFF // sleep the panel on screen-off instead of driving it unlit #define SCREEN_TRANSITION_FRAMERATE 3 // fps #define DISPLAY_FORCE_SMALL_FONTS diff --git a/variants/nrf52840/heltec_mesh_node_t1/variant.h b/variants/nrf52840/heltec_mesh_node_t1/variant.h index 8cfa6f65c..3b04905f0 100644 --- a/variants/nrf52840/heltec_mesh_node_t1/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t1/variant.h @@ -42,7 +42,7 @@ extern "C" { #define ST7735_MISO -1 #define ST7735_BUSY -1 #define ST7735_BL (0 + 15) -#define VTFT_CTRL (0 + 13) // Active HIGH, powers the ST7735 display +#define VTFT_CTRL (0 + 13) // Active LOW: LOW powers the ST7735 display rail #define SPI_FREQUENCY 80000000 #define SPI_READ_FREQUENCY 16000000 #define SCREEN_ROTATE @@ -51,6 +51,7 @@ extern "C" { #define TFT_OFFSET_X 24 #define TFT_OFFSET_Y 0 #define TFT_INVERT false +#define TFT_SLEEP_WHEN_OFF // sleep the panel on screen-off instead of driving it unlit #define DISPLAY_FORCE_SMALL_FONTS #define FORCE_LOW_RES 1 // 80px-wide panel causes artifacts with full-res UI elements