feat(esp32-s3-lcd-43): add touch test harness and coordinate transformation

This commit is contained in:
2026-02-18 18:58:37 -08:00
parent e4609c6978
commit d6eb2cd561
9 changed files with 173 additions and 7 deletions

View File

@@ -168,6 +168,17 @@ Use `#pragma once` (not `#ifndef` guards).
| `status` | Print state + memory info |
| `reboot` | Restart device |
**Touch Test Commands** (ESP32-S3-LCD-4.3 only):
| Command | Action |
|---------|--------|
| `TEST:touch X Y press` | Inject synthetic press at raw panel coords (X,Y) |
| `TEST:touch X Y release` | Inject synthetic release at raw panel coords (X,Y) |
| `TEST:touch clear` | Clear test mode (required between touch sequences) |
Note: The S3 touch panel is rotated 180° relative to the display. Use raw panel coordinates:
- Display (100,140) → Raw (700, 340)
- Display (700,140) → Raw (100, 340)
## Monitor Daemon and Logging
The build system includes a Python-based monitor agent that provides JSON logging and command pipes.

View File

@@ -58,16 +58,137 @@ int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISP_W; }
int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISP_H; }
// ── Touch handling ──
// Transform touch coordinates to match display orientation
// GT911 touch panel on this board is rotated 180° relative to display
void DisplayDriverGFX::transformTouch(int* x, int* y) {
if(!x || !y)
return;
// Flip both axes: (0,0) becomes (width, height)
*x = DISP_W - *x;
*y = DISP_H - *y;
}
// Test harness: parse serial commands to inject synthetic touches
// Commands:
// TEST:touch x y press - simulate press at (x, y) [raw panel coords]
// TEST:touch x y release - simulate release at (x, y)
// TEST:touch clear - clear test mode
bool DisplayDriverGFX::parseTestTouch(int* outX, int* outY, bool* outPressed) {
if(!Serial.available())
return false;
// Only consume if it starts with 'T' - don't steal other commands
// Use "TEST:" prefix to avoid conflict with [CMD] echo
if(Serial.peek() != 'T') {
return false;
}
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if(!cmd.startsWith("TEST:touch"))
return false;
// Parse: touch x y press|release
int firstSpace = cmd.indexOf(' ');
if(firstSpace < 0)
return false;
String args = cmd.substring(firstSpace + 1);
args.trim();
if(args.equals("clear")) {
_testMode = false;
Serial.println("[TEST] Test mode cleared");
return false;
}
// Parse x y state
int secondSpace = args.indexOf(' ');
if(secondSpace < 0)
return false;
String xStr = args.substring(0, secondSpace);
String yState = args.substring(secondSpace + 1);
yState.trim();
int x = xStr.toInt();
int y = yState.substring(0, yState.indexOf(' ')).toInt();
String state = yState.substring(yState.indexOf(' ') + 1);
state.trim();
bool pressed = state.equals("press");
Serial.printf("[TEST] Injecting touch: (%d,%d) %s\n", x, y, pressed ? "press" : "release");
if(outX) *outX = x;
if(outY) *outY = y;
if(outPressed) *outPressed = pressed;
_testMode = true;
return true;
}
// Touch handling
TouchEvent DisplayDriverGFX::readTouch() {
TouchEvent evt;
if(!_gfx)
return evt;
// Check for test injection via serial
int testX, testY;
bool testPressed;
if(parseTestTouch(&testX, &testY, &testPressed)) {
// Handle test touch with same logic as real touch
unsigned long now = millis();
if(testPressed && !_lastTouch.pressed) {
evt.pressed = true;
evt.downX = testX;
evt.downY = testY;
_lastTouch.downX = evt.downX;
_lastTouch.downY = evt.downY;
_touchBounced = false;
} else if(!testPressed && _lastTouch.pressed) {
evt.released = true;
evt.downX = _lastTouch.downX;
evt.downY = _lastTouch.downY;
_lastReleaseMs = now;
_touchBounced = true;
}
if(testPressed) {
evt.x = testX;
evt.y = testY;
evt.downX = _lastTouch.downX;
evt.downY = _lastTouch.downY;
_pressStartMs = millis();
}
if(_touchBounced && now - _lastReleaseMs >= TOUCH_DEBOUNCE_MS) {
_touchBounced = false;
}
_lastTouch.pressed = testPressed;
if(testPressed) {
_lastTouch.x = evt.x;
_lastTouch.y = evt.y;
}
return evt;
}
int32_t x, y;
bool pressed = _gfx->getTouch(&x, &y);
// Debounce: ignore repeated press events within debounce window after release
unsigned long now = millis();
if(pressed && _touchBounced) {
// Within debounce window - ignore this press
_lastTouch.pressed = pressed;
return evt;
}
// Detect transitions (press/release)
if(pressed && !_lastTouch.pressed) {
// Press transition: finger just touched down
@@ -76,11 +197,15 @@ TouchEvent DisplayDriverGFX::readTouch() {
evt.downY = static_cast<int>(y);
_lastTouch.downX = evt.downX;
_lastTouch.downY = evt.downY;
_touchBounced = false;
} else if(!pressed && _lastTouch.pressed) {
// Release transition: finger just lifted
evt.released = true;
evt.downX = _lastTouch.downX;
evt.downY = _lastTouch.downY;
// Start debounce window
_lastReleaseMs = now;
_touchBounced = true;
}
// Current position if still touched
@@ -92,6 +217,11 @@ TouchEvent DisplayDriverGFX::readTouch() {
_pressStartMs = millis();
}
// Check if debounce window has expired
if(_touchBounced && now - _lastReleaseMs >= TOUCH_DEBOUNCE_MS) {
_touchBounced = false;
}
// Track previous state
_lastTouch.pressed = pressed;
if(pressed) {
@@ -252,15 +382,15 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
_gfx->fillScreen(0x001030); // Dark blue
// Header
_gfx->fillRect(0, 0, DISP_W, 30, 0x1A1A); // Dark gray
_gfx->setFont(&fonts::Font0); // Built-in minimal font
_gfx->fillRect(0, 0, DISP_W, 40, 0x1A1A); // Dark gray
_gfx->setFont(&fonts::Font2);
_gfx->setTextColor(0xFFFF);
_gfx->setTextSize(1);
_gfx->setCursor(5, 10);
_gfx->setCursor(10, 12);
_gfx->printf("KLUBHAUS");
// WiFi status
_gfx->setCursor(DISP_W - 100, 10);
_gfx->setCursor(DISP_W - 120, 12);
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
// Get tile layouts from library helper

View File

@@ -18,6 +18,9 @@ public:
int width() override;
int height() override;
// Transform touch coordinates (handles rotated touch panels)
void transformTouch(int* x, int* y) override;
// Dashboard tile mapping
int dashboardTouch(int x, int y);
@@ -25,6 +28,9 @@ public:
static DisplayDriverGFX& instance();
private:
// Test harness: parse serial commands to inject synthetic touches
bool parseTestTouch(int* outX, int* outY, bool* outPressed);
// Helper rendering functions
void drawBoot(const ScreenState& state);
void drawAlert(const ScreenState& state);
@@ -34,6 +40,9 @@ private:
TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 };
unsigned long _pressStartMs = 0;
bool _isHolding = false;
unsigned long _lastReleaseMs = 0;
bool _touchBounced = false;
bool _testMode = false;
// Screen tracking
ScreenID _lastScreen = ScreenID::BOOT;

View File

@@ -21,7 +21,7 @@ void setup() {
}
void loop() {
// Read touch
// Read touch (includes test injection via serial "touch x y press/release")
TouchEvent evt = display.readTouch();
// State machine tick

View File

@@ -22,6 +22,7 @@
#define HINT_ANIMATION_MS 2000
#define HINT_MIN_BRIGHTNESS 30
#define HINT_MAX_BRIGHTNESS 60
#define TOUCH_DEBOUNCE_MS 100
// ── Loop yield (prevents Task Watchdog on ESP32) ──
#ifndef LOOP_YIELD_MS

View File

@@ -223,6 +223,15 @@ void DoorbellLogic::onSilence() {
void DoorbellLogic::silenceAlert() { onSilence(); }
void DoorbellLogic::dismissAlert() {
Serial.printf("[%lu] [DISMISS] Alert dismissed by user\n", millis());
_state.deviceState = DeviceState::SILENT;
_state.screen = ScreenID::DASHBOARD;
_state.alertTitle = "";
_state.alertBody = "";
_display->render(_state);
}
void DoorbellLogic::onAdmin(const String& cmd) {
Serial.printf("[ADMIN] %s\n", cmd.c_str());
if(cmd == "reboot") {
@@ -364,7 +373,9 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) {
}
if(_state.screen == ScreenID::ALERT) {
Serial.println("[TOUCH] ALERT tap");
Serial.printf("[%lu] [TOUCH] ALERT → DASHBOARD (dismiss)\n", millis());
dismissAlert();
return (int)TileAction::DISMISS;
}
}

View File

@@ -26,6 +26,8 @@ public:
/// Externally trigger silence (e.g. hold-to-silence gesture).
void silenceAlert();
/// Dismiss alert and return to dashboard (user tap on alert screen).
void dismissAlert();
void setScreen(ScreenID s);
/// Handle touch input — returns dashboard tile index if tapped, or -1.
int handleTouch(const TouchEvent& evt);

View File

@@ -12,6 +12,7 @@ enum class TileAction {
NONE,
ALERT, // Trigger alert
SILENCE, // Silence alert
DISMISS, // Dismiss alert and return to dashboard
STATUS, // Send heartbeat/status
REBOOT, // Reboot device
};

1
vendor/esp32-s3-lcd-43/LovyanGFX vendored Submodule

Submodule vendor/esp32-s3-lcd-43/LovyanGFX added at 42998359d8