From 11d7551ccd2ec01b3b866eae7ebd19fde630b810 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 28 Aug 2026 14:26:52 -0700 Subject: [PATCH] fix(display): correct R8 touch decode, display wake and power-off Four defects found by hardware testing of the Expansion Kit V2. Touch never registered. The panel's controller does not use the point-count encoding the reference CHSC6X drivers document: byte 0 reads 0x00 idle and 0x1F while a finger is down, so testing for a count of 1 never fired. A partly-failed read leaves 0xFF, which must not count as a press either, so the test is now != 0x00 && != 0xFF. Touch polling could stall the UI loop for ~1 s at a time. The controller NACKs its address whenever it has nothing to report, and calling requestFrom() unconditionally logged a bus error on every 50 ms poll and, once the bus wedged, burned a full ESP_ERR_TIMEOUT inside loop(). Probe the address first, which reports the same NACK quietly, and bound the read with setTimeOut(). The display could not be woken once it blanked; only RST brought it back. turnOn() re-ran the whole display.init(), which re-enters SPI setup, spends ~500 ms in Adafruit's reset delays and pulses GPIO 21 - the line shared with TP_RST, so it reset the touch controller on every wake. Since turnOff() no longer parks that line low, the panel stays configured while dark and waking is just the backlight. Toggling also clears the refresh deadline so the current frame is drawn immediately instead of the stale one. Power-off rebooted instead of staying off. powerOff() went through enterDeepSleep(), which always arms an ext1 wake on P_LORA_DIO_1; a deep-sleep wake is a full reboot, so a node in live traffic restarted within seconds of showing "Turning OFF". It now disables every wake source, so the node stays down until RST or a power cycle. Note that the display off/on cycle had never been exercised on this board before: observer builds pinned AUTO_OFF_MILLIS=0, so the panel never blanked until display.timeout made it a runtime setting. --- examples/simple_repeater/UITask.cpp | 1 + examples/simple_room_server/UITask.cpp | 1 + src/helpers/ui/CHSC6XTouch.h | 46 ++++++++++++++++------- src/helpers/ui/ST7789LCDDisplay.cpp | 17 +++++++++ src/helpers/ui/ST7789LCDDisplay.h | 3 ++ variants/heltec_v4_r8/HeltecV4R8Board.cpp | 14 ++++++- 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index ce46b9c7c..70fa78912 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -320,6 +320,7 @@ void UITask::toggleDisplay() { #ifdef DISPLAY_ACTIVITY_DASHBOARD _rows_valid = false; #endif + _next_refresh = 0; // redraw at once rather than showing the stale frame _auto_off = millis() + displayTimeoutMillis(); } #endif diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index 3f124cb1a..2e9818e58 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -297,6 +297,7 @@ void UITask::toggleDisplay() { #ifdef DISPLAY_ACTIVITY_DASHBOARD _rows_valid = false; #endif + _next_refresh = 0; // redraw at once rather than showing the stale frame _auto_off = millis() + displayTimeoutMillis(); } #endif diff --git a/src/helpers/ui/CHSC6XTouch.h b/src/helpers/ui/CHSC6XTouch.h index c4c5cd2e0..fc8f119fc 100644 --- a/src/helpers/ui/CHSC6XTouch.h +++ b/src/helpers/ui/CHSC6XTouch.h @@ -18,7 +18,12 @@ #endif #define CHSC6X_READ_LEN 5 -#define CHSC6X_MAX_POINTS 1 + +// Bounds a single read. The shared bus can wedge, and the ESP32 default lets a +// failed transfer burn ~1 s inside the UI loop. +#ifndef CHSC6X_I2C_TIMEOUT_MS +#define CHSC6X_I2C_TIMEOUT_MS 10 +#endif class CHSC6XTouch { public: @@ -63,32 +68,47 @@ private: TouchTapDetector _detector; bool readPressed() { + // Probe the address first. This controller NACKs while it has nothing to + // report, and going straight to requestFrom() turns that into a logged bus + // error every poll plus, once the bus wedges, a ~1 s timeout stall in the + // UI loop. endTransmission() reports the same NACK quietly and cheaply. + _wire->beginTransmission((uint8_t)CHSC6X_I2C_ADDR); + if (_wire->endTransmission() != 0) return false; + + const uint16_t prev_timeout = _wire->getTimeOut(); + _wire->setTimeOut(CHSC6X_I2C_TIMEOUT_MS); uint8_t got = _wire->requestFrom((uint8_t)CHSC6X_I2C_ADDR, (uint8_t)CHSC6X_READ_LEN); + uint8_t buf[CHSC6X_READ_LEN]; + for (uint8_t i = 0; i < CHSC6X_READ_LEN; i++) { + buf[i] = i < got ? (uint8_t)_wire->read() : 0xFF; + } + while (_wire->available()) _wire->read(); // drain a short read + _wire->setTimeOut(prev_timeout); + if (got != CHSC6X_READ_LEN) { - while (_wire->available()) _wire->read(); // drain a short read logRaw(got, NULL); return false; } - - uint8_t buf[CHSC6X_READ_LEN]; - for (uint8_t i = 0; i < CHSC6X_READ_LEN; i++) buf[i] = (uint8_t)_wire->read(); logRaw(got, buf); - // buf[0] is the reported touch-point count (buf[2]/buf[4] are x/y). It must - // be tested against a *valid* count, not merely against zero: an idle or - // NACKed read can come back as 0xFF, which "non-zero" reads as a finger - // held down forever - the tap detector then fires once and, seeing no - // release, never fires again. - return buf[0] >= 1 && buf[0] <= CHSC6X_MAX_POINTS; + // Measured on the Expansion Kit V2 panel: byte 0 reads 0x00 while idle and + // 0x1F while a finger is down - not the 0x01 point count the reference + // CHSC6X drivers document, so testing for a count of 1 never fires. A + // partly-failed read leaves 0xFF, which must not register as a press. + return buf[0] != 0x00 && buf[0] != 0xFF; } #ifdef DISPLAY_TOUCH_DEBUG int16_t _logged = -1; + uint8_t _log_budget = 5; // always show the first few frames, then on change - // Logs on change only, so a normal boot stays quiet. + // Mostly logs on change, so a normal boot stays quiet - but the opening + // frames are unconditional so an idle read that never changes is still + // visible in the log. void logRaw(uint8_t got, const uint8_t* buf) { int16_t key = buf ? (int16_t)buf[0] : (int16_t)(-2 - (int16_t)got); - if (key == _logged) return; + if (key == _logged && _log_budget == 0) return; + if (_log_budget > 0) _log_budget--; _logged = key; if (!buf) { diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index bcdaf81c8..6ccdd24ba 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -50,6 +50,22 @@ ColorVal UIColor::corp_blue = 0x001A; bool ST7789LCDDisplay::begin() { if (!_isOn) { + #ifdef HELTEC_V4_R8_TFT + // turnOff() leaves this panel configured and powered - its reset line is + // shared with the touch controller, so it is never parked low - which makes + // waking just a backlight switch. Re-running the init below would re-enter + // SPI setup and pulse GPIO 21, resetting the touch controller on every wake + // and stalling the UI loop for ~500 ms of reset delays. + if (_panel_ready) { + if (_peripher_power) _peripher_power->claim(); + if (PIN_TFT_LEDA_CTL != -1) { + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + } + _isOn = true; + return true; + } + #endif + if (_peripher_power) { _peripher_power->claim(); #ifdef HELTEC_V4_R8_TFT @@ -91,6 +107,7 @@ bool ST7789LCDDisplay::begin() { if (PIN_TFT_LEDA_CTL != -1) { digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); } + _panel_ready = true; #endif _isOn = true; diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index f6d7446b8..baa72bf01 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -13,6 +13,9 @@ class ST7789LCDDisplay : public DisplayDriver { #endif Adafruit_ST7789 display; bool _isOn; +#ifdef HELTEC_V4_R8_TFT + bool _panel_ready = false; // panel configured once; wake is backlight-only +#endif uint16_t _color; RefCountedDigitalPin* _peripher_power; diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 9a07db92f..434a83cae 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -71,9 +71,21 @@ void HeltecV4R8Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { } void HeltecV4R8Board::powerOff() { - enterDeepSleep(0); +#if defined(HELTEC_V4_R8_TFT) && defined(DISPLAY_CLASS) + display.turnOff(); +#endif + + // Deliberately NOT enterDeepSleep(): that always arms an ext1 wake on + // P_LORA_DIO_1, so a node sitting in live traffic woke - and a deep-sleep + // wake is a full reboot - within seconds of showing "Turning OFF". With every + // wake source disabled the node stays down until RST or a power cycle, which + // is what asking for power-off means. + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + loRaFEMControl.setRxModeEnableWhenMCUSleep(); + esp_deep_sleep_start(); } + uint16_t HeltecV4R8Board::getBattMilliVolts() { analogReadResolution(12);