// Required libraries for I2C communication and OLED display graphics
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// === Custom I2C buses (Avoid conflict with ESP32's built-in Wire/Wire1) ===
// Create two separate I2C instances for two OLEDs (each eye has its own bus)
TwoWire i2cLeft(0); // I2C bus 0 → Left Eye
TwoWire i2cRight(1); // I2C bus 1 → Right Eye
// === OLED display resolution definitions ===
#define SCREEN_WIDTH 128 // Width of each OLED in pixels
#define SCREEN_HEIGHT 64 // Height of each OLED in pixels
#define OLED_RESET -1 // Reset pin not used (wired to ESP32 reset or handled internally)
// === OLED Display Object Definitions ===
// Initialize two SSD1306 display objects using the respective I2C buses
Adafruit_SSD1306 displayLeft(SCREEN_WIDTH, SCREEN_HEIGHT, &i2cLeft, OLED_RESET);
Adafruit_SSD1306 displayRight(SCREEN_WIDTH, SCREEN_HEIGHT, &i2cRight, OLED_RESET);
// === Shared Eye Properties (for both eyes) ===
int eyeY = 12; // Vertical position of the eye on the screen
int eyeWidth = 40; // Width of the eye rectangle
int eyeHeight = 40; // Height of the eye rectangle
// === Variables for LEFT eye ===
int eyeX_L = 44; // Horizontal starting position
int targetOffsetX_L = 0; // Target offset in X (used for eye movement)
int targetOffsetY_L = 0; // Target offset in Y
int offsetX_L = 0; // Current offset X
int offsetY_L = 0; // Current offset Y
int moveSpeed_L = 3; // Speed at which the eye moves toward target
int blinkState_L = 0; // Blink status: 0 = open, 1 = closed
int blinkDelay_L = 4000; // Time between blinks (in ms)
unsigned long lastBlinkTime_L = 0; // Timestamp of last blink
unsigned long moveTime_L = 0; // Timestamp of last eye direction change
// === Variables for RIGHT eye ===
int eyeX_R = 44;
int targetOffsetX_R = 0;
int targetOffsetY_R = 0;
int offsetX_R = 0;
int offsetY_R = 0;
int moveSpeed_R = 3;
int blinkState_R = 0;
int blinkDelay_R = 4000;
unsigned long lastBlinkTime_R = 0;
unsigned long moveTime_R = 0;
void setup() {
// === Initialize I2C buses for both displays ===
i2cLeft.begin(8, 9); // Left Eye → SDA pin 8, SCL pin 9
i2cRight.begin(17, 18); // Right Eye → SDA pin 17, SCL pin 18
// === Initialize both OLED displays ===
displayLeft.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 0x3C is the I2C address
displayRight.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Both displays can share same address if on diff buses
// Display buffer content (startup garbage/boot logo)
displayLeft.display();
displayRight.display();
delay(1000); // Wait a moment for the displays to stabilize
// Clear both displays before starting animation
displayLeft.clearDisplay();
displayRight.clearDisplay();
}
void loop() {
unsigned long currentTime = millis(); // Get current time in milliseconds
// === LEFT EYE PROCESSING ===
handleBlink(currentTime, blinkState_L, lastBlinkTime_L, blinkDelay_L); // Blink logic
handleMovement(currentTime, moveTime_L, targetOffsetX_L, targetOffsetY_L, blinkState_L); // New target dir
smoothMove(offsetX_L, offsetY_L, targetOffsetX_L, targetOffsetY_L, moveSpeed_L); // Smooth movement
drawEyeDisplay(displayLeft, eyeX_L + offsetX_L, eyeY + offsetY_L, eyeWidth, eyeHeight, blinkState_L); // Draw eye
// === RIGHT EYE PROCESSING ===
handleBlink(currentTime, blinkState_R, lastBlinkTime_R, blinkDelay_R);
handleMovement(currentTime, moveTime_R, targetOffsetX_R, targetOffsetY_R, blinkState_R);
smoothMove(offsetX_R, offsetY_R, targetOffsetX_R, targetOffsetY_R, moveSpeed_R);
drawEyeDisplay(displayRight, eyeX_R + offsetX_R, eyeY + offsetY_R, eyeWidth, eyeHeight, blinkState_R);
delay(30); // Short delay to keep animation smooth
}
// === Handle blinking logic ===
void handleBlink(unsigned long currentTime, int &blinkState, unsigned long &lastBlink, int blinkDelay) {
if (currentTime - lastBlink > blinkDelay && blinkState == 0) {
blinkState = 1; // Start blinking
lastBlink = currentTime;
}
if (currentTime - lastBlink > 150 && blinkState == 1) {
blinkState = 0; // End blinking
lastBlink = currentTime;
}
}
// === Handle eye movement direction logic ===
void handleMovement(unsigned long currentTime, unsigned long &moveTime,
int &targetX, int &targetY, int blinkState) {
if (currentTime - moveTime > random(1500, 3000) && blinkState == 0) {
int dir = random(0, 6); // Random direction from 0 to 5
if (dir == 0) { targetX = -8; targetY = 0; } // Left
else if (dir == 1) { targetX = 8; targetY = 0; } // Right
else if (dir == 2) { targetX = 0; targetY = -6; } // Up
else if (dir == 3) { targetX = 0; targetY = 6; } // Down
else { targetX = 0; targetY = 0; } // Center
moveTime = currentTime; // Reset move time
}
}
// === Smoothly interpolate current offset toward target offset ===
void smoothMove(int &offsetX, int &offsetY, int targetX, int targetY, int speed) {
offsetX += (targetX - offsetX) / speed; // Move X toward target
offsetY += (targetY - offsetY) / speed; // Move Y toward target
}
// === Draw eye shape on given display ===
void drawEyeDisplay(Adafruit_SSD1306 &display, int x, int y, int w, int h, int blinkState) {
display.clearDisplay(); // Clear current display buffer
if (blinkState == 0) {
// Eye open → draw rounded rectangle
display.fillRoundRect(x, y, w, h, 8, WHITE);
} else {
// Eye closed → draw narrow horizontal bar (eyelid)
display.fillRect(x, y + h / 2 - 2, w, 4, WHITE);
}
display.display(); // Push buffer to the screen
}