feat(esp32-s3-lcd-43): add touch test harness and coordinate transformation
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -168,6 +168,17 @@ Use `#pragma once` (not `#ifndef` guards).
|
|||||||
| `status` | Print state + memory info |
|
| `status` | Print state + memory info |
|
||||||
| `reboot` | Restart device |
|
| `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
|
## Monitor Daemon and Logging
|
||||||
|
|
||||||
The build system includes a Python-based monitor agent that provides JSON logging and command pipes.
|
The build system includes a Python-based monitor agent that provides JSON logging and command pipes.
|
||||||
|
|||||||
@@ -58,16 +58,137 @@ int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISP_W; }
|
|||||||
|
|
||||||
int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISP_H; }
|
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 DisplayDriverGFX::readTouch() {
|
||||||
TouchEvent evt;
|
TouchEvent evt;
|
||||||
if(!_gfx)
|
if(!_gfx)
|
||||||
return evt;
|
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;
|
int32_t x, y;
|
||||||
bool pressed = _gfx->getTouch(&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)
|
// Detect transitions (press/release)
|
||||||
if(pressed && !_lastTouch.pressed) {
|
if(pressed && !_lastTouch.pressed) {
|
||||||
// Press transition: finger just touched down
|
// Press transition: finger just touched down
|
||||||
@@ -76,11 +197,15 @@ TouchEvent DisplayDriverGFX::readTouch() {
|
|||||||
evt.downY = static_cast<int>(y);
|
evt.downY = static_cast<int>(y);
|
||||||
_lastTouch.downX = evt.downX;
|
_lastTouch.downX = evt.downX;
|
||||||
_lastTouch.downY = evt.downY;
|
_lastTouch.downY = evt.downY;
|
||||||
|
_touchBounced = false;
|
||||||
} else if(!pressed && _lastTouch.pressed) {
|
} else if(!pressed && _lastTouch.pressed) {
|
||||||
// Release transition: finger just lifted
|
// Release transition: finger just lifted
|
||||||
evt.released = true;
|
evt.released = true;
|
||||||
evt.downX = _lastTouch.downX;
|
evt.downX = _lastTouch.downX;
|
||||||
evt.downY = _lastTouch.downY;
|
evt.downY = _lastTouch.downY;
|
||||||
|
// Start debounce window
|
||||||
|
_lastReleaseMs = now;
|
||||||
|
_touchBounced = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current position if still touched
|
// Current position if still touched
|
||||||
@@ -92,6 +217,11 @@ TouchEvent DisplayDriverGFX::readTouch() {
|
|||||||
_pressStartMs = millis();
|
_pressStartMs = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if debounce window has expired
|
||||||
|
if(_touchBounced && now - _lastReleaseMs >= TOUCH_DEBOUNCE_MS) {
|
||||||
|
_touchBounced = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Track previous state
|
// Track previous state
|
||||||
_lastTouch.pressed = pressed;
|
_lastTouch.pressed = pressed;
|
||||||
if(pressed) {
|
if(pressed) {
|
||||||
@@ -252,15 +382,15 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
|||||||
_gfx->fillScreen(0x001030); // Dark blue
|
_gfx->fillScreen(0x001030); // Dark blue
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
_gfx->fillRect(0, 0, DISP_W, 30, 0x1A1A); // Dark gray
|
_gfx->fillRect(0, 0, DISP_W, 40, 0x1A1A); // Dark gray
|
||||||
_gfx->setFont(&fonts::Font0); // Built-in minimal font
|
_gfx->setFont(&fonts::Font2);
|
||||||
_gfx->setTextColor(0xFFFF);
|
_gfx->setTextColor(0xFFFF);
|
||||||
_gfx->setTextSize(1);
|
_gfx->setTextSize(1);
|
||||||
_gfx->setCursor(5, 10);
|
_gfx->setCursor(10, 12);
|
||||||
_gfx->printf("KLUBHAUS");
|
_gfx->printf("KLUBHAUS");
|
||||||
|
|
||||||
// WiFi status
|
// WiFi status
|
||||||
_gfx->setCursor(DISP_W - 100, 10);
|
_gfx->setCursor(DISP_W - 120, 12);
|
||||||
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
|
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
|
||||||
|
|
||||||
// Get tile layouts from library helper
|
// Get tile layouts from library helper
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public:
|
|||||||
int width() override;
|
int width() override;
|
||||||
int height() override;
|
int height() override;
|
||||||
|
|
||||||
|
// Transform touch coordinates (handles rotated touch panels)
|
||||||
|
void transformTouch(int* x, int* y) override;
|
||||||
|
|
||||||
// Dashboard tile mapping
|
// Dashboard tile mapping
|
||||||
int dashboardTouch(int x, int y);
|
int dashboardTouch(int x, int y);
|
||||||
|
|
||||||
@@ -25,6 +28,9 @@ public:
|
|||||||
static DisplayDriverGFX& instance();
|
static DisplayDriverGFX& instance();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Test harness: parse serial commands to inject synthetic touches
|
||||||
|
bool parseTestTouch(int* outX, int* outY, bool* outPressed);
|
||||||
|
|
||||||
// Helper rendering functions
|
// Helper rendering functions
|
||||||
void drawBoot(const ScreenState& state);
|
void drawBoot(const ScreenState& state);
|
||||||
void drawAlert(const ScreenState& state);
|
void drawAlert(const ScreenState& state);
|
||||||
@@ -34,6 +40,9 @@ private:
|
|||||||
TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 };
|
TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 };
|
||||||
unsigned long _pressStartMs = 0;
|
unsigned long _pressStartMs = 0;
|
||||||
bool _isHolding = false;
|
bool _isHolding = false;
|
||||||
|
unsigned long _lastReleaseMs = 0;
|
||||||
|
bool _touchBounced = false;
|
||||||
|
bool _testMode = false;
|
||||||
|
|
||||||
// Screen tracking
|
// Screen tracking
|
||||||
ScreenID _lastScreen = ScreenID::BOOT;
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Read touch
|
// Read touch (includes test injection via serial "touch x y press/release")
|
||||||
TouchEvent evt = display.readTouch();
|
TouchEvent evt = display.readTouch();
|
||||||
|
|
||||||
// State machine tick
|
// State machine tick
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#define HINT_ANIMATION_MS 2000
|
#define HINT_ANIMATION_MS 2000
|
||||||
#define HINT_MIN_BRIGHTNESS 30
|
#define HINT_MIN_BRIGHTNESS 30
|
||||||
#define HINT_MAX_BRIGHTNESS 60
|
#define HINT_MAX_BRIGHTNESS 60
|
||||||
|
#define TOUCH_DEBOUNCE_MS 100
|
||||||
|
|
||||||
// ── Loop yield (prevents Task Watchdog on ESP32) ──
|
// ── Loop yield (prevents Task Watchdog on ESP32) ──
|
||||||
#ifndef LOOP_YIELD_MS
|
#ifndef LOOP_YIELD_MS
|
||||||
|
|||||||
@@ -223,6 +223,15 @@ void DoorbellLogic::onSilence() {
|
|||||||
|
|
||||||
void DoorbellLogic::silenceAlert() { 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) {
|
void DoorbellLogic::onAdmin(const String& cmd) {
|
||||||
Serial.printf("[ADMIN] %s\n", cmd.c_str());
|
Serial.printf("[ADMIN] %s\n", cmd.c_str());
|
||||||
if(cmd == "reboot") {
|
if(cmd == "reboot") {
|
||||||
@@ -364,7 +373,9 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(_state.screen == ScreenID::ALERT) {
|
if(_state.screen == ScreenID::ALERT) {
|
||||||
Serial.println("[TOUCH] ALERT tap");
|
Serial.printf("[%lu] [TOUCH] ALERT → DASHBOARD (dismiss)\n", millis());
|
||||||
|
dismissAlert();
|
||||||
|
return (int)TileAction::DISMISS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ public:
|
|||||||
|
|
||||||
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
||||||
void silenceAlert();
|
void silenceAlert();
|
||||||
|
/// Dismiss alert and return to dashboard (user tap on alert screen).
|
||||||
|
void dismissAlert();
|
||||||
void setScreen(ScreenID s);
|
void setScreen(ScreenID s);
|
||||||
/// Handle touch input — returns dashboard tile index if tapped, or -1.
|
/// Handle touch input — returns dashboard tile index if tapped, or -1.
|
||||||
int handleTouch(const TouchEvent& evt);
|
int handleTouch(const TouchEvent& evt);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ enum class TileAction {
|
|||||||
NONE,
|
NONE,
|
||||||
ALERT, // Trigger alert
|
ALERT, // Trigger alert
|
||||||
SILENCE, // Silence alert
|
SILENCE, // Silence alert
|
||||||
|
DISMISS, // Dismiss alert and return to dashboard
|
||||||
STATUS, // Send heartbeat/status
|
STATUS, // Send heartbeat/status
|
||||||
REBOOT, // Reboot device
|
REBOOT, // Reboot device
|
||||||
};
|
};
|
||||||
|
|||||||
1
vendor/esp32-s3-lcd-43/LovyanGFX
vendored
Submodule
1
vendor/esp32-s3-lcd-43/LovyanGFX
vendored
Submodule
Submodule vendor/esp32-s3-lcd-43/LovyanGFX added at 42998359d8
Reference in New Issue
Block a user