// ESP32-S3 N16R8 + ILI9341 + XPT2046-compatible touch. // Libraries: Adafruit ILI9341, Adafruit GFX Library, XPT2046_Touchscreen. // LCD: SCK=12 MOSI=11 CS=10 DC=9 RESET=8. LCD SDO disconnected. // Touch: T_CLK=12 T_DIN=11 T_DO=13 T_CS=7 T_IRQ=6 (polled). // SD disconnected. Keep the existing working power/backlight connections. // No startup calibration. Adjust touch mapping constants below if needed. #include #include #include #include #include Adafruit_ILI9341 tft(&SPI, 9, 10, 8); XPT2046_Touchscreen touch(7); // Small off-screen canvas avoids visible erase/redraw flashes. GFXcanvas16 eyes(280, 112); // Approximate raw range after touch.setRotation(1), not calibrated pixels. constexpr int TOUCH_MIN_X = 200, TOUCH_MAX_X = 3900; constexpr int TOUCH_MIN_Y = 200, TOUCH_MAX_Y = 3900; constexpr bool TOUCH_SWAP_XY = false; constexpr bool TOUCH_FLIP_X = true, TOUCH_FLIP_Y = true; constexpr uint32_t IDLE_AFTER_MS = 15000; uint32_t idleBlinkAt = 0, idleLookAt = 0; bool idle = false, idleLookingAway = false; float gazeX = 0, gazeY = 0, targetX = 0, targetY = 0; uint32_t lastTouch = 0, blinkStart = 0, lastFrame = 0, lastDown = 0; bool looking = false, blinking = false, contact = false; int previousX = 999, previousY = 999, previousOpen = -1; void renderEyes(int dx, int dy, int opening) { eyes.fillScreen(ILI9341_BLACK); for (int i = 0; i < 2; ++i) { int x = i == 0 ? 72 : 208; eyes.fillRoundRect(x-49, 12, 98, 88, 30, ILI9341_WHITE); eyes.fillCircle(x+dx, 56+dy, 25, ILI9341_CYAN); eyes.fillCircle(x+dx, 56+dy, 14, ILI9341_BLACK); eyes.fillCircle(x+dx-7, 49+dy, 5, ILI9341_WHITE); int cover = (100-opening)*44/100; if (cover > 0) { eyes.fillRect(x-50, 12, 100, cover, ILI9341_BLACK); eyes.fillRect(x-50, 100-cover, 100, cover, ILI9341_BLACK); } if (opening < 7) eyes.fillRoundRect(x-39, 54, 78, 4, 2, ILI9341_CYAN); } tft.drawRGBBitmap(20, 54, eyes.getBuffer(), 280, 112); } void setup() { Serial.begin(115200); pinMode(10, OUTPUT); digitalWrite(10, HIGH); pinMode(7, OUTPUT); digitalWrite(7, HIGH); pinMode(6, INPUT_PULLUP); SPI.begin(12, 13, 11, 10); tft.begin(10000000); tft.setRotation(1); touch.begin(SPI); touch.setRotation(1); if (!eyes.getBuffer()) { tft.fillScreen(ILI9341_RED); Serial.println("Eye canvas allocation failed."); while (true) delay(1000); } randomSeed(micros()); tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_CYAN); tft.setTextSize(2); tft.setCursor(46, 207); tft.print("Touch to look!"); lastFrame = lastTouch = millis(); } void loop() { uint32_t now = millis(); if (touch.touched()) { TS_Point p = touch.getPoint(); float rawX = TOUCH_SWAP_XY ? p.y : p.x; float rawY = TOUCH_SWAP_XY ? p.x : p.y; float px = constrain((rawX-TOUCH_MIN_X)*319.0f/(TOUCH_MAX_X-TOUCH_MIN_X), 0.0f, 319.0f); if (TOUCH_FLIP_X) px = 319.0f-px; float py = constrain((rawY-TOUCH_MIN_Y)*239.0f/(TOUCH_MAX_Y-TOUCH_MIN_Y), 0.0f, 239.0f); if (TOUCH_FLIP_Y) py = 239.0f-py; idle = false; idleLookingAway = false; if (!contact) { blinking = true; blinkStart = now; } contact = true; lastDown = lastTouch = now; looking = true; targetX = (px-159.5f)/159.5f*20.0f; targetY = (py-119.5f)/119.5f*14.0f; } else if (contact && now-lastDown > 100) { contact = false; // Short release debounce prevents repeated blinks. } if (looking && now-lastTouch >= 3000) { looking = false; targetX = targetY = 0; } // "Unattended" currently means no touch, not camera-based presence detection. if (!contact && !looking && now-lastTouch >= IDLE_AFTER_MS) { if (!idle) { idle = true; idleLookingAway = false; idleBlinkAt = now + random(400, 1600); idleLookAt = now + random(900, 2200); } if (!blinking && int32_t(now-idleBlinkAt) >= 0) { blinking = true; blinkStart = now; idleBlinkAt = now + random(2500, 6500); } if (int32_t(now-idleLookAt) >= 0) { idleLookingAway = !idleLookingAway; if (idleLookingAway) { targetX = random(0, 2) ? random(9, 21) : -random(9, 21); targetY = random(-12, 13); idleLookAt = now + random(800, 2000); } else { targetX = targetY = 0; idleLookAt = now + random(1800, 4500); } } } if (now-lastFrame < 33) { delay(2); return; } float dt = (now-lastFrame)/1000.0f; lastFrame = now; float blend = 1.0f-expf(-12.0f*dt); gazeX += (targetX-gazeX)*blend; gazeY += (targetY-gazeY)*blend; int opening = 100; if (blinking) { uint32_t elapsed = now-blinkStart; if (elapsed < 100) opening = 100-int(elapsed); else if (elapsed < 170) opening = 0; else if (elapsed < 330) opening = int((elapsed-170)*100/160); else blinking = false; } int dx = lroundf(gazeX), dy = lroundf(gazeY); if (dx != previousX || dy != previousY || opening != previousOpen) { renderEyes(dx, dy, opening); previousX = dx; previousY = dy; previousOpen = opening; } }