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.
This commit is contained in:
agessaman committed 2026-08-28 14:26:52 -07:00
1 parent 5d56267176
commit 11d7551ccd
6 files changed
+68 -14

No files matched your search

+1
View File
@@ -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
+1
View File
@@ -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
+33 -13
View File
@@ -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) {
+17
View File
@@ -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;
+3
View File
@@ -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;
+13 -1
View File
@@ -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);