Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions libs/hardware/BoardConfig/include/BoardConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand All @@ -1420,7 +1426,7 @@ constexpr BoardProfile XTEINK_X4_PRO = {
479,
false,
0x14,
false,
true,
true,
2,
true,
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions libs/hardware/InputManager/include/InputManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
159 changes: 159 additions & 0 deletions libs/hardware/InputManager/src/InputManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int8_t>(-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; }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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<uint8_t>(reg >> 8));
Wire.write(static_cast<uint8_t>(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<uint16_t>(sizeof(cfg) - off);
const uint8_t len = static_cast<uint8_t>(remaining < GT911_CONFIG_CHUNK ? remaining : GT911_CONFIG_CHUNK);
if (!gt911ReadReg(static_cast<uint16_t>(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<uint8_t>(sum + cfg[i]);
}
if (static_cast<uint8_t>(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<uint8_t>((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<uint8_t>(sum - cfg[0] - switch1 + patched);
const uint8_t version = 0x00;
const uint8_t tail[2] = {static_cast<uint8_t>(-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;
Expand Down