Skip to main content

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

  1. where the mount stl files for mounting motors to chasis

    ReplyDelete

Post a Comment

Popular posts from this blog

Solar Tracking System

Dual-Axis Solar Tracking System | Arduino SimpleCircuits Arduino Solar DIY Renewable Energy Project Dual-Axis Solar Tracking System Build a high-performance solar tracker that follows the sun in real-time. Four LDR sensors, an Arduino UNO, and two servo motors — boosting energy capture by up to 40% versus fixed mounts. 40% More Efficient 4 LDR Sensors 2 Servo Axes <300mA Power Draw Finished Build System Overview How the System Works The tracker reads the sky through four LDR sensors placed around the panel, computes intensity deltas, and drives two SG90 servos in a real-time closed loop — keeping the panel perpendicular to sunlight from sunrise to sunset. ...

Arduino Code Car Parking System

 // Created by Simple Circuits  #include <Wire.h>  #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27,16,2);    #include <Servo.h>  Servo myservo; int IR1 = 2; int IR2 = 3; int Slot = 4;           //Total number of parking Slots int flag1 = 0; int flag2 = 0; void setup() {   Serial.begin(9600);      lcd.init(); //initialize the lcd     lcd.backlight(); //open the backlight     pinMode(IR1, INPUT); pinMode(IR2, INPUT);    myservo.attach(4); myservo.write(100); lcd.setCursor (0,0); lcd.print("     ARDUINO    "); lcd.setCursor (0,1); lcd.print(" PARKING SYSTEM "); delay (2000); lcd.clear();   } void loop(){  if(digitalRead (IR1) == LOW && flag1==0){ if(Slot>0){flag1=1; if(flag2==0){myservo.write(0); Slot = Slot-1;} }else{ lcd.setCursor (0,0); lcd.print("    SORRY :(    ");   lc...

Arduino Code

 //define Pins #include <Servo.h> Servo servo; int trigPin = 11; int echoPin = 12; // defines variables long duration; int distance; void setup()  {   servo.attach(13);   servo.write(180);  delay(2000);    // Sets the trigPin as an Output pinMode(trigPin, OUTPUT); // Sets the echoPin as an Input  pinMode(echoPin, INPUT); } void loop()  { // Clears the trigPin digitalWrite(trigPin, LOW); delayMicroseconds(2); // Sets the trigPin on HIGH state for 10 micro seconds digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); // Reads the echoPin, returns the sound wave travel time in microseconds duration = pulseIn(echoPin, HIGH); // Calculating the distance distance= duration*0.034/2; // Prints the distance on the Serial Monitor Serial.print("Distance: "); Serial.println(distance); if ( distance <= 25   ) // Change Distance according to Ultrasonic Sensor Placement  { servo.write(180); delay(3000); ...