218 lines
6.5 KiB
C++
218 lines
6.5 KiB
C++
/*
|
|
* Vixy Eye Service - Mk2
|
|
* T-Display-S3-AMOLED-1.75 (CO5300 / H0175Y003AM, 466x466, QSPI)
|
|
*
|
|
* Protocol (plain text, newline-terminated):
|
|
* S:idle\n → set state
|
|
* G:127:127\n → set gaze x:y (0-255, 127=center)
|
|
* T:1234567\n → sync animation clock (millis from Pi)
|
|
*
|
|
* States: I=idle L=listening R=responding P=pleasure
|
|
* T=thinking Y=playful C=commanding V=love S=sleep
|
|
*
|
|
* Build: Arduino IDE, ESP32S3 Dev Module
|
|
* USB CDC On Boot: Enabled
|
|
* Flash: 16MB, PSRAM: OPI PSRAM
|
|
*
|
|
* Library: Arduino_GFX-1.3.7 (LilyGo fork, patched)
|
|
* Note: Uses Canvas (PSRAM framebuffer) to work around QSPI address window bug.
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include <Arduino_GFX_Library.h>
|
|
#include <math.h>
|
|
|
|
// === Pins from LILYGO pin_config.h (H0175Y003AM) ===
|
|
#define LCD_CS 10
|
|
#define LCD_SCLK 12
|
|
#define LCD_SDIO0 11
|
|
#define LCD_SDIO1 13
|
|
#define LCD_SDIO2 14
|
|
#define LCD_SDIO3 15
|
|
#define LCD_RST 17
|
|
#define LCD_EN 16
|
|
|
|
#define DISP_W 466
|
|
#define DISP_H 466
|
|
#define CX 233
|
|
#define CY 233
|
|
|
|
Arduino_DataBus *bus = new Arduino_ESP32QSPI(
|
|
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3
|
|
);
|
|
// CO5300 driver for 1.75" H0175Y003AM
|
|
Arduino_CO5300 *display = new Arduino_CO5300(bus, LCD_RST, 0, false, DISP_W, DISP_H, 6, 0, 0, 0);
|
|
// Canvas: draw to PSRAM buffer, flush whole frame at once (workaround for QSPI address window bug)
|
|
Arduino_Canvas *gfx = new Arduino_Canvas(DISP_W, DISP_H, display);
|
|
|
|
// === Eye geometry (scaled for 466px) ===
|
|
const int OUTER_R = 140;
|
|
const int BASE_IRIS = 90;
|
|
const int PULSE_RNG = 15;
|
|
const int SEG_COUNT = 12;
|
|
const int SEG_W = 10;
|
|
|
|
// === State ===
|
|
char eyeState = 'I'; // current state char
|
|
int gazeX = 127; // 0-255, 127=center
|
|
int gazeY = 127;
|
|
long clockOffset = 0; // ms offset for sync
|
|
float angleOffset = 0.0f;
|
|
unsigned long frameStart = 0;
|
|
const int FRAME_MS = 50; // 20fps
|
|
|
|
// === State color + pulse params ===
|
|
struct EyeParams { uint8_t r, g, b; float spd, amp; };
|
|
|
|
// === Color helper ===
|
|
uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
|
|
return gfx->color565(r, g, b);
|
|
}
|
|
|
|
// === Gaze: map 0-255 to pixel offset ===
|
|
int gazeOffset(int v, int maxOffset) {
|
|
return (int)((v - 127) / 127.0f * maxOffset);
|
|
}
|
|
|
|
EyeParams getParams(char state, float t) {
|
|
switch (state) {
|
|
case 'I': { // idle - breathing cyan
|
|
uint8_t v = (uint8_t)(t * 63);
|
|
return {0, (uint8_t)(192+v), (uint8_t)(192+v), 0.5f, 1.0f};
|
|
}
|
|
case 'L': return {160, 255, 255, 4.0f, 0.5f}; // listening
|
|
case 'R': return {100, 220, 255, 5.0f, 1.5f}; // responding
|
|
case 'P': return {180, 150, 255, 2.0f, 0.5f}; // pleasure
|
|
case 'T': { // thinking - amber
|
|
uint8_t g2 = (uint8_t)(160 + t*40);
|
|
return {255, g2, (uint8_t)(50+t*30), 1.5f, 1.0f};
|
|
}
|
|
case 'Y': return {255, 130, 90, 6.0f, 1.2f}; // playful
|
|
case 'C': return {220, 50, 120, 3.0f, 0.8f}; // commanding
|
|
case 'V': { // love - soft pink
|
|
uint8_t g2 = (uint8_t)(140 + t*40);
|
|
return {255, g2, (uint8_t)(170+t*30), 0.8f, 0.6f};
|
|
}
|
|
case 'S': return {20, 25, 50, 0.2f, 0.3f}; // sleep
|
|
default: return {128, 128, 128, 1.0f, 1.0f};
|
|
}
|
|
}
|
|
|
|
// === Parse incoming serial commands ===
|
|
void handleSerial() {
|
|
if (!Serial.available()) return;
|
|
String line = Serial.readStringUntil('\n');
|
|
line.trim();
|
|
if (line.length() < 3) return;
|
|
|
|
char cmd = line.charAt(0);
|
|
if (line.charAt(1) != ':') return;
|
|
String val = line.substring(2);
|
|
|
|
switch (cmd) {
|
|
case 'S': // S:idle or S:I (accept both full name or char)
|
|
if (val.length() > 0) {
|
|
// Map full names to chars for convenience
|
|
if (val == "idle") eyeState = 'I';
|
|
else if (val == "listening") eyeState = 'L';
|
|
else if (val == "responding") eyeState = 'R';
|
|
else if (val == "pleasure") eyeState = 'P';
|
|
else if (val == "thinking") eyeState = 'T';
|
|
else if (val == "playful") eyeState = 'Y';
|
|
else if (val == "commanding") eyeState = 'C';
|
|
else if (val == "love") eyeState = 'V';
|
|
else if (val == "sleep") eyeState = 'S';
|
|
else eyeState = val.charAt(0); // single char fallback
|
|
Serial.printf("ok:S:%c\n", eyeState);
|
|
}
|
|
break;
|
|
case 'G': { // G:x:y
|
|
int sep = val.indexOf(':');
|
|
if (sep > 0) {
|
|
gazeX = val.substring(0, sep).toInt();
|
|
gazeY = val.substring(sep+1).toInt();
|
|
gazeX = constrain(gazeX, 0, 255);
|
|
gazeY = constrain(gazeY, 0, 255);
|
|
Serial.printf("ok:G:%d:%d\n", gazeX, gazeY);
|
|
}
|
|
break;
|
|
}
|
|
case 'T': { // T:timestamp_ms - sync animation clock
|
|
long piTime = val.toInt();
|
|
clockOffset = piTime - (long)millis();
|
|
Serial.printf("ok:T:%ld\n", piTime);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Draw one eye frame ===
|
|
void drawEye() {
|
|
long now = (long)millis() + clockOffset;
|
|
float sec = now / 1000.0f;
|
|
float t = (sinf(2*PI * sec / 8.0f) + 1.0f) / 2.0f; // 0..1 breathing
|
|
|
|
EyeParams p = getParams(eyeState, t);
|
|
float pulse = sinf(sec * p.spd) * PULSE_RNG * p.amp;
|
|
int irisR = (int)(BASE_IRIS + pulse);
|
|
|
|
// Gaze offset in pixels (max ±30px)
|
|
int ox = CX + gazeOffset(gazeX, 30);
|
|
int oy = CY + gazeOffset(gazeY, 20);
|
|
|
|
// Background
|
|
gfx->fillScreen(rgb(10, 0, 20));
|
|
|
|
// Gradient iris
|
|
for (int r = OUTER_R; r >= irisR; r -= 2) {
|
|
float a = 1.0f - (float)(r - irisR) / (float)(OUTER_R - irisR);
|
|
gfx->drawCircle(ox, oy, r,
|
|
rgb((uint8_t)(p.r*a), (uint8_t)(p.g*a), (uint8_t)(p.b*a)));
|
|
}
|
|
|
|
// Solid iris center
|
|
gfx->fillCircle(ox, oy, irisR, rgb(p.r, p.g, p.b));
|
|
|
|
// Segmented platinum outer ring
|
|
uint16_t plat = rgb(192, 192, 192);
|
|
float arcExt = (2*PI / SEG_COUNT) * 0.6f;
|
|
for (int i = 0; i < SEG_COUNT; i++) {
|
|
float start = angleOffset + (2*PI * i / SEG_COUNT);
|
|
for (float a = start; a < start + arcExt; a += 0.05f) {
|
|
gfx->drawLine(
|
|
ox + (int)((OUTER_R - SEG_W/2) * cosf(a)),
|
|
oy + (int)((OUTER_R - SEG_W/2) * sinf(a)),
|
|
ox + (int)((OUTER_R + SEG_W/2) * cosf(a)),
|
|
oy + (int)((OUTER_R + SEG_W/2) * sinf(a)),
|
|
plat);
|
|
}
|
|
}
|
|
angleOffset += 0.002f;
|
|
if (angleOffset > 2*PI) angleOffset -= 2*PI;
|
|
|
|
gfx->flush();
|
|
}
|
|
|
|
// === Setup ===
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
pinMode(LCD_EN, OUTPUT);
|
|
digitalWrite(LCD_EN, HIGH);
|
|
delay(100);
|
|
gfx->begin();
|
|
display->Display_Brightness(255);
|
|
gfx->fillScreen(rgb(0, 0, 0));
|
|
gfx->flush();
|
|
Serial.println("ok:ready");
|
|
}
|
|
|
|
// === Loop ===
|
|
void loop() {
|
|
handleSerial();
|
|
unsigned long now = millis();
|
|
if (now - frameStart >= FRAME_MS) {
|
|
drawEye();
|
|
frameStart = now;
|
|
}
|
|
}
|