IRIS · Interactive Robotic Intelligence System

IRIS - Interactive Robotic Intelligence System | ESP32 Desk Robot

๐Ÿค– IRIS · Interactive Robotic Intelligence System

⚡ ESP32-S3 | 4WD | Emotional OLED Eyes | Motion Sensing | Angry when messy!

✨ What is IRIS?

IRIS (Interactive Robotic Intelligence System) is a tiny desk robot that roams, reacts to motion, and shows real emotions on its OLED face. When your desk gets messy — shake it! IRIS gets dizzy and then ANGRY, moving backward in protest. Touch it gently to see its charming water eyes. It also shows time and live weather.

⚙️ How It Works

• ๐Ÿง  XIAO ESP32-S3 controls 4 N20 motors via dual DRV8833 drivers.
• ๐ŸŽญ 1.3" OLED (SH1106) displays animated eyes with 11+ emotions.
• ๐Ÿ”„ MPU6050 gyro detects tilt and shake — very sensitive response.
• ๐Ÿ’ง Touch sensor (TTP223) triggers charming "water eyes" mode.
• ๐Ÿ”‹ 3.7V LiPo + MT3608 boost → 5V stable for ESP32 & motors.
• ๐ŸŒ WiFi + OpenWeather fetches live weather & NTP time.
• ๐Ÿคฌ Shake → Dizzy → Angry (moves backward). Edge detection makes it scared too!

๐Ÿ“ฆ Components Required

๐Ÿ–จ️ 3D Print Files + Material

๐Ÿ”Œ Pin Connections (XIAO ESP32-S3)

ComponentXIAO PinNotes
OLED SDA / MPU6050 SDAD4I2C shared
OLED SCL / MPU6050 SCLD5I2C shared
TTP223 Touch SensorD6INPUT_PULLUP
Left Front Motor (IN1,IN2)D0, D1DRV8833 #1
Left Back Motor (IN1,IN2)D2, D3DRV8833 #1
Right Front Motor (IN1,IN2)D7, D8DRV8833 #2
Right Back Motor (IN1,IN2)D9, D10DRV8833 #2
MT3608 Boost (5V out)5V / GNDpowers ESP32 & motors
⚡ Set MT3608 to 5.0V BEFORE connecting. Use slide switch between battery + and TP4056.

๐ŸŽญ Emotions & Movement

๐Ÿ˜ก Angry (shake → angry backward)
๐Ÿ’ง Charming Water Eyes (touch me!)
๐ŸŒ€ Dizzy + Stars (shake detected)
๐Ÿ˜ด Sleepy (idle → Zzz)
๐Ÿšถ 8 movement patterns (forward, back, rotate, wiggle, crab walk, circle)
๐ŸŒก️ Real‑time weather + clock

๐Ÿ’ป Arduino Firmware (Both Versions)

Complete Code (Beta / Charming + 8 Moves)
Original Code (Random Moves + Sleep)
IRIS_Complete_Charming.ino
// ==================================================
// IRIS - COMPLETE WITH CHARMING FACE + ALL 4 MOTORS
// Features: 
//   - Charming water eyes mood (accessible via touch/mood change)
//   - All 4 motors work in every movement
//   - 8 different movement patterns
//   - Shake → Dizzy → Angry
// ==================================================

#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include "time.h"
#include <math.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <MPU6050_tockn.h>

MPU6050 mpu6050(Wire);

// ==================================================
// WIFI CREDENTIALS
// ==================================================
const char* WIFI_SSID = "Galaxy";
const char* WIFI_PASSWORD = "rdj02480";
const char* OPENWEATHER_API_KEY = "d9c460c0d5b726695a5553885ac10025";
const char* CITY = "Jaipur";
const char* COUNTRY_CODE = "IN";
const char* TIMEZONE = "IST-5:30";

// ==================================================
// HARDWARE CONFIGURATION
// ==================================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SDA_PIN D4
#define SCL_PIN D5
#define TOUCH_PIN D6

Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// ==================================================
// DRV8833 MOTOR PINS - ALL 4 MOTORS
// ==================================================
#define LEFT_FRONT_IN1   D0
#define LEFT_FRONT_IN2   D1
#define LEFT_BACK_IN1    D2
#define LEFT_BACK_IN2    D3
#define RIGHT_FRONT_IN1  D7
#define RIGHT_FRONT_IN2  D8
#define RIGHT_BACK_IN1   D9
#define RIGHT_BACK_IN2   D10

// ==================================================
// MPU6050 MOTION VARIABLES
// ==================================================
#define CALIBRATION_SAMPLES 100
float calibAngleX = 0, calibAngleY = 0, calibAngleZ = 0;
bool isCalibrated = false;
float currentRoll = 0, currentPitch = 0;

// SHAKE DETECTION
float shakeIntensity = 0;
float shakeAccumulator = 0;
bool isShaking = false;
unsigned long shakeStartTime = 0;
unsigned long shakeEndTime = 0;
const int SHAKE_DURATION = 1500;

bool isAngry = false;
unsigned long angryEndTime = 0;
const int ANGRY_DURATION = 2500;

bool isScared = false;
unsigned long scaredEndTime = 0;
const int SCARED_DURATION = 1500;
float tiltThreshold = 12.0;
bool wasAtEdge = false;

bool isSleeping = false;
unsigned long sleepStartTime = 0;
const unsigned long SLEEP_INTERVAL = 60000;
const unsigned long WAKE_DURATION = 8000;

float targetPupilX = 0, targetPupilY = 0;
float currentPupilX = 0, currentPupilY = 0;

float lastAccelX = 0, lastAccelY = 0, lastAccelZ = 0;

// MOVEMENT VARIABLES
unsigned long lastMoveChange = 0;
unsigned long currentMoveDuration = 3000;
int currentMoveType = 0;
bool isMoving = false;

// ==================================================
// WEATHER ICONS
// ==================================================
const unsigned char bmp_clear[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0xc0, 0x80, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0xff, 0xff, 0x00, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x00, 0xff, 0xff, 0x00, 0x3e, 0xff, 0xff, 0x7c, 0x3e, 0xff, 0xff, 0x7c, 0x3e, 0xff, 0xff, 0x7c, 0x00, 0xff, 0xff, 0x00, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x00, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x01, 0x0f, 0xf0, 0x80, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_clouds[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x3f, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x7f, 0xff, 0x80, 0x00, 0xff, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_rain[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x3f, 0xfe, 0x00, 0x00, 0x7f, 0xff, 0x80, 0x00, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x00, 0xe0, 0x1c, 0x00, 0x00, 0xc0, 0x18, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x00, 0x60, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_tiny_drop[] PROGMEM = { 0x10, 0x38, 0x7c, 0xfe, 0xfe, 0x7c, 0x38, 0x00 };
const unsigned char bmp_dizzy_stars[] PROGMEM = { 0x08, 0x20, 0x14, 0x50, 0x22, 0x88, 0x41, 0x04, 0x82, 0x02, 0x41, 0x04, 0x22, 0x88, 0x14, 0x50, 0x08, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_angry_mark[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x00, 0x3e, 0x00, 0x7f, 0x00, 0x3e, 0x00, 0x1c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_heart[] PROGMEM = { 0x00, 0x00, 0x0c, 0x60, 0x1e, 0xf0, 0x3f, 0xf8, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x3f, 0xf8, 0x1f, 0xf0, 0x0f, 0xe0, 0x07, 0xc0, 0x03, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_zzz[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x30, 0x00, 0x7e, 0x00, 0x00, 0x3c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x30, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00 };

// ==================================================
// GLOBALS
// ==================================================
int currentPage = 0;
int subPage = 0;
int tapCounter = 0;
unsigned long lastTapTime = 0;
bool lastPinState = false;
unsigned long pressStartTime = 0;
bool isLongPressHandled = false;
const unsigned long LONG_PRESS_TIME = 4000;
const unsigned long DOUBLE_TAP_DELAY = 300;

// MOODS - CHARMING added as MOOD_CHARMING (index 10)
#define MOOD_NORMAL 0
#define MOOD_HAPPY 1
#define MOOD_SURPRISED 2
#define MOOD_SLEEPY 3
#define MOOD_ANGRY 4
#define MOOD_SAD 5
#define MOOD_EXCITED 6
#define MOOD_LOVE 7
#define MOOD_SUSPICIOUS 8
#define MOOD_DIZZY 9
#define MOOD_CHARMING 10   // NEW! Charming water eyes mood

int currentMood = MOOD_HAPPY;
int weatherMood = MOOD_HAPPY;

float temperature = 0.0;
float feelsLike = 0.0;
int humidity = 0;
String weatherMain = "Clear";
String weatherDesc = "Sunny";
unsigned long lastWeatherUpdate = 0;

// ==================================================
// EYE PHYSICS ENGINE
// ==================================================
struct Eye {
  float x, y;
  float w, h;
  float targetX, targetY, targetW, targetH;
  float pupilX, pupilY;
  float targetPupilX, targetPupilY;
  float velX, velY, velW, velH;
  float pVelX, pVelY;
  float k = 0.15;
  float d = 0.65;
  float pk = 0.2;
  float pd = 0.6;
  bool blinking;
  unsigned long lastBlink;
  unsigned long nextBlinkTime;

  void init(float _x, float _y, float _w, float _h) {
    x = targetX = _x;
    y = targetY = _y;
    w = targetW = _w;
    h = targetH = _h;
    pupilX = targetPupilX = 0;
    pupilY = targetPupilY = 0;
    nextBlinkTime = millis() + random(1000, 4000);
  }

  void update() {
    float ax = (targetX - x) * k;
    float ay = (targetY - y) * k;
    float aw = (targetW - w) * k;
    float ah = (targetH - h) * k;
    velX = (velX + ax) * d;
    velY = (velY + ay) * d;
    velW = (velW + aw) * d;
    velH = (velH + ah) * d;
    x += velX;
    y += velY;
    w += velW;
    h += velH;
    
    float pax = (targetPupilX - pupilX) * pk;
    float pay = (targetPupilY - pupilY) * pk;
    pVelX = (pVelX + pax) * pd;
    pVelY = (pVelY + pay) * pd;
    pupilX += pVelX;
    pupilY += pVelY;
  }
};

Eye leftEye, rightEye;
float breathVal = 0;
int waterDropOffset = 0;

// ==================================================
// MOTOR CONTROL - ALL 4 MOTORS WORK IN EVERY MOVEMENT
// ==================================================
void setMotor(int in1, int in2, int direction) {
  if (direction == 1) { digitalWrite(in1, HIGH); digitalWrite(in2, LOW); }
  else if (direction == -1) { digitalWrite(in1, LOW); digitalWrite(in2, HIGH); }
  else { digitalWrite(in1, LOW); digitalWrite(in2, LOW); }
}

// Movement 1: Forward - ALL 4 MOTORS
void moveForward() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("๐Ÿšถ FORWARD (3 sec)");
}

// Movement 2: Backward - ALL 4 MOTORS
void moveBackward() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, -1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, -1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, -1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, -1);
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("๐Ÿ”™ BACKWARD (3 sec)");
}

// Movement 3: Rotate Left - ALL 4 MOTORS
void rotateLeft() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, -1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, -1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("⬅️ ROTATE LEFT (3 sec)");
}

// Movement 4: Rotate Right - ALL 4 MOTORS
void rotateRight() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, -1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, -1);
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("➡️ ROTATE RIGHT (3 sec)");
}

// Movement 5: Circle Left - ALL 4 MOTORS
void circleLeft() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 0);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 0);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("๐Ÿ”˜ CIRCLE LEFT (3 sec)");
}

// Movement 6: Circle Right - ALL 4 MOTORS
void circleRight() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 0);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 0);
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("๐Ÿ”˜ CIRCLE RIGHT (3 sec)");
}

// Movement 7: Wiggle - ALL 4 MOTORS
void wiggle() {
  static int wiggleStep = 0;
  if (wiggleStep % 2 == 0) {
    setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, -1);
    setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, -1);
    setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
    setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  } else {
    setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
    setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
    setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, -1);
    setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, -1);
  }
  wiggleStep++;
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("๐Ÿ› WIGGLE (3 sec)");
}

// Movement 8: Crab Walk (sideways motion) - ALL 4 MOTORS
void crabWalk() {
  static int crabStep = 0;
  if (crabStep % 2 == 0) {
    setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
    setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, -1);
    setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, -1);
    setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  } else {
    setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, -1);
    setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
    setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
    setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, -1);
  }
  crabStep++;
  isMoving = true;
  lastMoveChange = millis();
  currentMoveDuration = 3000;
  Serial.println("๐Ÿฆ€ CRAB WALK (3 sec)");
}

void stopAllMotors() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 0);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 0);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 0);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 0);
  isMoving = false;
  Serial.println("๐Ÿ›‘ STOP");
}

// Array of all 8 movement functions
typedef void (*MoveFunction)();
MoveFunction movePatterns[] = { moveForward, moveBackward, rotateLeft, rotateRight, circleLeft, circleRight, wiggle, crabWalk };
const int NUM_PATTERNS = 8;

void randomMove() {
  int moveType = random(NUM_PATTERNS);
  movePatterns[moveType]();
}

void exploreBehavior() {
  if (!isMoving && !isAngry && !isScared && !isShaking && !isSleeping && currentPage == 0) {
    randomMove();
    return;
  }
  
  if (isMoving && (millis() - lastMoveChange >= currentMoveDuration) && !isAngry && !isScared && !isShaking && !isSleeping) {
    randomMove();
  }
}

// ==================================================
// MPU6050 FUNCTIONS
// ==================================================
void calibrateMPU6050() {
  display.clearDisplay();
  display.setFont(NULL);
  display.setCursor(20, 20);
  display.print("Calibrating...");
  display.setCursor(10, 35);
  display.print("Keep device STILL");
  display.display();
  
  float sumX = 0, sumY = 0, sumZ = 0;
  for(int i = 0; i < CALIBRATION_SAMPLES; i++) {
    mpu6050.update();
    sumX += mpu6050.getAngleX();
    sumY += mpu6050.getAngleY();
    sumZ += mpu6050.getAngleZ();
    delay(10);
  }
  
  calibAngleX = sumX / CALIBRATION_SAMPLES;
  calibAngleY = sumY / CALIBRATION_SAMPLES;
  calibAngleZ = sumZ / CALIBRATION_SAMPLES;
  isCalibrated = true;
  
  display.clearDisplay();
  display.setCursor(25, 30);
  display.print("Calibrated!");
  display.display();
  delay(1000);
}

void updateMotion() {
  if (!isCalibrated) return;
  
  mpu6050.update();
  
  currentRoll = mpu6050.getAngleX() - calibAngleX;
  currentPitch = mpu6050.getAngleY() - calibAngleY;
  
  float accelX = mpu6050.getAccX();
  float accelY = mpu6050.getAccY();
  float accelZ = mpu6050.getAccZ();
  
  float deltaX = fabs(accelX - lastAccelX);
  float deltaY = fabs(accelY - lastAccelY);
  float deltaZ = fabs(accelZ - lastAccelZ);
  
  float instantShake = (deltaX + deltaY + deltaZ) * 12;
  shakeAccumulator = shakeAccumulator * 0.7 + instantShake * 0.3;
  shakeIntensity = shakeAccumulator;
  
  // SHAKE DETECTION
  if (shakeIntensity > 10.0 && !isShaking && !isAngry && !isScared && currentPage == 0) {
    isShaking = true;
    shakeStartTime = millis();
    shakeEndTime = millis() + SHAKE_DURATION;
    currentMood = MOOD_DIZZY;
    stopAllMotors();
    Serial.print("๐ŸŒ€ DIZZY! Shake: ");
    Serial.println(shakeIntensity);
  }
  
  // DIZZY -> ANGRY
  if (isShaking && millis() >= shakeEndTime) {
    isShaking = false;
    isAngry = true;
    angryEndTime = millis() + ANGRY_DURATION;
    currentMood = MOOD_ANGRY;
    moveBackward();
    Serial.println("๐Ÿ˜  ANGRY!");
  }
  
  // ANGRY ends
  if (isAngry && millis() >= angryEndTime) {
    isAngry = false;
    currentMood = weatherMood;
    stopAllMotors();
    Serial.println("๐Ÿ˜Š Calm down");
  }
  
  // Edge detection (scared)
  if (!isShaking && !isAngry && !isScared && currentPage == 0) {
    float tiltX = fabs(currentRoll);
    float tiltY = fabs(currentPitch);
    
    if ((tiltX > tiltThreshold || tiltY > tiltThreshold) && !wasAtEdge) {
      wasAtEdge = true;
      isScared = true;
      scaredEndTime = millis() + SCARED_DURATION;
      currentMood = MOOD_SURPRISED;
      stopAllMotors();
      moveBackward();
      Serial.println("๐Ÿ˜จ SCARED! Edge!");
    } else if (tiltX < tiltThreshold && tiltY < tiltThreshold) {
      wasAtEdge = false;
    }
  }
  
  // Scared recovery
  if (isScared && millis() >= scaredEndTime) {
    isScared = false;
    currentMood = weatherMood;
    stopAllMotors();
    delay(300);
    int turn = random(2);
    if (turn == 0) rotateLeft();
    else rotateRight();
    delay(500);
    stopAllMotors();
    randomMove();
  }
  
  // Eye tracking
  if (!isShaking && !isAngry && !isScared && !isSleeping && currentPage == 0 && currentMood != MOOD_CHARMING) {
    float rawX = currentRoll / 5.0;
    float rawY = currentPitch / 5.0;
    
    targetPupilX = constrain(rawX, -12, 12);
    targetPupilY = constrain(rawY, -10, 10);
    
    currentPupilX = currentPupilX * 0.6 + targetPupilX * 0.4;
    currentPupilY = currentPupilY * 0.6 + targetPupilY * 0.4;
    
    leftEye.targetPupilX = currentPupilX;
    leftEye.targetPupilY = currentPupilY;
    rightEye.targetPupilX = currentPupilX;
    rightEye.targetPupilY = currentPupilY;
    
    leftEye.targetX = 28 + (currentRoll / 15);
    leftEye.targetY = 14 + (currentPitch / 15);
    rightEye.targetX = 80 + (currentRoll / 15);
    rightEye.targetY = 14 + (currentPitch / 15);
  }
  
  lastAccelX = accelX;
  lastAccelY = accelY;
  lastAccelZ = accelZ;
}

// ==================================================
// SLEEP MODE
// ==================================================
void handleSleepMode() {
  if (!isSleeping && !isMoving && !isScared && !isShaking && !isAngry && currentPage == 0) {
    if (millis() - lastMoveChange > SLEEP_INTERVAL) {
      isSleeping = true;
      sleepStartTime = millis();
      currentMood = MOOD_SLEEPY;
      stopAllMotors();
      Serial.println("๐Ÿ’ค Sleeping...");
    }
  }
  
  if (isSleeping && (millis() - sleepStartTime > WAKE_DURATION)) {
    isSleeping = false;
    currentMood = weatherMood;
    randomMove();
    Serial.println("✨ Waking up!");
  }
}

// ==================================================
// WEATHER FUNCTIONS
// ==================================================
const unsigned char* getBigIcon(String w) {
  if (w == "Clear") return bmp_clear;
  if (w == "Clouds") return bmp_clouds;
  if (w == "Rain" || w == "Drizzle") return bmp_rain;
  return bmp_clouds;
}

void updateWeatherMood() {
  if (weatherMain == "Clear") weatherMood = MOOD_HAPPY;
  else if (weatherMain == "Rain" || weatherMain == "Drizzle") weatherMood = MOOD_SAD;
  else if (weatherMain == "Thunderstorm") weatherMood = MOOD_SURPRISED;
  else if (weatherMain == "Clouds") weatherMood = MOOD_NORMAL;
  else if (temperature > 35) weatherMood = MOOD_ANGRY;
  else if (temperature < 10) weatherMood = MOOD_SLEEPY;
  else weatherMood = MOOD_NORMAL;
  
  if (!isShaking && !isAngry && !isScared && !isSleeping && currentMood != MOOD_CHARMING) {
    currentMood = weatherMood;
  }
}

void getWeatherAndForecast() {
  if (WiFi.status() != WL_CONNECTED) return;
  
  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/weather?q=" + String(CITY) + "," + String(COUNTRY_CODE) + "&appid=" + String(OPENWEATHER_API_KEY) + "&units=metric";
  http.begin(url);
  int httpCode = http.GET();
  if (httpCode == 200) {
    String payload = http.getString();
    JSONVar myObject = JSON.parse(payload);
    if (JSON.typeof(myObject) != "undefined") {
      temperature = double(myObject["main"]["temp"]);
      feelsLike = double(myObject["main"]["feels_like"]);
      humidity = int(myObject["main"]["humidity"]);
      weatherMain = (const char*)myObject["weather"][0]["main"];
      weatherDesc = (const char*)myObject["weather"][0]["description"];
      if (weatherDesc.length() > 0) weatherDesc[0] = toupper(weatherDesc[0]);
      updateWeatherMood();
    }
  }
  http.end();
}

// ==================================================
// CHARMING WATER EYES DRAW FUNCTION
// ==================================================
void drawCharmingEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;
  
  // Bright sparkling white eyes
  display.fillRoundRect(ix, iy, iw, ih, 8, SH110X_WHITE);
  display.drawRoundRect(ix, iy, iw, ih, 8, SH110X_BLACK);
  
  // Large dilated pupils (charming look)
  int cx = ix + iw/2;
  int cy = iy + ih/2;
  int pupilSize = iw / 1.6;
  
  int px = cx + (int)e.pupilX - pupilSize/2;
  int py = cy + (int)e.pupilY - pupilSize/2;
  
  px = constrain(px, ix + 2, ix + iw - pupilSize - 2);
  py = constrain(py, iy + 2, iy + ih - pupilSize - 2);
  
  display.fillRoundRect(px, py, pupilSize, pupilSize, 4, SH110X_BLACK);
  
  // Double catchlights (sparkle)
  display.fillCircle(px + pupilSize - 4, py + 4, 3, SH110X_WHITE);
  display.fillCircle(px + pupilSize - 8, py + 8, 2, SH110X_WHITE);
  display.fillCircle(px + 4, py + 4, 2, SH110X_WHITE);
  
  // Happy arch
  display.fillRect(ix, iy + ih - 12, iw, 14, SH110X_BLACK);
  
  // Animated water drops (tears of joy)
  waterDropOffset = (millis() / 150) % 20;
  
  if (isLeft) {
    display.fillCircle(ix - 4 - (waterDropOffset / 8), iy + ih/2 + sin(millis()/200.0)*2, 2, SH110X_WHITE);
    display.fillCircle(ix - 2 - (waterDropOffset / 5), iy + ih/2 + 4, 1, SH110X_WHITE);
  } else {
    display.fillCircle(ix + iw + 4 + (waterDropOffset / 8), iy + ih/2 + sin(millis()/200.0)*2, 2, SH110X_WHITE);
    display.fillCircle(ix + iw + 2 + (waterDropOffset / 5), iy + ih/2 + 4, 1, SH110X_WHITE);
  }
  
  // Sparkle stars
  unsigned long now = millis();
  int sparkle = (now / 100) % 4;
  if (sparkle == 0 && isLeft) {
    display.fillCircle(ix + 4, iy - 3, 1, SH110X_WHITE);
    display.fillCircle(ix + iw - 4, iy - 3, 1, SH110X_WHITE);
  }
}

void drawCharmingMouth() {
  int mx = 64;
  int my = 54;
  
  // Big happy smile
  for(int i = -10; i <= 10; i++) {
    int y = my - (i*i / 14) - 2;
    if(y < my + 2) display.drawPixel(mx + i, y, SH110X_WHITE);
  }
  
  // Blush cheeks
  display.fillCircle(mx - 15, my + 2, 3, SH110X_WHITE);
  display.fillCircle(mx + 15, my + 2, 3, SH110X_WHITE);
  
  // Mouth sparkle
  unsigned long now = millis();
  if ((now / 200) % 2 == 0) {
    display.fillCircle(mx - 5, my - 4, 1, SH110X_WHITE);
    display.fillCircle(mx + 5, my - 4, 1, SH110X_WHITE);
  }
}

// ==================================================
// TOUCH HANDLER
// ==================================================
void handleTouch() {
  bool currentPinState = digitalRead(TOUCH_PIN);
  unsigned long now = millis();
  
  if (currentPinState && !lastPinState) {
    pressStartTime = now;
    isLongPressHandled = false;
    if (isSleeping) {
      isSleeping = false;
      currentMood = MOOD_CHARMING;  // Wake up with charming face!
      randomMove();
    } else if (!isShaking && !isAngry && !isScared && currentPage == 0) {
      // Touch = Charming face!
      currentMood = MOOD_CHARMING;
      // Happy wiggle
      for(int i = 0; i < 2; i++) {
        rotateLeft();
        delay(150);
        rotateRight();
        delay(150);
      }
      stopAllMotors();
      Serial.println("๐Ÿ’ง CHARMING MODE - Water eyes!");
    }
  } 
  else if (currentPinState && lastPinState) {
    if ((now - pressStartTime > LONG_PRESS_TIME) && !isLongPressHandled) {
      currentPage++;
      if (currentPage > 2) currentPage = 0;
      if (currentPage != 0) stopAllMotors();
      else if (!isScared && !isSleeping && !isShaking && !isAngry) randomMove();
      isLongPressHandled = true;
      Serial.print("Page: ");
      Serial.println(currentPage);
    }
  } 
  else if (!currentPinState && lastPinState) {
    if ((now - pressStartTime < LONG_PRESS_TIME) && !isLongPressHandled) {
      tapCounter++;
      lastTapTime = now;
    }
  }
  lastPinState = currentPinState;
  
  if (tapCounter > 0 && (now - lastTapTime > DOUBLE_TAP_DELAY)) {
    if (tapCounter == 1 && currentPage == 0 && !isShaking && !isAngry && !isScared && !isSleeping) {
      currentMood++;
      if (currentMood > MOOD_CHARMING) currentMood = 0;
      weatherMood = currentMood;
      Serial.print("Mood: ");
      Serial.println(currentMood);
    }
    tapCounter = 0;
  }
}

// ==================================================
// DRAW FUNCTIONS
// ==================================================
void drawDizzyEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;
  int cx = ix + iw/2;
  int cy = iy + ih/2;
  
  display.fillCircle(cx, cy, iw/2, SH110X_WHITE);
  
  unsigned long now = millis();
  float angle = (now - shakeStartTime) * 0.025;
  int radius = iw / 3;
  
  int pupilX = cx + cos(angle) * radius;
  int pupilY = cy + sin(angle) * radius;
  
  display.fillCircle(pupilX, pupilY, 4, SH110X_BLACK);
  display.fillCircle(pupilX + 2, pupilY - 2, 1, SH110X_WHITE);
  
  int pupilX2 = cx - cos(angle) * radius;
  int pupilY2 = cy - sin(angle) * radius;
  display.fillCircle(pupilX2, pupilY2, 3, SH110X_BLACK);
  
  int starOffset = (now / 80) % 4;
  display.drawBitmap(6 - starOffset, 0, bmp_dizzy_stars, 16, 16, SH110X_WHITE);
  display.drawBitmap(106 + starOffset, 0, bmp_dizzy_stars, 16, 16, SH110X_WHITE);
}

void drawAngryEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;
  
  display.fillRoundRect(ix, iy, iw, ih, 8, SH110X_WHITE);
  
  int cx = ix + iw/2;
  int cy = iy + ih/2;
  int pupilSize = iw / 2.5;
  
  int px = cx - 2;
  int py = cy;
  
  display.fillRoundRect(px - pupilSize/2, py - pupilSize/2, pupilSize, pupilSize, pupilSize/2, SH110X_BLACK);
  
  if (isLeft) {
    for(int i = 0; i < 8; i++) {
      display.drawLine(ix - 2, iy - 2 + i, ix + iw + 2, iy + 6 + i, SH110X_BLACK);
    }
    display.drawBitmap(ix - 12, iy - 6, bmp_angry_mark, 16, 16, SH110X_WHITE);
  } else {
    for(int i = 0; i < 8; i++) {
      display.drawLine(ix - 2, iy + 6 + i, ix + iw + 2, iy - 2 + i, SH110X_BLACK);
    }
    display.drawBitmap(ix + iw - 4, iy - 6, bmp_angry_mark, 16, 16, SH110X_WHITE);
  }
}

void drawNormalEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;

  int r = 8;
  if (iw < 20) r = 3;
  display.fillRoundRect(ix, iy, iw, ih, r, SH110X_WHITE);

  int cx = ix + iw / 2;
  int cy = iy + ih / 2;

  int pw = iw / 2.2;
  int ph = ih / 2.2;

  int px = cx + (int)e.pupilX - (pw / 2);
  int py = cy + (int)e.pupilY - (ph / 2);

  if (px < ix) px = ix;
  if (px + pw > ix + iw) px = ix + iw - pw;
  if (py < iy) py = iy;
  if (py + ph > iy + ih) py = iy + ih - ph;

  display.fillRoundRect(px, py, pw, ph, r / 2, SH110X_BLACK);

  if (iw > 15 && ih > 15) {
    display.fillCircle(px + pw - 4, py + 4, 2, SH110X_WHITE);
  }
  
  if (currentMood == MOOD_HAPPY || currentMood == MOOD_LOVE) {
    display.fillRect(ix, iy + ih - 10, iw, 12, SH110X_BLACK);
  }
  else if (currentMood == MOOD_SLEEPY || isSleeping) {
    display.fillRect(ix, iy, iw, ih/2, SH110X_BLACK);
  }
  else if (currentMood == MOOD_SAD) {
    if (isLeft) {
      for(int i = 0; i < 8; i++) display.drawLine(ix, iy + i, ix + iw, iy + 4 + i, SH110X_BLACK);
    } else {
      for(int i = 0; i < 8; i++) display.drawLine(ix, iy + 4 + i, ix + iw, iy + i, SH110X_BLACK);
    }
  }
}

void drawMouth() {
  int mx = 64;
  int my = 54;
  
  if (isScared) {
    display.fillCircle(mx, my + 2, 6, SH110X_BLACK);
    display.fillCircle(mx, my, 3, SH110X_WHITE);
    return;
  }
  
  if (currentMood == MOOD_DIZZY && isShaking) {
    unsigned long now = millis();
    for(int i = -8; i <= 8; i++) {
      int y = my + sin(i * 0.8 + now * 0.03) * 4;
      display.drawPixel(mx + i, y, SH110X_WHITE);
    }
    return;
  }
  
  if (currentMood == MOOD_ANGRY && isAngry) {
    display.fillRect(mx - 8, my - 2, 16, 5, SH110X_BLACK);
    for(int i = -5; i <= 5; i+=3) {
      display.drawLine(mx + i, my - 2, mx + i, my + 2, SH110X_WHITE);
    }
    return;
  }
  
  switch(currentMood) {
    case MOOD_HAPPY:
      for(int i = -6; i <= 6; i++) {
        int y = my - (i*i / 16);
        if(y < my + 2) display.drawPixel(mx + i, y, SH110X_WHITE);
      }
      break;
    case MOOD_SAD:
      for(int i = -6; i <= 6; i++) {
        int y = my + 2 + (i*i / 16);
        display.drawPixel(mx + i, y, SH110X_WHITE);
      }
      break;
    case MOOD_SURPRISED:
      display.fillCircle(mx, my + 1, 3, SH110X_BLACK);
      display.drawCircle(mx, my + 1, 3, SH110X_WHITE);
      break;
    case MOOD_SLEEPY:
      display.drawLine(mx - 4, my, mx + 4, my, SH110X_WHITE);
      break;
    case MOOD_LOVE:
      display.drawBitmap(mx - 6, my - 2, bmp_heart, 12, 12, SH110X_WHITE);
      break;
    default:
      display.drawLine(mx - 4, my, mx + 4, my, SH110X_WHITE);
      break;
  }
}

void updateEyePhysics() {
  unsigned long now = millis();
  breathVal = sin(now / 800.0) * 1.5;
  
  // Skip blinking during charming mode
  if (currentMood == MOOD_CHARMING) {
    leftEye.blinking = false;
    rightEye.blinking = false;
    leftEye.update();
    rightEye.update();
    return;
  }
  
  int blinkDelay = (isShaking || isScared || isAngry) ? 60 : 120;
  int blinkMin = (isShaking || isScared || isAngry) ? 300 : 2000;
  int blinkMax = (isShaking || isScared || isAngry) ? 800 : 6000;
  
  if (now > leftEye.nextBlinkTime) {
    leftEye.blinking = true;
    rightEye.blinking = true;
    leftEye.lastBlink = now;
    leftEye.nextBlinkTime = now + random(blinkMin, blinkMax);
  }
  
  if (leftEye.blinking) {
    leftEye.targetH = 2;
    rightEye.targetH = 2;
    if (now - leftEye.lastBlink > blinkDelay) {
      leftEye.blinking = false;
      rightEye.blinking = false;
      leftEye.targetH = 32;
      rightEye.targetH = 32;
    }
  }
  
  leftEye.update();
  rightEye.update();
}

void drawEmoPage() {
  updateEyePhysics();
  
  if (currentMood == MOOD_CHARMING) {
    drawCharmingEye(leftEye, true);
    drawCharmingEye(rightEye, false);
    drawCharmingMouth();
  }
  else if (currentMood == MOOD_DIZZY && isShaking) {
    drawDizzyEye(leftEye, true);
    drawDizzyEye(rightEye, false);
    drawMouth();
  }
  else if (currentMood == MOOD_ANGRY && isAngry) {
    drawAngryEye(leftEye, true);
    drawAngryEye(rightEye, false);
    drawMouth();
  }
  else {
    drawNormalEye(leftEye, true);
    drawNormalEye(rightEye, false);
    drawMouth();
  }
  
  if (!isShaking && !isAngry && !isScared && !isSleeping && currentMood != MOOD_CHARMING) {
    if (currentMood == MOOD_LOVE) {
      display.drawBitmap(56, 0, bmp_heart, 16, 16, SH110X_WHITE);
    } else if (currentMood == MOOD_SLEEPY || isSleeping) {
      display.drawBitmap(110, 0, bmp_zzz, 16, 16, SH110X_WHITE);
    }
  }
}

void drawClockPage() {
  struct tm t;
  if (!getLocalTime(&t)) {
    display.setFont(NULL);
    display.setCursor(30, 30);
    display.print("Syncing...");
    return;
  }
  
  String ampm = (t.tm_hour >= 12) ? "PM" : "AM";
  int h12 = t.tm_hour % 12;
  if (h12 == 0) h12 = 12;
  
  display.setTextColor(SH110X_WHITE);
  display.setFont(NULL);
  display.setCursor(114, 0);
  display.print(ampm);
  
  display.setFont(&FreeSansBold18pt7b);
  char timeStr[6];
  sprintf(timeStr, "%02d:%02d", h12, t.tm_min);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - w) / 2, 42);
  display.print(timeStr);
  
  display.setFont(&FreeSans9pt7b);
  char dateStr[20];
  strftime(dateStr, 20, "%a, %b %d", &t);
  display.getTextBounds(dateStr, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - w) / 2, 60);
  display.print(dateStr);
}

void drawWeatherPage() {
  if (WiFi.status() != WL_CONNECTED) {
    display.setFont(NULL);
    display.setCursor(0, 0);
    display.print("No WiFi");
    return;
  }
  
  display.drawBitmap(96, 0, getBigIcon(weatherMain), 32, 32, SH110X_WHITE);
  display.setFont(&FreeSansBold9pt7b);
  String c = CITY;
  c.toUpperCase();
  display.setCursor(0, 14);
  if (c.length() > 9) c = c.substring(0, 8) + ".";
  display.print(c);
  
  display.setFont(&FreeSansBold18pt7b);
  int tempInt = (int)temperature;
  display.setCursor(0, 48);
  display.print(tempInt);
  
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(String(tempInt).c_str(), 0, 48, &x1, &y1, &w, &h);
  display.fillCircle(x1 + w + 5, 26, 4, SH110X_WHITE);
  
  display.setFont(NULL);
  display.drawBitmap(88, 32, bmp_tiny_drop, 8, 8, SH110X_WHITE);
  display.setCursor(100, 32);
  display.print(humidity);
  display.print("%");
  display.setCursor(88, 45);
  display.print("~");
  display.print((int)feelsLike);
  display.drawLine(0, 52, 128, 52, SH110X_WHITE);
  display.setCursor(0, 55);
  String shortDesc = weatherDesc;
  if (shortDesc.length() > 15) shortDesc = shortDesc.substring(0, 13) + "..";
  display.print(shortDesc);
}

void playBootAnimation() {
  display.setTextColor(SH110X_WHITE);
  int cx = 64;
  int cy = 32;
  
  for (int r = 0; r < 80; r += 4) {
    display.clearDisplay();
    display.fillCircle(cx, cy, r, SH110X_WHITE);
    display.display();
    delay(8);
  }
  
  for (int r = 0; r < 80; r += 4) {
    display.clearDisplay();
    display.fillCircle(cx, cy, 80, SH110X_WHITE);
    display.fillCircle(cx, cy, r, SH110X_BLACK);
    display.display();
    delay(8);
  }
  
  display.setFont(&FreeSansBold9pt7b);
  String bootText = "IRIS";
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(bootText, 0, 0, &x1, &y1, &w, &h);
  display.clearDisplay();
  display.setCursor((SCREEN_WIDTH - w) / 2, 36);
  display.print(bootText);
  display.display();
  delay(2000);
}

// ==================================================
// SETUP
// ==================================================
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("\n╔════════════════════════════════╗");
  Serial.println("║       IRIS DESK BUDDY         ║");
  Serial.println("║  8 Movements (3 sec each)     ║");
  Serial.println("║  Touch me for Charming Face!  ║");
  Serial.println("╚════════════════════════════════╝\n");
  
  pinMode(LEFT_FRONT_IN1, OUTPUT);
  pinMode(LEFT_FRONT_IN2, OUTPUT);
  pinMode(LEFT_BACK_IN1, OUTPUT);
  pinMode(LEFT_BACK_IN2, OUTPUT);
  pinMode(RIGHT_FRONT_IN1, OUTPUT);
  pinMode(RIGHT_FRONT_IN2, OUTPUT);
  pinMode(RIGHT_BACK_IN1, OUTPUT);
  pinMode(RIGHT_BACK_IN2, OUTPUT);
  stopAllMotors();
  
  Wire.begin(SDA_PIN, SCL_PIN);
  pinMode(TOUCH_PIN, INPUT_PULLUP);
  
  display.begin(0x3C, true);
  display.setTextColor(SH110X_WHITE);
  display.clearDisplay();
  
  mpu6050.begin();
  calibrateMPU6050();
  
  leftEye.init(28, 14, 32, 32);
  rightEye.init(80, 14, 32, 32);
  
  playBootAnimation();
  
  display.clearDisplay();
  display.setFont(NULL);
  display.setCursor(20, 30);
  display.print("Connecting WiFi");
  display.display();
  
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  unsigned long wifiStart = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - wifiStart < 15000)) {
    delay(500);
    display.print(".");
    display.display();
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    display.clearDisplay();
    display.setCursor(25, 30);
    display.print("WiFi Connected!");
    display.display();
    delay(1000);
    
    configTime(0, 0, "pool.ntp.org");
    setenv("TZ", TIMEZONE, 1);
    tzset();
    getWeatherAndForecast();
    lastWeatherUpdate = millis();
  } else {
    display.clearDisplay();
    display.setCursor(15, 30);
    display.print("WiFi Failed!");
    display.display();
    delay(2000);
  }
  
  randomMove();
  Serial.println("IRIS READY! Touch me for charming water eyes! ๐Ÿ’ง");
}

// ==================================================
// LOOP
// ==================================================
void loop() {
  handleTouch();
  updateMotion();
  
  if (!isSleeping && currentPage == 0 && !isShaking && !isAngry && !isScared) {
    exploreBehavior();
  } else if (currentPage != 0) {
    stopAllMotors();
  }
  
  handleSleepMode();
  
  if (millis() - lastWeatherUpdate > 600000 && WiFi.status() == WL_CONNECTED) {
    getWeatherAndForecast();
    lastWeatherUpdate = millis();
  }
  
  display.clearDisplay();
  
  if (currentPage == 0) {
    drawEmoPage();
  } 
  else if (currentPage == 1) {
    drawClockPage();
  } 
  else if (currentPage == 2) {
    drawWeatherPage();
  }
  
  display.display();
  delay(20);
}
IRIS_Original_Random.ino
// ==================================================
// IRIS - PERFECT EYES + SENSITIVE SHAKE + RANDOM MOVEMENTS
// Uses your working eye code from the good version
// Added: Very sensitive shake detection
// Added: Shake → Dizzy → Angry (moves backward)
// Added: Completely random movement patterns
// ==================================================

#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include "time.h"
#include <math.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <MPU6050_tockn.h>

MPU6050 mpu6050(Wire);

// ==================================================
// WIFI CREDENTIALS
// ==================================================
const char* WIFI_SSID = "Galaxy";
const char* WIFI_PASSWORD = "rdj02480";
const char* OPENWEATHER_API_KEY = "d9c460c0d5b726695a5553885ac10025";
const char* CITY = "Jaipur";
const char* COUNTRY_CODE = "IN";
const char* TIMEZONE = "IST-5:30";

// ==================================================
// HARDWARE CONFIGURATION
// ==================================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SDA_PIN D4
#define SCL_PIN D5
#define TOUCH_PIN D6

Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// ==================================================
// DRV8833 MOTOR PINS
// ==================================================
#define LEFT_FRONT_IN1   D0
#define LEFT_FRONT_IN2   D1
#define LEFT_BACK_IN1    D2
#define LEFT_BACK_IN2    D3
#define RIGHT_FRONT_IN1  D7
#define RIGHT_FRONT_IN2  D8
#define RIGHT_BACK_IN1   D9
#define RIGHT_BACK_IN2   D10

// ==================================================
// MPU6050 MOTION VARIABLES
// ==================================================
#define CALIBRATION_SAMPLES 100
float calibAngleX = 0, calibAngleY = 0, calibAngleZ = 0;
bool isCalibrated = false;
float currentRoll = 0, currentPitch = 0;

// SHAKE DETECTION - EXTRA SENSITIVE
float shakeIntensity = 0;
float shakeAccumulator = 0;
bool isShaking = false;
unsigned long shakeStartTime = 0;
unsigned long shakeEndTime = 0;
const int SHAKE_DURATION = 1500;

// ANGRY AFTER SHAKE
bool isAngry = false;
unsigned long angryEndTime = 0;
const int ANGRY_DURATION = 2500;

bool isScared = false;
unsigned long scaredEndTime = 0;
const int SCARED_DURATION = 1500;
float tiltThreshold = 12.0;
bool wasAtEdge = false;

bool isSleeping = false;
unsigned long sleepStartTime = 0;
const unsigned long SLEEP_INTERVAL = 60000;
const unsigned long WAKE_DURATION = 8000;

float targetPupilX = 0, targetPupilY = 0;
float currentPupilX = 0, currentPupilY = 0;

float lastAccelX = 0, lastAccelY = 0, lastAccelZ = 0;

// RANDOM MOVEMENT VARIABLES
unsigned long lastMoveChange = 0;
unsigned long currentMoveDuration = 0;
int currentDirection = 0;
bool isMoving = false;

// ==================================================
// WEATHER ICONS
// ==================================================
const unsigned char bmp_clear[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0xc0, 0x80, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0xff, 0xff, 0x00, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x00, 0xff, 0xff, 0x00, 0x3e, 0xff, 0xff, 0x7c, 0x3e, 0xff, 0xff, 0x7c, 0x3e, 0xff, 0xff, 0x7c, 0x00, 0xff, 0xff, 0x00, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x06, 0xff, 0xff, 0x60, 0x00, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x01, 0x0f, 0xf0, 0x80, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_clouds[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x3f, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x7f, 0xff, 0x80, 0x00, 0xff, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_rain[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x3f, 0xfe, 0x00, 0x00, 0x7f, 0xff, 0x80, 0x00, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x00, 0xe0, 0x1c, 0x00, 0x00, 0xc0, 0x18, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x80, 0x70, 0x00, 0x03, 0x00, 0x60, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_tiny_drop[] PROGMEM = { 0x10, 0x38, 0x7c, 0xfe, 0xfe, 0x7c, 0x38, 0x00 };
const unsigned char bmp_dizzy_stars[] PROGMEM = { 0x08, 0x20, 0x14, 0x50, 0x22, 0x88, 0x41, 0x04, 0x82, 0x02, 0x41, 0x04, 0x22, 0x88, 0x14, 0x50, 0x08, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_angry_mark[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x00, 0x3e, 0x00, 0x7f, 0x00, 0x3e, 0x00, 0x1c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_heart[] PROGMEM = { 0x00, 0x00, 0x0c, 0x60, 0x1e, 0xf0, 0x3f, 0xf8, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x3f, 0xf8, 0x1f, 0xf0, 0x0f, 0xe0, 0x07, 0xc0, 0x03, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
const unsigned char bmp_zzz[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x30, 0x00, 0x7e, 0x00, 0x00, 0x3c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x30, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00 };

// ==================================================
// GLOBALS
// ==================================================
int currentPage = 0;
int subPage = 0;
int tapCounter = 0;
unsigned long lastTapTime = 0;
bool lastPinState = false;
unsigned long pressStartTime = 0;
bool isLongPressHandled = false;
const unsigned long LONG_PRESS_TIME = 4000;
const unsigned long DOUBLE_TAP_DELAY = 300;

// MOODS
#define MOOD_NORMAL 0
#define MOOD_HAPPY 1
#define MOOD_SURPRISED 2
#define MOOD_SLEEPY 3
#define MOOD_ANGRY 4
#define MOOD_SAD 5
#define MOOD_EXCITED 6
#define MOOD_LOVE 7
#define MOOD_SUSPICIOUS 8
#define MOOD_DIZZY 9
int currentMood = MOOD_HAPPY;
int weatherMood = MOOD_HAPPY;

float temperature = 0.0;
float feelsLike = 0.0;
int humidity = 0;
String weatherMain = "Clear";
String weatherDesc = "Sunny";
unsigned long lastWeatherUpdate = 0;

// ==================================================
// EYE PHYSICS ENGINE (FROM YOUR WORKING CODE)
// ==================================================
struct Eye {
  float x, y;
  float w, h;
  float targetX, targetY, targetW, targetH;
  float pupilX, pupilY;
  float targetPupilX, targetPupilY;
  float velX, velY, velW, velH;
  float pVelX, pVelY;
  float k = 0.15;
  float d = 0.65;
  float pk = 0.2;
  float pd = 0.6;
  bool blinking;
  unsigned long lastBlink;
  unsigned long nextBlinkTime;

  void init(float _x, float _y, float _w, float _h) {
    x = targetX = _x;
    y = targetY = _y;
    w = targetW = _w;
    h = targetH = _h;
    pupilX = targetPupilX = 0;
    pupilY = targetPupilY = 0;
    nextBlinkTime = millis() + random(1000, 4000);
  }

  void update() {
    float ax = (targetX - x) * k;
    float ay = (targetY - y) * k;
    float aw = (targetW - w) * k;
    float ah = (targetH - h) * k;
    velX = (velX + ax) * d;
    velY = (velY + ay) * d;
    velW = (velW + aw) * d;
    velH = (velH + ah) * d;
    x += velX;
    y += velY;
    w += velW;
    h += velH;
    
    float pax = (targetPupilX - pupilX) * pk;
    float pay = (targetPupilY - pupilY) * pk;
    pVelX = (pVelX + pax) * pd;
    pVelY = (pVelY + pay) * pd;
    pupilX += pVelX;
    pupilY += pVelY;
  }
};

Eye leftEye, rightEye;
float breathVal = 0;

// ==================================================
// MOTOR CONTROL - RANDOM MOVEMENTS
// ==================================================
void setMotor(int in1, int in2, int direction) {
  if (direction == 1) { digitalWrite(in1, HIGH); digitalWrite(in2, LOW); }
  else if (direction == -1) { digitalWrite(in1, LOW); digitalWrite(in2, HIGH); }
  else { digitalWrite(in1, LOW); digitalWrite(in2, LOW); }
}

void moveForward() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  isMoving = true;
  currentDirection = 0;
  lastMoveChange = millis();
  currentMoveDuration = random(500, 7000);  // Completely random!
  Serial.println("๐Ÿšถ FORWARD");
}

void moveBackward() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, -1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, -1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, -1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, -1);
  isMoving = true;
  currentDirection = 1;
  lastMoveChange = millis();
  currentMoveDuration = random(500, 7000);  // Completely random!
  Serial.println("๐Ÿ”™ BACKWARD");
}

void rotateLeft() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, -1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, -1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 1);
  isMoving = true;
  currentDirection = 2;
  lastMoveChange = millis();
  currentMoveDuration = random(500, 7000);  // Completely random!
  Serial.println("⬅️ ROTATE LEFT");
}

void rotateRight() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 1);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 1);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, -1);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, -1);
  isMoving = true;
  currentDirection = 3;
  lastMoveChange = millis();
  currentMoveDuration = random(500, 7000);  // Completely random!
  Serial.println("➡️ ROTATE RIGHT");
}

void stopAllMotors() {
  setMotor(LEFT_FRONT_IN1, LEFT_FRONT_IN2, 0);
  setMotor(LEFT_BACK_IN1, LEFT_BACK_IN2, 0);
  setMotor(RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, 0);
  setMotor(RIGHT_BACK_IN1, RIGHT_BACK_IN2, 0);
  isMoving = false;
  Serial.println("๐Ÿ›‘ STOP");
}

// RANDOM MOVEMENT SELECTION
void randomMove() {
  int moveType = random(4);  // 0,1,2,3 - completely random
  switch(moveType) {
    case 0: moveForward(); break;
    case 1: moveBackward(); break;
    case 2: rotateLeft(); break;
    case 3: rotateRight(); break;
  }
}

void exploreBehavior() {
  // Start moving if not moving and in normal state
  if (!isMoving && !isAngry && !isScared && !isShaking && !isSleeping && currentPage == 0) {
    randomMove();
    return;
  }
  
  // When current movement finishes, pick another random movement
  if (isMoving && (millis() - lastMoveChange > currentMoveDuration) && !isAngry && !isScared && !isShaking && !isSleeping) {
    randomMove();
  }
}

// ==================================================
// MPU6050 FUNCTIONS - EXTRA SENSITIVE SHAKE
// ==================================================
void calibrateMPU6050() {
  display.clearDisplay();
  display.setFont(NULL);
  display.setCursor(20, 20);
  display.print("Calibrating...");
  display.setCursor(10, 35);
  display.print("Keep device STILL");
  display.display();
  
  float sumX = 0, sumY = 0, sumZ = 0;
  for(int i = 0; i < CALIBRATION_SAMPLES; i++) {
    mpu6050.update();
    sumX += mpu6050.getAngleX();
    sumY += mpu6050.getAngleY();
    sumZ += mpu6050.getAngleZ();
    delay(10);
  }
  
  calibAngleX = sumX / CALIBRATION_SAMPLES;
  calibAngleY = sumY / CALIBRATION_SAMPLES;
  calibAngleZ = sumZ / CALIBRATION_SAMPLES;
  isCalibrated = true;
  
  display.clearDisplay();
  display.setCursor(25, 30);
  display.print("Calibrated!");
  display.display();
  delay(1000);
}

void updateMotion() {
  if (!isCalibrated) return;
  
  mpu6050.update();
  
  currentRoll = mpu6050.getAngleX() - calibAngleX;
  currentPitch = mpu6050.getAngleY() - calibAngleY;
  
  float accelX = mpu6050.getAccX();
  float accelY = mpu6050.getAccY();
  float accelZ = mpu6050.getAccZ();
  
  // EXTRA SENSITIVE SHAKE DETECTION
  float deltaX = fabs(accelX - lastAccelX);
  float deltaY = fabs(accelY - lastAccelY);
  float deltaZ = fabs(accelZ - lastAccelZ);
  
  // Accumulate shake for smoother detection
  float instantShake = (deltaX + deltaY + deltaZ) * 12;
  shakeAccumulator = shakeAccumulator * 0.7 + instantShake * 0.3;
  shakeIntensity = shakeAccumulator;
  
  // SHAKE DETECTION - Very sensitive (threshold 10)
  if (shakeIntensity > 10.0 && !isShaking && !isAngry && !isScared && currentPage == 0) {
    isShaking = true;
    shakeStartTime = millis();
    shakeEndTime = millis() + SHAKE_DURATION;
    currentMood = MOOD_DIZZY;
    stopAllMotors();
    Serial.print("๐ŸŒ€ DIZZY! Shake: ");
    Serial.println(shakeIntensity);
  }
  
  // DIZZY -> ANGRY (move backward)
  if (isShaking && millis() >= shakeEndTime) {
    isShaking = false;
    isAngry = true;
    angryEndTime = millis() + ANGRY_DURATION;
    currentMood = MOOD_ANGRY;
    moveBackward();
    Serial.println("๐Ÿ˜  ANGRY! Moving backward!");
  }
  
  // ANGRY ends
  if (isAngry && millis() >= angryEndTime) {
    isAngry = false;
    currentMood = weatherMood;
    stopAllMotors();
    Serial.println("๐Ÿ˜Š Calm down");
  }
  
  // Edge detection (scared)
  if (!isShaking && !isAngry && !isScared && currentPage == 0) {
    float tiltX = fabs(currentRoll);
    float tiltY = fabs(currentPitch);
    
    if ((tiltX > tiltThreshold || tiltY > tiltThreshold) && !wasAtEdge) {
      wasAtEdge = true;
      isScared = true;
      scaredEndTime = millis() + SCARED_DURATION;
      currentMood = MOOD_SURPRISED;
      stopAllMotors();
      moveBackward();
      Serial.println("๐Ÿ˜จ SCARED! Edge!");
    } else if (tiltX < tiltThreshold && tiltY < tiltThreshold) {
      wasAtEdge = false;
    }
  }
  
  // Scared recovery
  if (isScared && millis() >= scaredEndTime) {
    isScared = false;
    currentMood = weatherMood;
    stopAllMotors();
    delay(300);
    int turn = random(2);
    if (turn == 0) rotateLeft();
    else rotateRight();
    delay(500);
    stopAllMotors();
    randomMove();
  }
  
  // EYE TRACKING (from your working code)
  if (!isShaking && !isAngry && !isScared && !isSleeping && currentPage == 0) {
    float rawX = currentRoll / 5.0;
    float rawY = currentPitch / 5.0;
    
    targetPupilX = constrain(rawX, -12, 12);
    targetPupilY = constrain(rawY, -10, 10);
    
    currentPupilX = currentPupilX * 0.6 + targetPupilX * 0.4;
    currentPupilY = currentPupilY * 0.6 + targetPupilY * 0.4;
    
    leftEye.targetPupilX = currentPupilX;
    leftEye.targetPupilY = currentPupilY;
    rightEye.targetPupilX = currentPupilX;
    rightEye.targetPupilY = currentPupilY;
    
    leftEye.targetX = 28 + (currentRoll / 15);
    leftEye.targetY = 14 + (currentPitch / 15);
    rightEye.targetX = 80 + (currentRoll / 15);
    rightEye.targetY = 14 + (currentPitch / 15);
  }
  
  lastAccelX = accelX;
  lastAccelY = accelY;
  lastAccelZ = accelZ;
}

// ==================================================
// SLEEP MODE
// ==================================================
void handleSleepMode() {
  if (!isSleeping && !isMoving && !isScared && !isShaking && !isAngry && currentPage == 0) {
    if (millis() - lastMoveChange > SLEEP_INTERVAL) {
      isSleeping = true;
      sleepStartTime = millis();
      currentMood = MOOD_SLEEPY;
      stopAllMotors();
      Serial.println("๐Ÿ’ค Sleeping...");
    }
  }
  
  if (isSleeping && (millis() - sleepStartTime > WAKE_DURATION)) {
    isSleeping = false;
    currentMood = weatherMood;
    randomMove();
    Serial.println("✨ Waking up!");
  }
}

// ==================================================
// WEATHER FUNCTIONS
// ==================================================
const unsigned char* getBigIcon(String w) {
  if (w == "Clear") return bmp_clear;
  if (w == "Clouds") return bmp_clouds;
  if (w == "Rain" || w == "Drizzle") return bmp_rain;
  return bmp_clouds;
}

void updateWeatherMood() {
  if (weatherMain == "Clear") weatherMood = MOOD_HAPPY;
  else if (weatherMain == "Rain" || weatherMain == "Drizzle") weatherMood = MOOD_SAD;
  else if (weatherMain == "Thunderstorm") weatherMood = MOOD_SURPRISED;
  else if (weatherMain == "Clouds") weatherMood = MOOD_NORMAL;
  else if (temperature > 35) weatherMood = MOOD_ANGRY;
  else if (temperature < 10) weatherMood = MOOD_SLEEPY;
  else weatherMood = MOOD_NORMAL;
  
  if (!isShaking && !isAngry && !isScared && !isSleeping) {
    currentMood = weatherMood;
  }
}

void getWeatherAndForecast() {
  if (WiFi.status() != WL_CONNECTED) return;
  
  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/weather?q=" + String(CITY) + "," + String(COUNTRY_CODE) + "&appid=" + String(OPENWEATHER_API_KEY) + "&units=metric";
  http.begin(url);
  int httpCode = http.GET();
  if (httpCode == 200) {
    String payload = http.getString();
    JSONVar myObject = JSON.parse(payload);
    if (JSON.typeof(myObject) != "undefined") {
      temperature = double(myObject["main"]["temp"]);
      feelsLike = double(myObject["main"]["feels_like"]);
      humidity = int(myObject["main"]["humidity"]);
      weatherMain = (const char*)myObject["weather"][0]["main"];
      weatherDesc = (const char*)myObject["weather"][0]["description"];
      if (weatherDesc.length() > 0) weatherDesc[0] = toupper(weatherDesc[0]);
      updateWeatherMood();
    }
  }
  http.end();
}

// ==================================================
// TOUCH HANDLER (4 seconds for menu)
// ==================================================
void handleTouch() {
  bool currentPinState = digitalRead(TOUCH_PIN);
  unsigned long now = millis();
  
  if (currentPinState && !lastPinState) {
    pressStartTime = now;
    isLongPressHandled = false;
    if (isSleeping) {
      isSleeping = false;
      currentMood = weatherMood;
      randomMove();
    }
  } 
  else if (currentPinState && lastPinState) {
    if ((now - pressStartTime > LONG_PRESS_TIME) && !isLongPressHandled) {
      // 4 second press - change page
      currentPage++;
      if (currentPage > 2) currentPage = 0;
      if (currentPage != 0) stopAllMotors();
      else if (!isScared && !isSleeping && !isShaking && !isAngry) randomMove();
      isLongPressHandled = true;
      Serial.print("Page: ");
      Serial.println(currentPage);
    }
  } 
  else if (!currentPinState && lastPinState) {
    if ((now - pressStartTime < LONG_PRESS_TIME) && !isLongPressHandled) {
      tapCounter++;
      lastTapTime = now;
    }
  }
  lastPinState = currentPinState;
  
  if (tapCounter > 0) {
    if (now - lastTapTime > DOUBLE_TAP_DELAY) {
      if (tapCounter == 1 && currentPage == 0 && !isShaking && !isAngry && !isScared && !isSleeping) {
        // Single tap - change mood
        currentMood++;
        if (currentMood > MOOD_SUSPICIOUS) currentMood = 0;
        weatherMood = currentMood;
        Serial.print("Mood: ");
        Serial.println(currentMood);
      }
      tapCounter = 0;
    }
  }
}

// ==================================================
// DRAW FUNCTIONS (FROM YOUR WORKING CODE)
// ==================================================
void drawDizzyEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;
  int cx = ix + iw/2;
  int cy = iy + ih/2;
  
  display.fillCircle(cx, cy, iw/2, SH110X_WHITE);
  
  unsigned long now = millis();
  float angle = (now - shakeStartTime) * 0.025;
  int radius = iw / 3;
  
  int pupilX = cx + cos(angle) * radius;
  int pupilY = cy + sin(angle) * radius;
  
  display.fillCircle(pupilX, pupilY, 4, SH110X_BLACK);
  display.fillCircle(pupilX + 2, pupilY - 2, 1, SH110X_WHITE);
  
  int pupilX2 = cx - cos(angle) * radius;
  int pupilY2 = cy - sin(angle) * radius;
  display.fillCircle(pupilX2, pupilY2, 3, SH110X_BLACK);
  
  int starOffset = (now / 80) % 4;
  display.drawBitmap(6 - starOffset, 0, bmp_dizzy_stars, 16, 16, SH110X_WHITE);
  display.drawBitmap(106 + starOffset, 0, bmp_dizzy_stars, 16, 16, SH110X_WHITE);
}

void drawAngryEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;
  
  display.fillRoundRect(ix, iy, iw, ih, 8, SH110X_WHITE);
  
  int cx = ix + iw/2;
  int cy = iy + ih/2;
  int pupilSize = iw / 2.5;
  
  int px = cx - 2;
  int py = cy;
  
  display.fillRoundRect(px - pupilSize/2, py - pupilSize/2, pupilSize, pupilSize, pupilSize/2, SH110X_BLACK);
  
  if (isLeft) {
    for(int i = 0; i < 8; i++) {
      display.drawLine(ix - 2, iy - 2 + i, ix + iw + 2, iy + 6 + i, SH110X_BLACK);
    }
    display.drawBitmap(ix - 12, iy - 6, bmp_angry_mark, 16, 16, SH110X_WHITE);
  } else {
    for(int i = 0; i < 8; i++) {
      display.drawLine(ix - 2, iy + 6 + i, ix + iw + 2, iy - 2 + i, SH110X_BLACK);
    }
    display.drawBitmap(ix + iw - 4, iy - 6, bmp_angry_mark, 16, 16, SH110X_WHITE);
  }
  
  for(int i = 0; i < 3; i++) {
    display.drawLine(ix + 2 + i*4, iy + ih - 2, ix + 6 + i*4, iy + ih - 6, SH110X_WHITE);
  }
}

void drawNormalEye(Eye& e, bool isLeft) {
  int ix = (int)e.x;
  int iy = (int)e.y;
  int iw = (int)e.w;
  int ih = (int)e.h;

  int r = 8;
  if (iw < 20) r = 3;
  display.fillRoundRect(ix, iy, iw, ih, r, SH110X_WHITE);

  int cx = ix + iw / 2;
  int cy = iy + ih / 2;

  int pw = iw / 2.2;
  int ph = ih / 2.2;

  int px = cx + (int)e.pupilX - (pw / 2);
  int py = cy + (int)e.pupilY - (ph / 2);

  if (px < ix) px = ix;
  if (px + pw > ix + iw) px = ix + iw - pw;
  if (py < iy) py = iy;
  if (py + ph > iy + ih) py = iy + ih - ph;

  display.fillRoundRect(px, py, pw, ph, r / 2, SH110X_BLACK);

  if (iw > 15 && ih > 15) {
    display.fillCircle(px + pw - 4, py + 4, 2, SH110X_WHITE);
  }
  
  if (currentMood == MOOD_HAPPY || currentMood == MOOD_LOVE) {
    display.fillRect(ix, iy + ih - 10, iw, 12, SH110X_BLACK);
  }
  else if (currentMood == MOOD_SLEEPY || isSleeping) {
    display.fillRect(ix, iy, iw, ih/2, SH110X_BLACK);
  }
  else if (currentMood == MOOD_SAD) {
    if (isLeft) {
      for(int i = 0; i < 8; i++) display.drawLine(ix, iy + i, ix + iw, iy + 4 + i, SH110X_BLACK);
    } else {
      for(int i = 0; i < 8; i++) display.drawLine(ix, iy + 4 + i, ix + iw, iy + i, SH110X_BLACK);
    }
  }
}

void drawMouth() {
  int mx = 64;
  int my = 54;
  
  if (isScared) {
    display.fillCircle(mx, my + 2, 6, SH110X_BLACK);
    display.fillCircle(mx, my, 3, SH110X_WHITE);
    return;
  }
  
  if (currentMood == MOOD_DIZZY && isShaking) {
    unsigned long now = millis();
    for(int i = -8; i <= 8; i++) {
      int y = my + sin(i * 0.8 + now * 0.03) * 4;
      display.drawPixel(mx + i, y, SH110X_WHITE);
    }
    return;
  }
  
  if (currentMood == MOOD_ANGRY && isAngry) {
    display.fillRect(mx - 8, my - 2, 16, 5, SH110X_BLACK);
    for(int i = -5; i <= 5; i+=3) {
      display.drawLine(mx + i, my - 2, mx + i, my + 2, SH110X_WHITE);
    }
    return;
  }
  
  switch(currentMood) {
    case MOOD_HAPPY:
      for(int i = -6; i <= 6; i++) {
        int y = my - (i*i / 16);
        if(y < my + 2) display.drawPixel(mx + i, y, SH110X_WHITE);
      }
      break;
    case MOOD_SAD:
      for(int i = -6; i <= 6; i++) {
        int y = my + 2 + (i*i / 16);
        display.drawPixel(mx + i, y, SH110X_WHITE);
      }
      break;
    case MOOD_SURPRISED:
      display.fillCircle(mx, my + 1, 3, SH110X_BLACK);
      display.drawCircle(mx, my + 1, 3, SH110X_WHITE);
      break;
    case MOOD_SLEEPY:
      display.drawLine(mx - 4, my, mx + 4, my, SH110X_WHITE);
      break;
    case MOOD_LOVE:
      display.drawBitmap(mx - 6, my - 2, bmp_heart, 12, 12, SH110X_WHITE);
      break;
    default:
      display.drawLine(mx - 4, my, mx + 4, my, SH110X_WHITE);
      break;
  }
}

void updateEyePhysics() {
  unsigned long now = millis();
  breathVal = sin(now / 800.0) * 1.5;
  
  int blinkDelay = (isShaking || isScared || isAngry) ? 60 : 120;
  int blinkMin = (isShaking || isScared || isAngry) ? 300 : 2000;
  int blinkMax = (isShaking || isScared || isAngry) ? 800 : 6000;
  
  if (now > leftEye.nextBlinkTime) {
    leftEye.blinking = true;
    rightEye.blinking = true;
    leftEye.lastBlink = now;
    leftEye.nextBlinkTime = now + random(blinkMin, blinkMax);
  }
  
  if (leftEye.blinking) {
    leftEye.targetH = 2;
    rightEye.targetH = 2;
    if (now - leftEye.lastBlink > blinkDelay) {
      leftEye.blinking = false;
      rightEye.blinking = false;
      leftEye.targetH = 32;
      rightEye.targetH = 32;
    }
  }
  
  leftEye.update();
  rightEye.update();
}

void drawEmoPage() {
  updateEyePhysics();
  
  if (currentMood == MOOD_DIZZY && isShaking) {
    drawDizzyEye(leftEye, true);
    drawDizzyEye(rightEye, false);
  }
  else if (currentMood == MOOD_ANGRY && isAngry) {
    drawAngryEye(leftEye, true);
    drawAngryEye(rightEye, false);
  }
  else {
    drawNormalEye(leftEye, true);
    drawNormalEye(rightEye, false);
  }
  
  drawMouth();
  
  if (!isShaking && !isAngry && !isScared && !isSleeping) {
    if (currentMood == MOOD_LOVE) {
      display.drawBitmap(56, 0, bmp_heart, 16, 16, SH110X_WHITE);
    } else if (currentMood == MOOD_SLEEPY || isSleeping) {
      display.drawBitmap(110, 0, bmp_zzz, 16, 16, SH110X_WHITE);
    }
  }
}

void drawClockPage() {
  struct tm t;
  if (!getLocalTime(&t)) {
    display.setFont(NULL);
    display.setCursor(30, 30);
    display.print("Syncing...");
    return;
  }
  
  String ampm = (t.tm_hour >= 12) ? "PM" : "AM";
  int h12 = t.tm_hour % 12;
  if (h12 == 0) h12 = 12;
  
  display.setTextColor(SH110X_WHITE);
  display.setFont(NULL);
  display.setCursor(114, 0);
  display.print(ampm);
  
  display.setFont(&FreeSansBold18pt7b);
  char timeStr[6];
  sprintf(timeStr, "%02d:%02d", h12, t.tm_min);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - w) / 2, 42);
  display.print(timeStr);
  
  display.setFont(&FreeSans9pt7b);
  char dateStr[20];
  strftime(dateStr, 20, "%a, %b %d", &t);
  display.getTextBounds(dateStr, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - w) / 2, 60);
  display.print(dateStr);
}

void drawWeatherPage() {
  if (WiFi.status() != WL_CONNECTED) {
    display.setFont(NULL);
    display.setCursor(0, 0);
    display.print("No WiFi");
    return;
  }
  
  display.drawBitmap(96, 0, getBigIcon(weatherMain), 32, 32, SH110X_WHITE);
  display.setFont(&FreeSansBold9pt7b);
  String c = CITY;
  c.toUpperCase();
  display.setCursor(0, 14);
  if (c.length() > 9) c = c.substring(0, 8) + ".";
  display.print(c);
  
  display.setFont(&FreeSansBold18pt7b);
  int tempInt = (int)temperature;
  display.setCursor(0, 48);
  display.print(tempInt);
  
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(String(tempInt).c_str(), 0, 48, &x1, &y1, &w, &h);
  display.fillCircle(x1 + w + 5, 26, 4, SH110X_WHITE);
  
  display.setFont(NULL);
  display.drawBitmap(88, 32, bmp_tiny_drop, 8, 8, SH110X_WHITE);
  display.setCursor(100, 32);
  display.print(humidity);
  display.print("%");
  display.setCursor(88, 45);
  display.print("~");
  display.print((int)feelsLike);
  display.drawLine(0, 52, 128, 52, SH110X_WHITE);
  display.setCursor(0, 55);
  String shortDesc = weatherDesc;
  if (shortDesc.length() > 15) shortDesc = shortDesc.substring(0, 13) + "..";
  display.print(shortDesc);
}

void playBootAnimation() {
  display.setTextColor(SH110X_WHITE);
  int cx = 64;
  int cy = 32;
  
  for (int r = 0; r < 80; r += 4) {
    display.clearDisplay();
    display.fillCircle(cx, cy, r, SH110X_WHITE);
    display.display();
    delay(8);
  }
  
  for (int r = 0; r < 80; r += 4) {
    display.clearDisplay();
    display.fillCircle(cx, cy, 80, SH110X_WHITE);
    display.fillCircle(cx, cy, r, SH110X_BLACK);
    display.display();
    delay(8);
  }
  
  display.setFont(&FreeSansBold9pt7b);
  String bootText = "IRIS";
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(bootText, 0, 0, &x1, &y1, &w, &h);
  display.clearDisplay();
  display.setCursor((SCREEN_WIDTH - w) / 2, 36);
  display.print(bootText);
  display.display();
  delay(2000);
}

// ==================================================
// SETUP
// ==================================================
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("\n╔════════════════════════════╗");
  Serial.println("║       IRIS DESK BUDDY      ║");
  Serial.println("║  Random Movements + Shake  ║");
  Serial.println("╚════════════════════════════╝\n");
  
  pinMode(LEFT_FRONT_IN1, OUTPUT);
  pinMode(LEFT_FRONT_IN2, OUTPUT);
  pinMode(LEFT_BACK_IN1, OUTPUT);
  pinMode(LEFT_BACK_IN2, OUTPUT);
  pinMode(RIGHT_FRONT_IN1, OUTPUT);
  pinMode(RIGHT_FRONT_IN2, OUTPUT);
  pinMode(RIGHT_BACK_IN1, OUTPUT);
  pinMode(RIGHT_BACK_IN2, OUTPUT);
  stopAllMotors();
  
  Wire.begin(SDA_PIN, SCL_PIN);
  pinMode(TOUCH_PIN, INPUT_PULLUP);
  
  display.begin(0x3C, true);
  display.setTextColor(SH110X_WHITE);
  display.clearDisplay();
  
  mpu6050.begin();
  calibrateMPU6050();
  
  // Eyes positioned HIGHER (y=14)
  leftEye.init(28, 14, 32, 32);
  rightEye.init(80, 14, 32, 32);
  
  playBootAnimation();
  
  display.clearDisplay();
  display.setFont(NULL);
  display.setCursor(20, 30);
  display.print("Connecting WiFi");
  display.display();
  
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  unsigned long wifiStart = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - wifiStart < 15000)) {
    delay(500);
    display.print(".");
    display.display();
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    display.clearDisplay();
    display.setCursor(25, 30);
    display.print("WiFi Connected!");
    display.display();
    delay(1000);
    
    configTime(0, 0, "pool.ntp.org");
    setenv("TZ", TIMEZONE, 1);
    tzset();
    getWeatherAndForecast();
    lastWeatherUpdate = millis();
  } else {
    display.clearDisplay();
    display.setCursor(15, 30);
    display.print("WiFi Failed!");
    display.display();
    delay(2000);
  }
  
  randomMove();
  Serial.println("IRIS READY! Shake me to see dizzy/angry!");
}

// ==================================================
// LOOP
// ==================================================
void loop() {
  handleTouch();
  updateMotion();
  
  if (!isSleeping && currentPage == 0 && !isShaking && !isAngry && !isScared) {
    exploreBehavior();
  } else if (currentPage != 0) {
    stopAllMotors();
  }
  
  handleSleepMode();
  
  if (millis() - lastWeatherUpdate > 600000 && WiFi.status() == WL_CONNECTED) {
    getWeatherAndForecast();
    lastWeatherUpdate = millis();
  }
  
  display.clearDisplay();
  
  if (currentPage == 0) {
    drawEmoPage();
  } 
  else if (currentPage == 1) {
    drawClockPage();
  } 
  else if (currentPage == 2) {
    drawWeatherPage();
  }
  
  display.display();
  delay(20);
}
๐Ÿ“ก Both codes include: WiFi connection, OpenWeather API, NTP clock, MPU6050 calibration, eye tracking, 4 motor control, shake→dizzy→angry, sleep mode, touch handling (long press changes page, single tap changes mood).
๐Ÿ”ง For Beta/Charming code: additional 8 movement patterns, MOOD_CHARMING with water eyes, happy wiggle on touch.

๐Ÿ› ️ Assembly Steps

  • 1️⃣ 3D print enclosure using SLS 1172 Pro Nylon (order from JLC3DP).
  • 2️⃣ Solder DRV8833 modules and connect N20 motors to D0-D10 according to pin table.
  • 3️⃣ Set MT3608 output to 5.0V, connect to ESP32 5V/GND.
  • 4️⃣ Wire TP4056, LiPo battery, and slide switch.
  • 5️⃣ Connect OLED (SDA→D4, SCL→D5), MPU6050 (same I2C), touch sensor (D6).
  • 6️⃣ Choose which code to upload (Complete/Charming or Original). Change WiFi & OpenWeather API key. Select XIAO ESP32-S3 board. Upload and power on!

๐ŸŽ“ Design your own PCBs with Altium

Altium makes custom PCB design fast and professional. Students get free access to Altium Student Lab – step‑by‑step courses & certifications.

๐Ÿ‘‰ Explore Altium Student Lab →
๐Ÿ’ก Tip: For best results, place IRIS on a flat desk. Shake gently to see angry reaction. Touch the sensor (or top of enclosure) to activate charming water eyes (Beta code only). Long‑press cycles through Clock → Weather → Eyes.

Comments

Popular posts from this blog

Solar Tracking System

Arduino Code

Arduino Code Car Parking System