diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index ee948fe0..b74c62ac 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -490,7 +490,11 @@ struct TouchConfig { uint16_t rawMinY, rawMaxY; bool synthesizeConfirm; // emit a CONFIRM button event on tap uint8_t i2cAddressAlt; // alternate I2C address to probe (GT911 0x14; 0 = none) - bool irqActiveLow; // touch IRQ asserted LOW (CHSC6x) + // Asserted level of the irq pin (true = LOW). Reported by + // InputManager::touchWakeIrqActiveLow() so a host can arm the right level as a + // light-sleep wake source; the GT911 INT-wake path also refuses to configure a + // profile that claims active-high, since it only programs a low-level hold. + bool irqActiveLow; // GT911 point-frame layout: false = datasheet standard (track-id at 0x8150, so // coords start at byte 1); true = coords start at byte 0 (no track-id), as seen // on M5Paper's GT911 which boots without a reset/config dance. Ignored (CHSC6x). @@ -1406,6 +1410,8 @@ constexpr BoardProfile XTEINK_X4_PRO = { // internal config on the standard reset dance — no host config upload needed. Mounted PORTRAIT // (reports X:0..480, Y:0..800) on the 800x480 landscape panel → swapXY=true; rawMax describe the // post-swap panel axes. Coords start at byte 0 of the 0x8150 read → gt911CoordsAtByte0=true. + // irqActiveLow=true: the GT911 INT asserts LOW in the low-level hold mode the driver programs + // for wake (FREEINK_GT911_INT_WAKE) and idles HIGH on its pull-up between report frames. // flipX/flipY pending a corner-tap test. {ctrl,sda,scl,irq,rst,addr,rawMinX,rawMaxX,rawMinY,rawMaxY, // synthConfirm,altAddr,irqActiveLow,coordsAtByte0,powerEnable,swapXY,flipX,flipY,hasHomeKey,pwrActiveHigh} {TouchController::Gt911, @@ -1420,7 +1426,7 @@ constexpr BoardProfile XTEINK_X4_PRO = { 479, false, 0x14, - false, + true, true, 2, true, @@ -1463,8 +1469,9 @@ constexpr BoardProfile XTEINK_X4_PRO = { // board_begin at IROM 0x420a23dc). Carried as power.latch0 so holdPowerRails() asserts it // early — without it the panel rail and the SD slot both stay unpowered (the bring-up // symptom: EPD BUSY never asserts, SD returns 0xFF). GPIO2 is a second board-init output - // driven LOW (role unknown); not modeled here. NOTE: GPIO1/GPIO2 are therefore NOT the ADC - // button ladder — that earlier assumption was wrong; the ladder pins remain unconfirmed. + // driven LOW: the touch controller's active-LOW rail, modeled above as the TouchConfig + // powerEnable=2 / powerEnableActiveHigh=false pair. NOTE: GPIO1/GPIO2 are therefore NOT the + // ADC button ladder — that earlier assumption was wrong; the ladder pins remain unconfirmed. {1}, 0, // displayControllerVariant: filled by the boot probe // Bezel overlap: the panel sits recessed, and 7px is the empirically-tuned diff --git a/libs/hardware/InputManager/include/InputManager.h b/libs/hardware/InputManager/include/InputManager.h index 3df38433..d598602f 100644 --- a/libs/hardware/InputManager/include/InputManager.h +++ b/libs/hardware/InputManager/include/InputManager.h @@ -140,6 +140,25 @@ class InputManager { // (~700 ms), while still down — a hold shortcut (e.g. open the reader menu). // Cleared each #update(). bool wasHomeKeyLongPressed() const; + // Home key currently held (level, not an edge). A motionless hold produces + // no new-data frames, so its long-press threshold is timed by #update() + // against the wall clock — a host that stops polling while this is true + // stretches that threshold by however long it stayed away. + bool isHomeKeyDown() const; + + // Touch INT line usable as a light-sleep wake source, or -1. A host can only + // wake from light sleep on a GPIO LEVEL (the edge detector is clock-gated + // while asleep), so this reports the pin ONLY once the driver has put the + // controller in a level-hold INT mode — the line then stays asserted from the + // report frame until the poll clears the status register, which a level wake + // cannot miss. -1 whenever that cannot be guaranteed (no touch, probe failed, + // FREEINK_GT911_INT_WAKE not built, or the controller refused the mode); + // callers must keep polling in that case. + int8_t touchWakeIrqPin() const; + // Asserted level of #touchWakeIrqPin (true = LOW), from the board profile's + // TouchConfig::irqActiveLow. The driver only ever programs a low-level hold, + // and refuses to report the pin at all on a profile that claims otherwise. + bool touchWakeIrqActiveLow() const; // Optional board hook for buttons that aren't direct GPIOs — e.g. a key // behind an I2C IO-expander (the LilyGo T5 S3 user button on its PCA9535). It @@ -148,6 +167,10 @@ class InputManager { // none. using ButtonHook = uint8_t (*)(); static void setButtonHook(ButtonHook hook) { s_buttonHook = hook; } + // True once a board has installed such a hook. Lets a host tell that some of + // its buttons are NOT readable as GPIO levels — e.g. a GPIO-interrupt idle + // wait cannot see them and must keep polling instead. + static bool hasButtonHook() { return s_buttonHook != nullptr; } // Boards such as Sticky wire OK/confirm and power/wake to the same GPIO. By // default a short click emits CONFIRM and a hold emits POWER. Apps that @@ -240,6 +263,12 @@ class InputManager { void gt911ClearStatus(); void beginFt6336u(); void pollFt6336u(unsigned long now); +#ifdef FREEINK_GT911_INT_WAKE + bool gt911WriteReg(uint16_t reg, const uint8_t* buf, uint8_t len); + // Put the INT line in low-level hold mode so it can serve as a light-sleep + // wake source; sets touchWakeIrqUsable on success. + void gt911ConfigureIntWake(); +#endif uint8_t currentState; uint8_t lastState; @@ -261,6 +290,7 @@ class InputManager { bool twoButtonLongPressActive; bool touchDataEnabled = false; // I2C up, controller present + bool touchWakeIrqUsable = false; // INT held in a level mode (touchWakeIrqPin) uint8_t gt911Addr = 0; // resolved GT911 address (0 until probed) unsigned long touchIrqPulseUntil = 0; // synthesized-confirm window after a press unsigned long touchReadAt = 0; // next scheduled I2C poll diff --git a/libs/hardware/InputManager/src/InputManager.cpp b/libs/hardware/InputManager/src/InputManager.cpp index 00fcf0b4..3a480d93 100644 --- a/libs/hardware/InputManager/src/InputManager.cpp +++ b/libs/hardware/InputManager/src/InputManager.cpp @@ -505,6 +505,26 @@ bool InputManager::hasTouch() const { #endif } +int8_t InputManager::touchWakeIrqPin() const { +#if FREEINK_CAP_TOUCH + return touchWakeIrqUsable ? BoardConfig::ACTIVE.touch.irq : static_cast(-1); +#else + return -1; +#endif +} + +bool InputManager::touchWakeIrqActiveLow() const { +#if FREEINK_CAP_TOUCH + // The board profile owns the polarity. gt911ConfigureIntWake() refuses to + // mark the line usable unless the profile agrees with the low-level hold it + // programs, so a caller that armed on touchWakeIrqPin() cannot read a + // polarity the driver did not actually configure. + return BoardConfig::ACTIVE.touch.irqActiveLow; +#else + return true; +#endif +} + InputManager::TouchPoint InputManager::getTouchPoint() const { return touchPoint; } bool InputManager::isTouchPressed() const { return touchPressed; } bool InputManager::wasTouchPressed() const { return touchPressedEvent; } @@ -678,6 +698,8 @@ bool InputManager::wasHomeKeyTapped() const { return touchHomeKeyTapEvent; } bool InputManager::wasHomeKeyLongPressed() const { return touchHomeKeyLongEvent; } +bool InputManager::isHomeKeyDown() const { return touchHomeKeyDown; } + void InputManager::beginTouch() { #if FREEINK_CAP_TOUCH const auto& t = BoardConfig::ACTIVE.touch; @@ -1090,6 +1112,11 @@ void InputManager::beginGt911() { } touchDataEnabled = (gt911Addr != 0); +#ifdef FREEINK_GT911_INT_WAKE + if (touchDataEnabled) { + gt911ConfigureIntWake(); + } +#endif #ifdef TOUCH_PROBE_DEBUG touchDebugPrintf( "[touch] GT911 probe: addr=0x%02X enabled=%d (sda=%d scl=%d " @@ -1367,6 +1394,138 @@ void InputManager::pollFt6336u(const unsigned long now) { } } +#ifdef FREEINK_GT911_INT_WAKE + +// GT911 configuration block: 0x8047..0x80FE, its 8-bit two's-complement +// checksum at 0x80FF, the apply flag at 0x8100. Module_Switch1 (0x804D) +// bits[1:0] pick the INT trigger — 0 rising edge, 1 falling edge, 2 low level, +// 3 high level. Only the level modes hold the line from the report frame until +// the host clears 0x814E; the edge modes emit one pulse per frame, and light +// sleep clock-gates the edge detector, so a pulse can be missed entirely. +namespace { +constexpr uint16_t GT911_CONFIG_START = 0x8047; +constexpr uint16_t GT911_CONFIG_LEN = 184; // 0x8047..0x80FE, checksum excluded +constexpr uint8_t GT911_CONFIG_CHUNK = 64; // Wire's buffer is 128 bytes +constexpr uint16_t GT911_MODULE_SWITCH1_OFF = 0x804D - GT911_CONFIG_START; +constexpr uint8_t GT911_INT_MODE_MASK = 0x03; +constexpr uint8_t GT911_INT_LOW_LEVEL = 0x02; +constexpr uint16_t GT911_CONFIG_FRESH = 0x8100; +// The controller clears Config_Fresh when it has actually re-read the table. +// Datasheet flows allow it a few ms; give it 100 ms before calling it refused. +constexpr uint8_t GT911_FRESH_POLL_MS = 10; +constexpr uint8_t GT911_FRESH_POLL_TRIES = 10; +} // namespace + +bool InputManager::gt911WriteReg(const uint16_t reg, const uint8_t* buf, const uint8_t len) { + Wire.beginTransmission(gt911Addr); + Wire.write(static_cast(reg >> 8)); + Wire.write(static_cast(reg & 0xFF)); + for (uint8_t i = 0; i < len; ++i) { + Wire.write(buf[i]); + } + return Wire.endTransmission() == 0; +} + +void InputManager::gt911ConfigureIntWake() { + const auto& t = BoardConfig::ACTIVE.touch; + if (gt911Addr == 0 || t.irq < 0) { + return; + } + if (!t.irqActiveLow) { + // Only the low-level hold is programmed below, i.e. an INT that asserts + // LOW. A profile that describes its INT as active-high disagrees with what + // this would configure, so fail closed rather than let a caller arm a + // high-level wake on a line that idles high (a permanent wake trigger). +#ifdef TOUCH_PROBE_DEBUG + touchDebugPrintf("[touch] GT911 int-wake: profile says INT active-high\n"); +#endif + return; + } + + uint8_t cfg[GT911_CONFIG_LEN + 1] = {}; // config block + its stored checksum + for (uint16_t off = 0; off < sizeof(cfg); off += GT911_CONFIG_CHUNK) { + const uint16_t remaining = static_cast(sizeof(cfg) - off); + const uint8_t len = static_cast(remaining < GT911_CONFIG_CHUNK ? remaining : GT911_CONFIG_CHUNK); + if (!gt911ReadReg(static_cast(GT911_CONFIG_START + off), cfg + off, len)) { + return; + } + } + + uint8_t sum = 0; + for (uint16_t i = 0; i < GT911_CONFIG_LEN; ++i) { + sum = static_cast(sum + cfg[i]); + } + if (static_cast(sum + cfg[GT911_CONFIG_LEN]) != 0) { + // Checksum mismatch: the table is not what this driver thinks it is, so + // rewriting any of it would be a guess. Leave the controller alone. +#ifdef TOUCH_PROBE_DEBUG + touchDebugPrintf("[touch] GT911 int-wake: config checksum mismatch\n"); +#endif + return; + } + + const uint8_t switch1 = cfg[GT911_MODULE_SWITCH1_OFF]; + if ((switch1 & GT911_INT_MODE_MASK) != GT911_INT_LOW_LEVEL) { + const uint8_t patched = static_cast((switch1 & ~GT911_INT_MODE_MASK) | GT911_INT_LOW_LEVEL); + // Config_Version 0x00 = apply without burning the table into the + // controller's own NVM, so the panel's factory config returns on the next + // reset and this can never permanently alter the hardware. Cost: the mode + // has to be re-applied on every boot (this runs from beginGt911()). + sum = static_cast(sum - cfg[0] - switch1 + patched); + const uint8_t version = 0x00; + const uint8_t tail[2] = {static_cast(-sum), 0x01}; // Config_Chksum, Config_Fresh + if (!gt911WriteReg(GT911_CONFIG_START, &version, 1) || !gt911WriteReg(0x804D, &patched, 1) || + !gt911WriteReg(0x80FF, tail, sizeof(tail))) { + return; + } + + // Wait for the controller to CONSUME the table, not just to acknowledge + // the write: it clears Config_Fresh itself once it has re-read the block. + // The 0x804D read-back below cannot stand in for this — that register + // holds whatever was written to it whether or not the config was applied, + // so it only proves the I2C transfer landed. A fresh flag still set after + // the timeout means the panel refused the version-0x00 table. + bool consumed = false; + for (uint8_t tries = 0; !consumed && tries < GT911_FRESH_POLL_TRIES; ++tries) { + delay(GT911_FRESH_POLL_MS); + uint8_t fresh = 1; + consumed = gt911ReadReg(GT911_CONFIG_FRESH, &fresh, 1) && fresh == 0; + } + if (!consumed) { +#ifdef TOUCH_PROBE_DEBUG + touchDebugPrintf("[touch] GT911 int-wake: Config_Fresh not cleared\n"); +#endif + return; + } + } + + uint8_t readback = 0; + if (!gt911ReadReg(0x804D, &readback, 1) || (readback & GT911_INT_MODE_MASK) != GT911_INT_LOW_LEVEL) { + // Second guard, on top of the Config_Fresh clear above: the mode the + // controller now reports has to be the one asked for. +#ifdef TOUCH_PROBE_DEBUG + touchDebugPrintf("[touch] GT911 int-wake: mode not applied (0x%02X)\n", readback); +#endif + return; + } + + // The reset dance leaves INT floating as a plain INPUT. A low-level INT is + // released between frames, so pull it up: an open-drain module then still + // reads HIGH at idle and the level wake cannot false-trigger. + pinMode(t.irq, INPUT_PULLUP); + // In low-level mode a pending report holds INT down until the status + // register is cleared, and re-reading the config table makes the controller + // latch one. Clear it so the line idles released and the app's first level + // wake is armed on a quiet pin instead of an already-asserted one. + gt911ClearStatus(); + touchWakeIrqUsable = true; +#ifdef TOUCH_PROBE_DEBUG + touchDebugPrintf("[touch] GT911 int-wake: INT=%d low-level hold\n", t.irq); +#endif +} + +#endif // FREEINK_GT911_INT_WAKE + void InputManager::pollGt911(const unsigned long now) { if (gt911Addr == 0) { return;