Files
wadamesh/variants/tdisplay_p4/Hi8561Touch.cpp
T
Kaj SchittecatandClaude Opus 4.8 99423e5bf8 touch: beta_47 — Discover app (find/list/map nearby nodes + wardriving), Attaky release wiring, P4 LCD
- New Discover app: active NODE_DISCOVER sweep -> signal-ranked nearby list, tap-to-add-contact, GPS wardriving log (SD CSV) + signal-coloured coverage overlay on the map. Board-agnostic.
- T-Display P4 TFT-LCD (HI8561) variant support (WADA_P4_LCD build; AMOLED bin untouched).
- Wire the Attaky Core board (#158, @attakygit) into the release matrix + web flasher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:45:05 +02:00

416 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: GPL-3.0-or-later
// LilyGo T-Display P4 (AMOLED) touch input — implements the shared touch-input
// API (HeltecV4CapTouch.h) for the board's HI8561 capacitive controller.
//
// The HI8561 shares I2C_1 (Wire, SDA=7 / SCL=8) with the XL9535 expander, the
// PCF8563 RTC and the BQ27220 gauge. Wire is already begun by the XL9535 driver
// (xl9535.begin(7,8) in main.cpp) and the controller's RST/INT lines are driven
// by the expander — powerOnSequence() releases TOUCH_RST (IO3) before we probe.
// So, like RakTapV2Touch, this driver uses the shared Wire and must NOT re-init
// or reset the bus.
//
// STATE MACHINE: the tap/swipe/long-press gesture logic below is the proven one
// shared with the CHSC6x (Heltec V4) and FT5x06 (RAK Tap) drivers — only the
// physical report read (hi8561ReadPoint) and the panel geometry differ.
//
// ⚠️ FIRST-BOOT STATUS (display-first): hi8561ReadPoint() currently reports NO
// touch — the panel boots and renders, but taps aren't delivered yet. Enabling
// real touch is a single localised change: fill in the HI8561 report parse (addr
// 0x48; see the TODO in hi8561ReadPoint) and verify the coordinate mapping on the
// device. A wrong report layout produces phantom presses that lock the UI, so it
// stays disabled until it can be validated against hardware. Everything else
// (probe, async poll task, gesture machine, rotation) is complete.
#if defined(HAS_HI8561_TOUCH) && defined(ESP32)
#include <helpers/input/HeltecV4CapTouch.h> // shared touch-API contract (this file lives in variants/, not next to it)
#include <Arduino.h>
#include <Wire.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <helpers/ui/MomentaryButton.h>
#ifndef PIN_TOUCH_SDA
#define PIN_TOUCH_SDA 7
#endif
#ifndef PIN_TOUCH_SCL
#define PIN_TOUCH_SCL 8
#endif
// RM69A10 AMOLED is a fixed 568×1232 portrait panel (no display rotation on this
// board — CAP_ROTATABLE=0). The controller reports in the same portrait space,
// so raw == screen and the default (identity) mapping applies.
// LVGL renders at 284x616 (half the 568x1232 panel; RM69A10Display upscales 2x on flush), so touch
// coordinates must be reported in that SAME 284x616 logical space — the GT9895's native 1060x2400
// grid is scaled straight to 284x616 here.
// Logical (LVGL) space = native panel / UI-scale(2). The LilyGo T-Display P4 has TWO panel SKUs,
// each with its OWN touch controller (a build-time choice — HAS_TDP4_LCD):
// AMOLED (RM69A10, 568x1232) -> 284x616 logical, touch = Goodix GT9895 (0x5D).
// TFT-LCD (HI8561, 540x1168) -> 270x584 logical, touch = HI8561 integrated TDDI touch (0x68).
#if defined(HAS_TDP4_LCD)
static const int SCR_W = 270;
static const int SCR_H = 584;
static const int RAW_W = 270;
static const int RAW_H = 584;
// The HI8561 is a TDDI (touch+display in one). Its integrated touch is at I2C 0x68 and reads via an
// indirect ERAM-pointer protocol (ported from LilyGo cpp_bus_driver hi8561_touch.cpp, GPL-3.0):
// once at init, read a touch-info start-address out of the ERAM section table (InitAddressInfo);
// then each poll writes {0xF3, addr(BE32), 0x03} and reads the point back (x/y big-endian, 0xFFFF =
// no finger). It reports in panel-native 540x1168; we scale /2 (SCR_W/HI8561_NATIVE_W) to logical.
// TODO(device): confirm the native grid + axis orientation against on-screen taps (the touch-debug
// overlay shows raw coords, via heltecV4CapTouchGetRaw).
static const uint8_t HI8561_ADDR = 0x68;
static const uint32_t HI8561_ERAM_BASE = 0x20011000;
static const uint16_t HI8561_ERAM_SIZE = 4096;
// kEsramSectionInfoStartAddress = base + 4 + 25*8 + 4 = 0x200110D0 (see cpp_bus_driver hi8561_touch.h).
static const uint32_t HI8561_ESRAM_SECTION_INFO = HI8561_ERAM_BASE + 4 + 25 * 8 + 4;
static const uint8_t HI8561_POINT_OFFSET = 3; // kTouchPointAddressOffset
static const int HI8561_NATIVE_W = 540;
static const int HI8561_NATIVE_H = 1168;
static uint32_t s_hi8561_info_addr = 0; // touch_info_start_address_ (learned at init)
#else
static const int SCR_W = 284;
static const int SCR_H = 616;
static const int RAW_W = 284;
static const int RAW_H = 616;
// NOTE: despite this file's name, the LilyGo T-Display P4 AMOLED (RM69A10) SKU actually ships a
// Goodix GT9895 touch controller at 0x5D — NOT the HI8561 (that's the LCD SKU, gated above). An I2C
// bus scan on the real board found 0x5D. This path talks to the GT9895. Ported from LilyGo
// cpp_bus_driver gt9895.cpp: the touch report is at register 0x00010308; each read = write that
// 32-bit address big-endian (4 bytes), then read the report.
static const uint8_t GT9895_ADDR = 0x5D;
static const uint8_t GT9895_TOUCH_CMD[4] = { 0x00, 0x01, 0x03, 0x08 }; // register 0x00010308, big-endian
// The GT9895 reports in its native 1060x2400 digitiser grid; scale to the 284x616 logical space.
static const int GT9895_NATIVE_W = 1060;
static const int GT9895_NATIVE_H = 2400;
#endif
static bool s_init_ok = false;
static bool s_given_up = false;
static int s_retries = 0;
static char s_scan_str[128] = "scan: (not run)";
static volatile uint16_t s_dbg_rawx = 0, s_dbg_rawy = 0;
static bool s_have_touch = false;
static uint16_t s_cur_x = 0, s_cur_y = 0;
static bool s_down = false;
static unsigned long s_down_at = 0;
static uint16_t s_start_x = 0, s_start_y = 0;
static uint16_t s_last_x = 0, s_last_y = 0;
static bool s_live = false;
static uint16_t s_live_x = 0, s_live_y = 0;
static bool s_tap_pending = false;
static uint16_t s_tap_x = 0, s_tap_y = 0;
static bool s_swiping_now = false;
static bool s_swipe_pending = false;
static int8_t s_swipe_x = 0, s_swipe_y = 0;
static uint8_t s_ui_rotation = 0;
static uint8_t s_point_rotation = 0;
// Larger panel -> larger swipe threshold than the 240×320 boards.
#ifndef P4_TOUCH_SWIPE_MIN
#define P4_TOUCH_SWIPE_MIN 60
#endif
#ifndef P4_TOUCH_TAP_MOVE_MAX
#define P4_TOUCH_TAP_MOVE_MAX 24
#endif
#ifndef P4_TOUCH_LONG_MS
#define P4_TOUCH_LONG_MS 1000
#endif
// ---- Touch-controller low-level access ----
// Write an N-byte command (repeated start, no stop), then read M bytes. One helper per SKU (only
// the compiled one is used); the transaction shape is identical, just the I2C address differs.
#if defined(HAS_TDP4_LCD)
static bool hi8561WriteRead(const uint8_t* cmd, uint8_t clen, uint8_t* out, uint8_t olen) {
Wire.beginTransmission(HI8561_ADDR);
Wire.write(cmd, clen);
if (Wire.endTransmission(false) != 0) return false; // repeated start, no stop
uint8_t got = Wire.requestFrom((int)HI8561_ADDR, (int)olen);
if (got != olen) return false;
for (uint8_t i = 0; i < olen; i++) out[i] = Wire.read();
return true;
}
#else
static bool gt9895WriteRead(const uint8_t* cmd, uint8_t clen, uint8_t* out, uint8_t olen) {
Wire.beginTransmission(GT9895_ADDR);
Wire.write(cmd, clen);
if (Wire.endTransmission(false) != 0) return false; // repeated start, no stop
uint8_t got = Wire.requestFrom((int)GT9895_ADDR, (int)olen);
if (got != olen) return false;
for (uint8_t i = 0; i < olen; i++) out[i] = Wire.read();
return true;
}
#endif
// ---- Init (called by UITask loop every tick until success or give-up) ----
bool heltecV4CapTouchBegin() {
if (s_init_ok) return true;
if (s_given_up) return false;
++s_retries;
if (s_retries > 3) {
s_given_up = true;
Serial.println("[TOUCH] HI8561 giving up after 3 attempts — booting without touch");
return false;
}
// Wire is up on 7/8 (xl9535.begin). Bump to 400 kHz + short timeout so a stuck
// controller can't stall the UI loop.
Wire.setClock(400000);
Wire.setTimeOut(20);
delay(10);
// Bring-up: scan the I2C bus once so we can see exactly what touch controller is present
// (HI8561=0x68, GT9895=0x5D/0x14, others) — the RM69A10 variant may pair with GT9895, not HI8561.
if (s_retries == 1) {
char sc[96]; int n = 0; n += snprintf(sc, sizeof sc, "[TOUCH] i2c7/8 scan:");
for (uint8_t a = 0x08; a < 0x78 && n < (int)sizeof sc - 6; a++) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) n += snprintf(sc + n, sizeof sc - n, " %02X", a);
}
Serial.println(sc);
}
#if defined(HAS_TDP4_LCD)
// HI8561 integrated TDDI touch at 0x68 — probe, then learn the ERAM touch-info pointer once.
Wire.beginTransmission(HI8561_ADDR);
if (Wire.endTransmission() != 0) {
snprintf(s_scan_str, sizeof s_scan_str, "i2c7/8: no ACK at 0x%02X", HI8561_ADDR);
Serial.printf("[TOUCH] HI8561 not found (retry %d/3)\n", s_retries);
return false;
}
// InitAddressInfo: read touch_info_start_address_ out of the ERAM section table (offset 8).
{
const uint32_t sa = HI8561_ESRAM_SECTION_INFO;
uint8_t cmd[6] = { 0xF3, (uint8_t)(sa >> 24), (uint8_t)(sa >> 16),
(uint8_t)(sa >> 8), (uint8_t)sa, 0x03 };
uint8_t buf[48] = {0};
if (!hi8561WriteRead(cmd, sizeof cmd, buf, sizeof buf)) {
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X hi8561 info read fail", HI8561_ADDR);
Serial.printf("[TOUCH] %s (retry %d/3)\n", s_scan_str, s_retries);
return false;
}
uint32_t info = (uint32_t)buf[8] | ((uint32_t)buf[9] << 8) |
((uint32_t)buf[10] << 16) | ((uint32_t)buf[11] << 24);
if (info < HI8561_ERAM_BASE || info >= (HI8561_ERAM_BASE + HI8561_ERAM_SIZE)) {
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X hi8561 info bad 0x%08lX",
HI8561_ADDR, (unsigned long)info);
Serial.printf("[TOUCH] %s (retry %d/3)\n", s_scan_str, s_retries);
return false;
}
s_hi8561_info_addr = info;
}
s_init_ok = true;
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X hi8561 OK (info 0x%08lX)",
HI8561_ADDR, (unsigned long)s_hi8561_info_addr);
Serial.printf("[TOUCH] %s\n", s_scan_str);
return true;
#else
Wire.beginTransmission(GT9895_ADDR);
if (Wire.endTransmission() != 0) {
snprintf(s_scan_str, sizeof s_scan_str, "i2c7/8: no ACK at 0x%02X", GT9895_ADDR);
Serial.printf("[TOUCH] GT9895 not found (retry %d/3)\n", s_retries);
return false;
}
s_init_ok = true;
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X gt9895 OK (retry %d)", GT9895_ADDR, s_retries - 1);
Serial.printf("[TOUCH] %s\n", s_scan_str);
return true;
#endif
}
// ---- Coordinate mapping (identity in portrait; rotation kept for parity) ----
static void mapRaw(uint16_t rx, uint16_t ry, uint16_t* ox, uint16_t* oy) {
int sx, sy;
switch (s_point_rotation) {
case 1: sx = (RAW_H - 1) - (int)ry; sy = (int)rx; break; // ROT_90
case 3: sx = (int)ry; sy = (RAW_W - 1) - (int)rx; break; // ROT_270
default: sx = (int)rx; sy = (int)ry; break; // portrait / 180
}
if (sx < 0) sx = 0; if (sx >= SCR_W) sx = SCR_W - 1;
if (sy < 0) sy = 0; if (sy >= SCR_H) sy = SCR_H - 1;
*ox = (uint16_t)sx;
*oy = (uint16_t)sy;
}
// ---- Physical report read ------------------------------------------------
// Returns 1 = finger down (rx/ry set to logical coords), 0 = no touch, -1 = I2C error.
// TODO(device): verify X/Y axis orientation + origin against on-screen taps (may need flip/swap in mapRaw).
#if defined(HAS_TDP4_LCD)
// HI8561 integrated touch: read finger 1's point at (info_addr + kTouchPointAddressOffset). The
// report is 5 bytes: [x_hi][x_lo][y_hi][y_lo][pressure] (x/y big-endian); 0xFFFF/0xFFFF = no touch.
static int hi8561ReadPoint(uint16_t* rx, uint16_t* ry) {
if (!s_hi8561_info_addr) return -1;
const uint32_t addr = s_hi8561_info_addr + HI8561_POINT_OFFSET; // finger 1 (stride handled at init)
uint8_t cmd[6] = { 0xF3, (uint8_t)(addr >> 24), (uint8_t)(addr >> 16),
(uint8_t)(addr >> 8), (uint8_t)addr, 0x03 };
uint8_t buf[5] = {0};
if (!hi8561WriteRead(cmd, sizeof cmd, buf, sizeof buf)) return -1;
uint16_t x = ((uint16_t)buf[0] << 8) | buf[1]; // big-endian
uint16_t y = ((uint16_t)buf[2] << 8) | buf[3];
if (x == 0xFFFF && y == 0xFFFF) return 0; // no finger down
if (x >= HI8561_NATIVE_W) x = HI8561_NATIVE_W - 1; // clamp a stray out-of-range report
if (y >= HI8561_NATIVE_H) y = HI8561_NATIVE_H - 1;
// Scale the native 540x1168 grid down to the 270x584 logical space.
*rx = (uint16_t)((uint32_t)x * SCR_W / HI8561_NATIVE_W);
*ry = (uint16_t)((uint32_t)y * SCR_H / HI8561_NATIVE_H);
return 1;
}
#else
// GT9895 single-touch read: 16 bytes = TOUCH_POINT_ADDRESS_OFFSET(8) + 1*SINGLE_TOUCH_POINT_DATA_SIZE(8).
// buf[2] = finger count; point data at offset 8: [id/status][?][x_lo][x_hi][y_lo][y_hi][pressure][?]
// (x/y little-endian).
static int hi8561ReadPoint(uint16_t* rx, uint16_t* ry) {
uint8_t buf[16] = {0};
if (!gt9895WriteRead(GT9895_TOUCH_CMD, 4, buf, sizeof buf)) return -1;
uint8_t fingers = buf[2];
if (fingers < 1 || fingers > 10) return 0; // no finger down
uint32_t x = (uint32_t)buf[10] | ((uint32_t)buf[11] << 8);
uint32_t y = (uint32_t)buf[12] | ((uint32_t)buf[13] << 8);
// Scale the native 1060x2400 grid down to the 284x616 logical space.
*rx = (uint16_t)(x * (uint32_t)SCR_W / GT9895_NATIVE_W);
*ry = (uint16_t)(y * (uint32_t)SCR_H / GT9895_NATIVE_H);
return 1;
}
#endif
static void hi8561Poll() {
uint16_t rx = 0, ry = 0;
if (hi8561ReadPoint(&rx, &ry) == 1) {
s_dbg_rawx = rx;
s_dbg_rawy = ry;
mapRaw(rx, ry, &s_cur_x, &s_cur_y);
s_have_touch = true;
} else {
s_have_touch = false;
}
}
void heltecV4CapTouchGetRaw(uint16_t* rx, uint16_t* ry) {
if (rx) *rx = s_dbg_rawx;
if (ry) *ry = s_dbg_rawy;
}
// ---- Gesture state machine (shared logic; mirrors RakTapV2Touch) ----
int heltecV4CapTouchCheck() {
if (!s_init_ok) return BUTTON_EVENT_NONE;
hi8561Poll();
if (s_have_touch) {
s_live = true;
s_live_x = s_cur_x;
s_live_y = s_cur_y;
if (!s_down) {
s_down = true;
s_down_at = millis();
s_start_x = s_cur_x;
s_start_y = s_cur_y;
s_swiping_now = false;
}
s_last_x = s_cur_x;
s_last_y = s_cur_y;
if (!s_swiping_now) {
int adx = abs((int)s_last_x - (int)s_start_x);
int ady = abs((int)s_last_y - (int)s_start_y);
if (adx >= P4_TOUCH_SWIPE_MIN && adx > ady) s_swiping_now = true;
}
return BUTTON_EVENT_NONE;
}
if (s_down) {
s_down = false;
s_live = false;
s_swiping_now = false;
unsigned long dur = millis() - s_down_at;
int adx = abs((int)s_last_x - (int)s_start_x);
int ady = abs((int)s_last_y - (int)s_start_y);
if (adx >= P4_TOUCH_SWIPE_MIN && adx > (ady + 8)) {
bool left = ((int)s_last_x - (int)s_start_x) < 0;
s_swipe_x = left ? -1 : 1;
s_swipe_y = 0;
s_swipe_pending = true;
return left ? BUTTON_EVENT_DOUBLE_CLICK : BUTTON_EVENT_TRIPLE_CLICK;
}
if (ady >= P4_TOUCH_SWIPE_MIN && ady > (adx + 8)) {
s_swipe_x = 0;
s_swipe_y = ((int)s_last_y - (int)s_start_y) < 0 ? -1 : 1;
s_swipe_pending = true;
return BUTTON_EVENT_NONE;
}
if (dur >= 12 && dur < (unsigned long)P4_TOUCH_LONG_MS &&
adx <= P4_TOUCH_TAP_MOVE_MAX && ady <= P4_TOUCH_TAP_MOVE_MAX) {
s_tap_x = s_last_x;
s_tap_y = s_last_y;
s_tap_pending = true;
return BUTTON_EVENT_CLICK;
}
if (dur >= (unsigned long)P4_TOUCH_LONG_MS) {
return BUTTON_EVENT_LONG_PRESS;
}
} else {
s_live = false;
}
return BUTTON_EVENT_NONE;
}
bool heltecV4CapTouchGetLive(uint16_t* x, uint16_t* y) {
if (!s_live) return false;
if (x) *x = s_live_x;
if (y) *y = s_live_y;
return true;
}
bool heltecV4CapTouchPopTap(uint16_t* x, uint16_t* y) {
if (!s_tap_pending) return false;
s_tap_pending = false;
if (x) *x = s_tap_x;
if (y) *y = s_tap_y;
return true;
}
bool heltecV4CapTouchPopSwipe(int8_t* xd, int8_t* yd) {
if (!s_swipe_pending) return false;
s_swipe_pending = false;
if (xd) *xd = s_swipe_x;
if (yd) *yd = s_swipe_y;
return true;
}
// ---- Background poll (core 0) ----
static TaskHandle_t s_poll_task = nullptr;
static volatile bool s_async = false;
static uint32_t s_period_ms = 8;
static void touchPollTask(void* arg) {
(void)arg;
for (;;) {
heltecV4CapTouchCheck();
vTaskDelay(pdMS_TO_TICKS(s_period_ms));
}
}
bool heltecV4CapTouchStartBackgroundPoll(uint32_t period_ms) {
if (s_async || !s_init_ok) return false;
s_period_ms = period_ms < 4 ? 4 : (period_ms > 100 ? 100 : period_ms);
BaseType_t ok = xTaskCreatePinnedToCore(touchPollTask, "hi8561_touch", 3072,
nullptr, 2, &s_poll_task, 0);
if (ok == pdPASS) { s_async = true; return true; }
return false;
}
bool heltecV4CapTouchIsAsyncPolling() { return s_async; }
bool heltecV4CapTouchIsSwiping() { return s_swiping_now; }
void heltecV4CapTouchSetRotation(uint8_t r) { s_ui_rotation = r & 3; }
void heltecV4CapTouchSetPointRotation(uint8_t r) { s_point_rotation = r & 3; }
const char* heltecV4CapTouchDebug() { return s_scan_str; }
#endif // HAS_HI8561_TOUCH && ESP32