IRIS · Interactive Robotic Intelligence System
๐ค 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!
• ๐ญ 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
- XIAO ESP32-S3 – Buy Here
- N20 Motors (4x) – Buy Here
- TP4056 Charging Module – Buy Here
- 3.7V LiPo Battery 1000mAh – Buy Here
- 4mm Slide Switch – Buy Here
- Jumper Wires – Buy Here
- 1.3" OLED Display (SH1106) – Buy Here
- TTP223 Touch Sensor – Buy Here
- MPU6050 Gyroscope – Buy Here
- DRV8833 Motor Driver (2x) – Buy Here
- MT3608 Booster Module – Buy Here
๐จ️ 3D Print Files + Material
๐ Download STL files (enclosure, wheels, mounts):
Recommended material: SLS 1172 Pro Nylon – white, grainy finish, strong & lightweight.
๐ง Print via JLC3DP (upload STL, choose SLS Nylon).
๐ Pin Connections (XIAO ESP32-S3)
| Component | XIAO Pin | Notes |
|---|---|---|
| OLED SDA / MPU6050 SDA | D4 | I2C shared |
| OLED SCL / MPU6050 SCL | D5 | I2C shared |
| TTP223 Touch Sensor | D6 | INPUT_PULLUP |
| Left Front Motor (IN1,IN2) | D0, D1 | DRV8833 #1 |
| Left Back Motor (IN1,IN2) | D2, D3 | DRV8833 #1 |
| Right Front Motor (IN1,IN2) | D7, D8 | DRV8833 #2 |
| Right Back Motor (IN1,IN2) | D9, D10 | DRV8833 #2 |
| MT3608 Boost (5V out) | 5V / GND | powers 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.
๐ง 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
Post a Comment