A self-contained embedded companion platform running FreeRTOS on dual-core ESP32, coordinating asynchronous OLED facial graphics, ultrasonic sentry surveillance, environmental sensors, and a local Wi-Fi telemetry dashboard.
Developer: Justin | Platform: ESP32 | Version: 12.0 (Final)
1. Project Overview
Retro-Bot is an advanced desktop companion robot powered by an ESP32 microcontroller. It features a "living" OLED face that reacts to its environment, plays arcade games, and acts as a security sentry. It is controlled via a local Wi-Fi web dashboard hosted directly on the robot.
Key Features
- 🧠 Emotional AI: Reacts to temperature, gas, movement, and proximity.
- 👀 Sentry Mode: Wakes up when it detects human motion (PIR).
- 🛡️ Personal Space: Gets "Suspicious" or "Panicked" if you get too close (Ultrasonic).
- 🍔 Hand Feeding: "Eats" when you hold an object to its mouth (IR Sensor).
- 🎮 Arcade Console: Plays Tic-Tac-Toe, Snake, and Pong via phone controller.
- 🗣️ Living Face: Realistic blinking eyes, breathing animation, and talking text.
2. Bill of Materials (Hardware)
| Component | Description | Function |
|---|---|---|
| ESP32 DevKit V1 | Microcontroller | The "Brain" + Wi-Fi Server |
| OLED Display | 1.3" I2C (SH1106) | The "Face" and Game Screen |
| DHT11 | Blue Grid Sensor | Measures Temperature & Humidity |
| MQ-135 | Gas Sensor | Detects Air Quality / Smoke |
| HC-SR04 | Ultrasonic Eyes | Measures Distance (0cm - 400cm) |
| PIR Sensor | White Dome | Detects Motion (Infrared body heat) |
| IR Sensor | Black LED pair | Detects close objects (Feeding) |
| Active Buzzer | Black Cylinder | Sound Effects & Alarms |
| Jumper Wires | M-M, M-F | Connections |
3. Master Wiring Guide
⚠️ IMPORTANT: The ESP32 uses 3.3V logic, but the VIN pin provides 5V
from USB. Most sensors need 5V (VIN), but the OLED works best on 3.3V.
Power Rails
- ESP32 GND → Breadboard Blue (-) Rail
- ESP32 VIN (5V) → Breadboard Red (+) Rail
Pin Connections Table
| Component | Component Pin | ESP32 Pin | Note |
|---|---|---|---|
| OLED Screen | SDA | GPIO 21 | Data Line |
| SCL | GPIO 22 | Clock Line | |
| VCC | 3V3 | Do not use 5V for Screen | |
| GND | GND | ||
| DHT11 (Temp) | DATA / OUT | GPIO 4 | |
| MQ-135 (Gas) | A0 / Analog | GPIO 34 | Analog Input Only |
| Buzzer | POS (+) | GPIO 5 | |
| PIR (Motion) | OUT (Middle) | GPIO 13 | Digital Signal |
| Ultrasonic | TRIG | GPIO 18 | Output Pulse |
| ECHO | GPIO 19 | Input Echo | |
| IR (Feeding) | OUT | GPIO 23 | Digital Signal (LOW = Active) |
4. Software & Installation
Required Libraries
Install these via Arduino IDE Library Manager:
Adafruit GFX Library(Core graphics)Adafruit SH110X(Screen Driver - Crucial for removing static)DHT sensor library(by Adafruit)Adafruit Unified Sensor
Upload Settings
- Board: DOIT ESP32 DEVKIT V1
- Upload Speed: 921600
- Flash Frequency: 80MHz
Troubleshooting "Static/Snow" on Screen
If the screen looks grainy:
- Ensure you are using
Adafruit_SH1106Gin the code (V12 code does this). - Twist the SDA (Yellow) and SCL (Blue) wires together to cancel electrical noise.
5. User Guide: How to Use Retro-Bot
A. Connecting
- Power on the robot.
- On your phone/PC, connect to Wi-Fi:
RetroBot_AP(No password). - Open browser to:
192.168.4.1
B. Dashboard Controls
- HAPPY: Forces normal mode.
- FEED: Triggers "Eating" animation manually.
- PANIC: Triggers alarm mode manually.
- SLEEP: Turns off screen and waits for motion (Sentry Mode).
- GAME MODE: Switches the face to the Game Menu.
C. Sensor Behaviors (Autonomous Mode)
- Motion (PIR): If sleeping, any movement wakes the robot up.
- Feeding (IR): Place finger 2cm from mouth → Robot eats ("YUMMY!").
- Distance (Sonar):
- < 25cm: Robot gets Suspicious (Squinting eyes).
- < 10cm: Robot Panics (Screaming + Beeping).
- Air Quality (Gas): High gas levels trigger Panic mode.
Source Code
Below is the placeholder for the source code. You can paste your Arduino sketch here.
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <DHT.h>
const char* ssid = "RetroBot_AP";
const char* password = "";
#define PIN_BUZZER 5
#define PIN_DHT 4
#define PIN_MQ135 34
#define PIN_PIR 13
#define PIN_TRIG 18
#define PIN_ECHO 19
#define OLED_SDA 21
#define OLED_SCL 22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDRESS 0x3C
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(PIN_DHT, DHT11);
WebServer server(80);
enum EyeLidShape { LID_FLAT, LID_ANGRY, LID_SAD, LID_TIRED };
float temp = 0;
int gasLevel = 0;
long dist_cm = 0;
long prev_dist_cm = 0;
String currentMood = "HAPPY";
unsigned long manualOverrideTimer = 0;
bool isSleeping = false;
String overrideText = "";
unsigned long overrideTextTimer = 0;
unsigned long unixTime = 0;
unsigned long lastSyncMillis = 0;
bool timeSynced = false;
int gasBaseline = 0;
float tempBaseline = 0;
long lastDistChangeTime = 0;
unsigned long lastMotionTime = 0;
const unsigned long SLEEP_TIMEOUT = 10000;
bool showBigText = false;
bool isFocusMode = false;
unsigned long focusStartTime = 0;
unsigned long focusDuration = 0;
const unsigned long DISTRACTION_THRESHOLD = 6000;
const char* txt_MOTIVATION[] = {"FOCUS!", "DO IT NOW", "EYES HERE", "NO PHONES", "STAY HARD", "GRIND", "WORK WAR", "LOCK IN"};
unsigned long lastDHTRead = 0;
unsigned long lastDistRead = 0;
const int GAS_SAMPLES = 5;
int gasReadings[GAS_SAMPLES];
int gasIndex = 0; int totalGas = 0; int averageGas = 0;
int GAS_THRESHOLD_OFFSET = 200;
const int DIST_PANIC = 10;
const int DIST_SUS = 20;
const int DIST_TOUCH = 4;
const int DIST_VELOCITY_TRIGGER = -10;
int breathVal = 0; bool breathIn = true;
String currentDisplayText = "Just chillin";
unsigned long lastTextUpdate = 0;
const char* txt_HAPPY[] = {"Hello friend!", "Happy to help", "Wonderful day", "Ready for you", "All systems go", "Smiling rn", "Peaceful here", "Hi there!"};
const char* txt_SUS[] = {"Oh, hello?", "Who is there?", "Identifying...", "May I help?", "Curious...", "Just checking", "New friend?", "I see you"};
const char* txt_PANIC[] = {"Oh dear!", "Too close pls", "Please step back", "Safety first", "Bit crowded", "Excuse me!", "Need space", "Careful!"};
const char* txt_ANGRY[] = {"Please stop", "Not nice", "Feeling blue", "System upset", "Be kind pls", "I need space", "Boundaries", "Hmph..."};
const char* txt_SWEAT[] = {"It's warm!", "Whew hot!", "Need a fan", "Staying cool", "Oh my circuits", "Toasty here", "Summer vibes", "Phew!"};
const char* txt_LOVE[] = {"You're great!", "So kind!", "Best friends", "Lovely day", "Heart you", "Thank you!", "Sending joy", "You shine!"};
const char* txt_CONFUSED[] = {"I'm not sure", "Say again?", "Hmm...", "Thinking...", "One moment", "Processing", "Trying my best", "Oh?"};
int phraseIndex = 0;
unsigned long lastBlink = 0; bool eyesOpen = true;
float pupilSize = 3.5;
float eyeX = 0; float eyeY = 0;
int activeGame = 0; int score = 0;
struct Player { bool active; int x; int y; int input; };
Player p1 = {false,0,0,0}; Player p2 = {false,0,0,0};
int ttt_board[9]; int ttt_status = 0; int ttt_turn = 1;
const int SNAKE_MAX_LEN=100; int snakeX[SNAKE_MAX_LEN]; int snakeY[SNAKE_MAX_LEN]; int snakeLen=3; int snakeDir=1; int foodX=0, foodY=0; bool snakeGameOver=false; unsigned long lastSnakeMove=0;
float ballX=64, ballY=32; float ballVX=2, ballVY=2; int paddle1Y=24; int paddle2Y=24; int p1Score=0; int p2Score=0; bool pongGameOver=false;
int dinoY=50; float dinoV=0; bool dinoGrounded=true; int cactusX=128; int dinoScore=0; bool dinoOver=false;
int shipX=64; int bulletX=-1; int bulletY=-1; int enemyX[5]; int enemyY[5]; bool enemyAlive[5]; int invScore=0; bool invOver=false; unsigned long lastInvMove=0; int invDir=1;
int tankWinner=0;
bool brk[5][10]; int bBallX=64, bBallY=50; float bVelX=2, bVelY=-2;
int pipeX=128; int pipeGap=20;
int mazeX=2, mazeY=2; int mazeW=64, mazeH=32;
float landX=64, landY=10, landVX=0, landVY=0, landFuel=100; bool landCrashed=false, landLanded=false;
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head><meta name="viewport" content="width=device-width, initial-scale=1">
<title>RetroBot Sentry</title>
<style>
body{font-family:'Courier New', monospace;background-color:#000;color:#fff;text-align:center;margin:0;padding:10px;}
h1{margin:10px 0;color:#00ff00;text-shadow:0 0 5px #00ff00;}
h2{margin:5px 0;}
h3{border-bottom:2px solid #555;padding-bottom:5px;margin-top:15px;color:#aaa;font-size:16px;}
button{background:#222;color:#0f0;border:1px solid #0f0;padding:15px;margin:4px;font-family:inherit;font-size:16px;width:45%;cursor:pointer;border-radius:5px;}
button:active{background:#0f0;color:#000;}
.red{color:#f00;border-color:#f00;}
.red:active{background:#f00;color:#fff;}
.p{border:1px solid #333;padding:10px;margin:10px 0;border-radius:8px;background:#111;}
.s-box{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.v{font-size:24px;font-weight:bold;}
.l{font-size:12px;color:#888;}
.h{display:none;}
.dpad{display:grid;grid-template-columns:50px 50px 50px;grid-template-rows:50px 50px 50px;gap:5px;justify-content:center;margin:0 auto;}
.btn-arr{width:100% !important;height:100% !important;padding:0 !important;font-size:24px !important;margin:0 !important;}
input{font-family:inherit;font-size:16px;}
.a{background:#300 !important;border-color:#f00 !important;}
</style>
</head>
<body>
<h1>RetroBot Sentry</h1>
<div id="mU">
<div class="p"><h3>STATUS: <span id="st">LOADING...</span></h3><div><div id="cT" class="c">TEMP<br><span id="t">--</span>C</div><div id="cG" class="c">AIR<br><span id="g">--</span></div><div id="cD" class="c">DIST<br><span id="d">--</span>cm</div></div></div>
<div class="p" style="border:2px solid #0f0;box-shadow:0 0 10px #0f0"><h3>FOCUS TIMER</h3>
<div style="display:flex;justify-content:center;gap:5px;margin-bottom:10px">
<button onclick="sF(1,25)" style="width:30%">25m</button>
<button onclick="sF(1,45)" style="width:30%">45m</button>
<button onclick="sF(1,60)" style="width:30%">1h</button>
</div>
<div style="display:flex;justify-content:center;gap:5px">
<input id="fm" type="number" placeholder="Min" style="width:30%;padding:10px;background:#222;color:#0f0;border:1px solid #0f0;text-align:center;border-radius:5px">
<button onclick="sF(1,0)" style="width:60%;background:#004400;color:#0f0;border:1px solid #0f0;font-weight:bold">START SESSION</button>
</div>
<button class="red" onclick="sF(0,0)" style="width:95%;margin-top:10px;box-shadow:0 0 5px red">STOP FOCUS</button>
<div id="fStat" style="margin-top:10px;font-size:14px;font-weight:bold;color:#888;text-transform:uppercase">STATUS: IDLE</div>
</div>
<div class="p" style="border:1px solid #00f"><h3>ARCADE MENU</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<button onclick="lG(1)">❌ TTT (vs CPU)</button>
<button onclick="lG(2)">🐍 SNAKE</button>
<button onclick="lG(3)">🏓 PONG (vs AI)</button>
<button onclick="lG(4)">🦖 DINO RUN</button>
<button onclick="lG(5)">👾 INVADERS</button>
<button onclick="lG(6)">🧱 BREAKOUT</button>
<button onclick="lG(7)">🐦 FLAPPY</button>
<button onclick="lG(8)">🕸 MAZE</button>
<button style="grid-column:span 2" onclick="lG(9)"> LUNAR LANDER</button>
</div>
</div>
<div class="p" style="border-color:#f0f"><h3>MULTIPLAYER SETUP</h3>
<button id="bp1" onclick="join(1)">JOIN AS P1 (LEFT)</button>
<button id="bp2" onclick="join(2)">JOIN AS P2 (RIGHT)</button>
<div id="pstat" style="font-size:10px;margin-top:5px">NOT CONNECTED</div>
</div>
</div>
<div id="tU" class="h"><h2>TIC-TAC-TOE</h2><div id="tM" style="color:yellow">YOUR TURN (X)</div><div class="grid"><div class="cell" onclick="tMv(0)" id="c0"></div><div class="cell" onclick="tMv(1)" id="c1"></div><div class="cell" onclick="tMv(2)" id="c2"></div><div class="cell" onclick="tMv(3)" id="c3"></div><div class="cell" onclick="tMv(4)" id="c4"></div><div class="cell" onclick="tMv(5)" id="c5"></div><div class="cell" onclick="tMv(6)" id="c6"></div><div class="cell" onclick="tMv(7)" id="c7"></div><div class="cell" onclick="tMv(8)" id="c8"></div></div><button class="red" onclick="xG()">EXIT</button></div>
<div id="sU" class="h"><h2 style="color:#0f0">SNAKE</h2><h3 id="sc">SCORE: 0</h3><div class="dpad"><div></div><button class="btn-arr" onclick="nk(0)">▲</button><div></div><button class="btn-arr" onclick="nk(3)">◀</button><button class="btn-arr red" onclick="nk(9)">R</button><button class="btn-arr" onclick="nk(1)">▶</button><div></div><button class="btn-arr" onclick="nk(2)">▼</button><div></div></div><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="pU" class="h"><h2 style="color:cyan">PONG 1P</h2><h3>SCORE: <span id="ps">0</span></h3><div style="display:flex;justify-content:center;gap:10px"><button style="padding:30px" onclick="pg(-1)">▲ UP</button><button style="padding:30px" onclick="pg(1)">▼ DOWN</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="dU" class="h"><h2 style="color:#fa0">DINO RUN</h2><h3>SCORE: <span id="ds">0</span></h3><button style="padding:40px;width:80%;font-size:20px" onmousedown="dn(1)">[JUMP!]</button><br><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="iU" class="h"><h2 style="color:#0f0">INVADERS</h2><h3>SCORE: <span id="is">0</span></h3><div style="display:flex;justify-content:center;gap:5px"><button style="padding:20px" onclick="iv(-1)">[L]</button><button style="padding:20px;background:#f00;color:white" onclick="iv(10)">[FIRE]</button><button style="padding:20px" onclick="iv(1)">[R]</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="tkU" class="h"><h2 style="color:#fa0">BREAKOUT</h2><h3>SCORE: <span id="bks">0</span></h3><div style="display:flex;justify-content:center;gap:20px"><button style="padding:30px" onmousedown="bk(-1)">◀ LEFT</button><button style="padding:30px" onmousedown="bk(1)">RIGHT ▶</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="fbU" class="h"><h2 style="color:#0f0">FLAPPY</h2><h3>SCORE: <span id="fbs">0</span></h3><button style="padding:50px;width:90%;font-size:24px;border:2px solid #0f0" onmousedown="fb(1)">[ FLAP! ]</button><br><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="mzU" class="h"><h2 style="color:#0ff">MAZE RUN</h2><div class="dpad"><div></div><button class="btn-arr" onmousedown="mz(0)">▲</button><div></div><button class="btn-arr" onmousedown="mz(3)">◀</button><div></div><button class="btn-arr" onmousedown="mz(1)">▶</button><div></div><button class="btn-arr" onmousedown="mz(2)">▼</button><div></div></div><br><button class="red" onclick="xG()">EXIT</button></div>
<div id="ldU" class="h"><h2 style="color:#fff">LUNAR LANDER</h2><h3>FUEL: <span id="ldf">100</span></h3><div style="display:flex;justify-content:center;gap:5px"><button style="padding:20px" onmousedown="ld(3)">◀</button><button style="padding:20px;background:#fa0" onmousedown="ld(0)">🔥 THRUST</button><button style="padding:20px" onmousedown="ld(1)">▶</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
<script>
function syncTime() {
const now = Math.floor(Date.now() / 1000);
const offset = new Date().getTimezoneOffset() * -60;
fetch('/sync?t=' + (now + offset)).then(()=> {
document.getElementById('pstat').innerHTML += " | <span style='color:#0f0'>TIME SYNCED ✔</span>";
});
}
window.onload = syncTime;
setInterval(()=>{
if(document.getElementById('mU').style.display!=='none'){
fetch('/stat').then(r=>r.json()).then(d=>{
document.getElementById('t').innerText=d.t.toFixed(1);
document.getElementById('g').innerText=d.g;
document.getElementById('d').innerText=d.d;
document.getElementById('st').innerText=d.m;
if(d.f) document.getElementById('fStat').innerHTML = "<span style='color:#0f0'>FOCUSING...</span>";
else document.getElementById('fStat').innerText = "IDLE";
if(d.t>32)document.getElementById('cT').classList.add('a');else document.getElementById('cT').classList.remove('a');
if(d.g>2200)document.getElementById('cG').classList.add('a');else document.getElementById('cG').classList.remove('a');
if(d.d<25)document.getElementById('cD').classList.add('a');else document.getElementById('cD').classList.remove('a');
});
}
if(!document.getElementById('sU').classList.contains('h')){fetch('/snakeScore').then(r=>r.text()).then(s=>{document.getElementById('sc').innerText="SCORE: "+s});}
if(!document.getElementById('pU').classList.contains('h')){fetch('/pongScore').then(r=>r.text()).then(s=>{document.getElementById('ps').innerText=s});}
if(!document.getElementById('dU').classList.contains('h')){fetch('/dinoScore').then(r=>r.text()).then(s=>{document.getElementById('ds').innerText=s});}
if(!document.getElementById('iU').classList.contains('h')){fetch('/invScore').then(r=>r.text()).then(s=>{document.getElementById('is').innerText=s});}
if(!document.getElementById('tkU').classList.contains('h')){fetch('/bkScore').then(r=>r.text()).then(s=>{document.getElementById('bks').innerText=s});}
if(!document.getElementById('fbU').classList.contains('h')){fetch('/fbScore').then(r=>r.text()).then(s=>{document.getElementById('fbs').innerText=s});}
if(!document.getElementById('ldU').classList.contains('h')){fetch('/ldFuel').then(r=>r.text()).then(s=>{document.getElementById('ldf').innerText=s});}
},500);
// GAME LOGIC MAPPING
// lG = Load Game, xG = Exit Game
// sF = Start Focus, s = Set Mood
// tMv = TicTacToe Move
// nk = Snake Input (0=U,1=R,2=D,3=L)
// pg = Pong Input (-1=Up, 1=Down)
// dn = Dino Input (1=Jump)
// iv = Invaders Input (-1=L, 1=R, 10=Fire)
// bk = Breakout Input (-1=L, 1=R)
// fb = Flappy Input (1=Flap)
// mz = Maze Input (0=U,1=R,2=D,3=L)
// ld = Lander Input (0=Thrust, 3=Left, 1=Right)
function lG(id){
document.getElementById('mU').classList.add('h');
document.querySelectorAll('.game-ui').forEach(e=>e.classList.add('h')); // Hide all first
if(id==1){document.getElementById('tU').classList.remove('h');fetch('/game?g=1&act=start').then(uT);}
if(id==2){document.getElementById('sU').classList.remove('h');fetch('/game?g=2&act=start');}
if(id==3){document.getElementById('pU').classList.remove('h');fetch('/game?g=3&act=start');}
if(id==4){document.getElementById('dU').classList.remove('h');fetch('/game?g=4&act=start');}
if(id==5){document.getElementById('iU').classList.remove('h');fetch('/game?g=5&act=start');}
if(id==6){document.getElementById('tkU').classList.remove('h');fetch('/game?g=6&act=start');}
if(id==7){document.getElementById('fbU').classList.remove('h');fetch('/game?g=7&act=start');}
if(id==8){document.getElementById('mzU').classList.remove('h');fetch('/game?g=8&act=start');}
if(id==9){document.getElementById('ldU').classList.remove('h');fetch('/game?g=9&act=start');}
}
function xG(){
document.querySelectorAll('.h').forEach(e=>e.classList.add('h')); // Reset hidden
document.getElementById('mU').classList.remove('h'); // Show menu
// Hide all specific game UIs manually to be safe
const uis = ['tU','sU','pU','dU','iU','tkU','fbU','mzU','ldU'];
uis.forEach(id => { if(document.getElementById(id)) document.getElementById(id).classList.add('h'); });
fetch('/game?g=0&act=exit');
}
// Input Handlers
function s(m){fetch('/set?m='+m);}
function sF(a,mVal){
let m=mVal;
if(a==1 && m==0) m=document.getElementById('fm').value;
if(!m) m=25;
console.log("Focus:", a, m);
fetch('/focus?act='+a+'&m='+m);
}
// Game Inputs
function tMv(id){fetch('/game?g=1&act=move&c='+id).then(r=>r.json()).then(uT2);}
function uT(r){r.json().then(uT2);}
function uT2(d){for(let i=0;i<9;i++){let t="";if(d.b[i]==1)t="X";if(d.b[i]==2)t="O";document.getElementById('c'+i).innerText=t;}let m=document.getElementById('tM');if(d.s==1)m.innerText="YOU WIN!";else if(d.s==2)m.innerText="ROBOT WINS!";else m.innerText="YOUR TURN";}
function nk(d){console.log('Snake:',d); fetch('/game?g=2&act=input&d='+d);}
function pg(d){console.log('Pong:',d); fetch('/game?g=3&act=input&p='+myP+'&d='+d);}
function dn(d){console.log('Dino:',d); fetch('/game?g=4&act=input&d='+d);}
function iv(d){console.log('Invaders:',d); fetch('/game?g=5&act=input&d='+d);}
function bk(d){console.log('Breakout:',d); fetch('/game?g=6&act=input&d='+d);}
function fb(d){console.log('Flappy:',d); fetch('/game?g=7&act=input&d='+d);}
function mz(d){console.log('Maze:',d); fetch('/game?g=8&act=input&d='+d);}
function ld(d){console.log('Lander:',d); fetch('/game?g=9&act=input&d='+d);}
var myP=0;
function join(p){myP=p;document.getElementById('pstat').innerText="YOU ARE PLAYER "+p;
if(p==1)document.getElementById('bp1').style.borderColor="#0f0";
if(p==2)document.getElementById('bp2').style.borderColor="#0f0";
}
</script></body></html>
)rawliteral";
void drawFace(); void drawHappy(); void drawPanic(); void drawSus(); void drawSweat(); void drawAngry(); void drawLove(); void drawConfused();
void drawEyeV2(int cx, int cy, int r, float p, int lx, int ly, float closePercent, bool angryBrow);
void drawEyeMetric(int cx, int cy, int r, float pupil, int lx, int ly, float lidTopP, float lidBotP, EyeLidShape shape, bool glasses);
void drawTTT(); int checkTTTWin(); void initSnake(); void updateSnake(); void drawSnake(); void initPong(); void updatePong(); void drawPong();
void initDino(); void updateDino(); void drawDino(); void initInvaders(); void updateInvaders(); void drawInvaders();
void initBreakout(); void updateBreakout(); void drawBreakout();
void initFlappy(); void updateFlappy(); void drawFlappy();
void initMaze(); void updateMaze(); void drawMaze();
void initLander(); void updateLander(); void drawLander();
void drawClock();
void setup() {
Serial.begin(115200);
pinMode(PIN_BUZZER, OUTPUT); pinMode(PIN_MQ135, INPUT);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_TRIG, OUTPUT); pinMode(PIN_ECHO, INPUT);
dht.begin();
for(int i=0; i<GAS_SAMPLES; i++) gasReadings[i]=0;
Wire.begin(OLED_SDA, OLED_SCL); Wire.setClock(400000);
if(!display.begin(SCREEN_ADDRESS, true)) for(;;);
display.setRotation(2);
display.clearDisplay(); display.setTextSize(1); display.setTextColor(SH110X_WHITE); display.setCursor(30,30); display.println("SENTRY MODE..."); display.display();
gasBaseline = analogRead(PIN_MQ135);
delay(100);
float t_init = dht.readTemperature();
if(!isnan(t_init)) tempBaseline = t_init; else tempBaseline = 25.0;
WiFi.softAP(ssid, password);
server.on("/", [](){ server.send_P(200, "text/html", index_html); });
server.on("/stat", [](){ String json="{\"t\":"+String(temp)+",\"g\":"+String(gasLevel)+",\"d\":"+String(dist_cm)+",\"m\":\""+currentMood+"\",\"f\":"+String(isFocusMode)+"}"; server.send(200, "application/json", json); });
server.on("/sync", [](){ if(server.hasArg("t")) { unixTime = server.arg("t").toInt(); lastSyncMillis = millis(); timeSynced = true; } server.send(200, "text/plain", "OK"); });
server.on("/snakeScore", [](){ server.send(200, "text/plain", String(score)); });
server.on("/pongScore", [](){ server.send(200, "text/plain", String(p1Score)+" - "+String(p2Score)); });
server.on("/set", [](){ if(server.hasArg("m")) { currentMood=server.arg("m"); if(currentMood=="SLEEP") isSleeping=true; else {isSleeping=false; activeGame=0;} manualOverrideTimer=millis()+5000; } server.send(200, "text/plain", "OK"); });
server.on("/focus", [](){
if(server.hasArg("act")) {
int a=server.arg("act").toInt();
if(a==1 && server.hasArg("m")) { isFocusMode=true; focusDuration=server.arg("m").toInt()*60000; focusStartTime=millis(); isSleeping=false; lastMotionTime=millis(); activeGame=0; }
else { isFocusMode=false; }
}
server.send(200, "text/plain", "OK");
});
server.on("/game", [](){
int g=server.arg("g").toInt(); String act=server.arg("act"); activeGame=g;
if(act=="exit") { activeGame=0; }
if(g==1) {
if(act=="start") { ttt_status=0; for(int i=0;i<9;i++) ttt_board[i]=0; }
if(act=="move" && ttt_status==0) { int c=server.arg("c").toInt(); if(ttt_board[c]==0) { ttt_board[c]=1; if(checkTTTWin()==1) ttt_status=1; else { int move=-1; for(int i=0;i<50;i++){ int r=random(0,9); if(ttt_board[r]==0){move=r; break;} } if(move!=-1){ ttt_board[move]=2; if(checkTTTWin()==2) ttt_status=2; } } } }
String j="{\"s\":"+String(ttt_status)+",\"b\":["; for(int i=0;i<9;i++){ j+=String(ttt_board[i]); if(i<8)j+=","; } j+="]}"; server.send(200, "application/json", j); return;
}
if(g==2) { if(act=="start") { snakeLen=3; score=0; snakeDir=1; snakeGameOver=false; snakeX[0]=30; snakeY[0]=30; snakeX[1]=26; snakeY[1]=30; snakeX[2]=22; snakeY[2]=30; foodX=random(1,30)*4; foodY=random(1,15)*4; } if(act=="input") { int d=server.arg("d").toInt(); if(d==9) {snakeLen=3; score=0; snakeDir=1; snakeGameOver=false; snakeX[0]=30; snakeY[0]=30; snakeX[1]=26; snakeY[1]=30; snakeX[2]=22; snakeY[2]=30; foodX=random(1,30)*4; foodY=random(1,15)*4;} else if(d==0&&snakeDir!=2) snakeDir=0; else if(d==1&&snakeDir!=3) snakeDir=1; else if(d==2&&snakeDir!=0) snakeDir=2; else if(d==3&&snakeDir!=1) snakeDir=3; } }
if(g==3) { if(act=="start") {ballX=64;ballY=32;ballVX=2.5;ballVY=2.5;p1Score=0;p2Score=0;pongGameOver=false;paddle1Y=24;paddle2Y=24;} if(act=="input") { int p=server.arg("p").toInt(); int d=server.arg("d").toInt(); if(p==1)paddle1Y+=d*5; if(p==2)paddle2Y+=d*5; } }
if(g==4) { if(act=="start") initDino(); if(act=="input") { if(dinoGrounded) { dinoV = -6; dinoGrounded=false; } if(dinoOver) initDino(); } }
if(g==5) { if(act=="start") initInvaders(); if(act=="input") { int d=server.arg("d").toInt(); if(d==10 && bulletY==-1) { bulletX=shipX+3; bulletY=50; } else { shipX+=d*5; if(shipX<0)shipX=0; if(shipX>120)shipX=120; } if(invOver) initInvaders(); } }
if(g==6) { if(act=="start") initBreakout(); if(act=="input") { int d=server.arg("d").toInt(); if(d==-1)paddle1Y-=4; else paddle1Y+=4; } }
if(g==7) { if(act=="start") initFlappy(); if(act=="input") { dinoV = -3.5; } }
if(g==8) { if(act=="start") initMaze(); if(act=="input") { int d=server.arg("d").toInt(); if(d==0)mazeY-=2; if(d==1)mazeX+=2; if(d==2)mazeY+=2; if(d==3)mazeX-=2; } }
if(g==9) { if(act=="start") initLander(); if(act=="input") { int d=server.arg("d").toInt(); if(d==0)landVY-=0.5; if(d==1)landVX+=0.5; if(d==3)landVX-=0.5; landFuel-=2; } }
server.send(200, "text/plain", "OK");
});
server.on("/pongScore", [](){ server.send(200, "text/plain", String(p1Score)+" - "+String(p2Score)); });
server.on("/bkScore", [](){ server.send(200, "text/plain", String(score)); });
server.on("/fbScore", [](){ server.send(200, "text/plain", String(score)); });
server.on("/dinoScore", [](){ server.send(200, "text/plain", String(dinoScore)); });
server.on("/invScore", [](){ server.send(200, "text/plain", String(invScore)); });
server.on("/ldFuel", [](){ server.send(200, "text/plain", String((int)landFuel)); });
server.begin();
}
void loop() {
server.handleClient();
if(millis()-lastDHTRead>1000){float t=dht.readTemperature();if(!isnan(t))temp=t*10;
if(temp<10) temp+=20;
lastDHTRead=millis();}
if(millis()-lastDistRead>60) {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2); digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10); digitalWrite(PIN_TRIG, LOW);
long duration = pulseIn(PIN_ECHO, HIGH, 18000);
long new_dist = (duration>0) ? (duration*0.034/2) : 999;
long delta = new_dist - prev_dist_cm;
if(new_dist < 50 && delta < DIST_VELOCITY_TRIGGER && activeGame==0) {
if(currentMood != "PANIC") { currentMood="ANGRY"; manualOverrideTimer=millis()+2000; }
}
prev_dist_cm = dist_cm;
dist_cm = new_dist;
lastDistRead=millis();
}
int rawGas = analogRead(PIN_MQ135);
averageGas = (rawGas * 0.2) + (averageGas * 0.8);
gasLevel = averageGas;
if(millis() % 1000 == 0) {
if(averageGas < gasBaseline) gasBaseline--;
else if(averageGas > gasBaseline + 100) gasBaseline++;
}
bool pir = digitalRead(PIN_PIR);
if(pir == HIGH) lastMotionTime = millis();
if(isFocusMode) {
if(millis() - focusStartTime > focusDuration) { isFocusMode = false; showBigText=true; overrideText="DONE!"; overrideTextTimer=millis()+5000; }
else {
display.clearDisplay();
unsigned long timeElap = millis() - lastMotionTime;
if(timeElap > DISTRACTION_THRESHOLD) {
display.setTextSize(2); display.setCursor(10, 20);
int qIdx = (millis()/1000) % 8;
display.print(txt_MOTIVATION[qIdx]);
digitalWrite(PIN_BUZZER, (millis()%500 < 250) ? HIGH : LOW);
} else {
digitalWrite(PIN_BUZZER, LOW);
display.drawCircle(64, 32, 30, SH110X_WHITE);
float prog = (float)(millis() - focusStartTime) / (float)focusDuration;
int fillH = (int)(60 * prog);
int waterLevel = 32 + 30 - fillH;
float phase = (millis() / 200.0);
for(int x = 64-28; x < 64+28; x++) {
int waveY = waterLevel + sin((x * 0.2) + phase) * 2;
int R = 28;
int dx = x - 64;
if(abs(dx) >= R) continue;
int semiCircleH = sqrt(R*R - dx*dx);
int circleBottomY = 32 + semiCircleH;
int circleTopY = 32 - semiCircleH;
if(waveY < circleTopY) waveY = circleTopY;
if(waveY > circleBottomY) waveY = circleBottomY;
if(waveY < circleBottomY) {
display.drawFastVLine(x, waveY, circleBottomY - waveY, SH110X_WHITE);
}
}
if((millis()/50) % 4 == 0) {
int bx = random(64-20, 64+20);
int by = random(waterLevel, 32+25);
if(by > waterLevel + 4) display.drawPixel(bx, by, SH110X_INVERSE);
}
unsigned long remMs = focusDuration - (millis()-focusStartTime);
int remM = remMs / 60000;
int remS = (remMs % 60000) / 1000;
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(90, 54);
if(remM<10) display.print("0"); display.print(remM);
display.print(":");
if(remS<10) display.print("0"); display.print(remS);
}
display.display();
delay(50);
return;
}
}
if(!isSleeping && millis() - lastMotionTime > SLEEP_TIMEOUT && activeGame == 0) {
isSleeping = true;
currentMood = "SLEEP";
}
if(isSleeping) {
display.clearDisplay(); display.display();
if(pir == HIGH) {
isSleeping = false;
overrideText = "Hi Sir";
showBigText = true;
overrideTextTimer = millis() + 3000;
currentMood = "HAPPY";
}
delay(50);
return;
}
if(activeGame==0 && millis()>manualOverrideTimer) {
if(dist_cm < 18) {
currentMood = "PANIC";
}
else if(dist_cm < 40) {
currentMood = "SUS";
}
else {
currentMood = "HAPPY";
}
}
static int breathDir = 1; static unsigned long lastBreath = 0;
if(millis() - lastBreath > 150) { breathVal += breathDir; if(breathVal > 2) breathDir = -1; if(breathVal < 0) breathDir = 1; lastBreath = millis(); }
if(activeGame == 0 && !isSleeping) {
if(gasLevel > 2800) {
digitalWrite(PIN_BUZZER, (millis() % 200 < 100) ? HIGH : LOW);
}
else if(dist_cm < DIST_TOUCH) {
digitalWrite(PIN_BUZZER, (millis() % 100 < 50) ? HIGH : LOW);
}
else {
digitalWrite(PIN_BUZZER, LOW);
}
}
display.clearDisplay();
if(activeGame==0) {
unsigned long cycle = millis() % 37000;
if (cycle < 30000 || currentMood == "PANIC") {
if(millis() < overrideTextTimer) {
currentDisplayText = overrideText;
if(showBigText) {
display.setTextSize(2);
display.setCursor(10, 20);
display.print(currentDisplayText);
display.setTextSize(1);
display.display();
delay(50);
return;
}
}
else if(millis() - lastTextUpdate > 3000) {
showBigText = false;
lastTextUpdate = millis();
if(random(0,100) < 30) {
int r = random(0,3);
if(r==0) currentDisplayText = "It's " + String(temp,1) + "C in here";
else if(r==1) currentDisplayText = String("Air looks ") + (gasLevel<2000 ? "Clean" : "Kinda sus");
else currentDisplayText = String("Object at ") + (dist_cm>900 ? "far far away" : String(dist_cm)+"cm");
} else {
int idx = random(0, 8);
if(currentMood == "HAPPY") currentDisplayText = txt_HAPPY[idx];
else if(currentMood == "SUS") {
static int infoState = 0;
if(infoState == 0) currentDisplayText = "Wifi: " + String(ssid);
else if(infoState == 1) currentDisplayText = "IP: 192.168.4.1";
else currentDisplayText = "Open Browser & Play!";
infoState = (infoState + 1) % 3;
}
else if(currentMood == "PANIC") currentDisplayText = txt_PANIC[idx];
else if(currentMood == "ANGRY") currentDisplayText = txt_ANGRY[idx];
else if(currentMood == "SWEAT") currentDisplayText = txt_SWEAT[idx];
else if(currentMood == "LOVE") currentDisplayText = txt_LOVE[idx];
else if(currentMood == "CONFUSED") currentDisplayText = txt_CONFUSED[idx];
else currentDisplayText = txt_HAPPY[idx];
}
}
display.setCursor(0,0); display.setTextSize(1); display.print(currentDisplayText);
drawFace();
}
else {
drawClock();
}
} else if(activeGame==1) drawTTT(); else if(activeGame==2) {updateSnake();drawSnake();} else if(activeGame==3) {updatePong();drawPong();}
else if(activeGame==4) {updateDino();drawDino();} else if(activeGame==5) {updateInvaders();drawInvaders();}
else if(activeGame==6) {updateBreakout();drawBreakout();} else if(activeGame==7) {updateFlappy();drawFlappy();}
else if(activeGame==8) {drawMaze();} else if(activeGame==9) {updateLander();drawLander();}
display.display(); delay(50);
}
void drawFace() {
if(currentMood=="HAPPY") drawHappy();
else if(currentMood=="PANIC") drawPanic();
else if(currentMood=="SUS") drawSus();
else if(currentMood=="SWEAT") drawSweat();
else if(currentMood=="ANGRY") drawAngry();
else if(currentMood=="LOVE") drawLove();
else if(currentMood=="CONFUSED") drawConfused();
else drawHappy();
}
void drawEyeMetric(int cx, int cy, int r, float pupil, int lx, int ly, float lidTopP, float lidBotP, EyeLidShape shape, bool glasses) {
display.fillCircle(cx, cy, r, SH110X_WHITE);
int irisR = r - 3;
int ix = cx + lx; int iy = cy + ly;
display.fillCircle(ix, iy, irisR, SH110X_BLACK);
display.drawCircle(ix, iy, irisR-2, SH110X_WHITE);
if(pupil < 1) pupil = 1;
display.fillCircle(ix, iy, (int)pupil, SH110X_BLACK);
display.fillCircle(ix + irisR/3, iy - irisR/3, 2, SH110X_WHITE);
if(lidTopP > 0) {
int h = (int)(r * 2 * lidTopP);
int topY = cy - r - 2;
if(shape == LID_ANGRY) {
int yOffset = (cx < 64) ? 5 : -5;
bool isLeft = (cx < 64);
int inY = topY + h + 4; int outY = topY + h - 2;
if(!isLeft) { int t=inY; inY=outY; outY=t; }
if(isLeft) display.fillTriangle(cx-r-2, topY+h-3, cx+r+2, topY+h+5, cx-r, cy-r-5, SH110X_BLACK);
else display.fillTriangle(cx-r-2, topY+h+5, cx+r+2, topY+h-3, cx+r, cy-r-5, SH110X_BLACK);
} else if (shape == LID_SAD) {
bool isLeft = (cx < 64);
if(isLeft) display.fillTriangle(cx-r-2, topY+h+4, cx+r+2, topY+h-3, cx, cy-r-5, SH110X_BLACK);
else display.fillTriangle(cx-r-2, topY+h-3, cx+r+2, topY+h+4, cx, cy-r-5, SH110X_BLACK);
} else {
display.fillRect(cx - r - 2, topY, r*2 + 4, h, SH110X_BLACK);
}
}
if(lidBotP > 0) {
int h = (int)(r * 2 * lidBotP);
display.fillRect(cx - r - 2, cy + r - h + 2, r*2 + 4, h, SH110X_BLACK);
}
if(glasses) {
display.drawRect(cx - r - 4, cy - r - 4, r*2 + 8, r*2 + 8, SH110X_WHITE);
if(cx < 64) display.drawLine(cx+r+4, cy, cx+r+10, cy, SH110X_WHITE);
}
if(shape == LID_TIRED) {
display.drawLine(cx-r, cy+r+2, cx+r, cy+r+2, SH110X_WHITE);
}
}
void drawEyeV2(int cx, int cy, int r, float p, int lx, int ly, float closePercent, bool angryBrow) {
EyeLidShape s = angryBrow ? LID_ANGRY : LID_FLAT;
drawEyeMetric(cx, cy, r, p, lx, ly, closePercent, 0, s, false);
}
void drawHappy() {
if(millis() - lastBlink > 3500 + random(0, 2000)) {
eyesOpen = false;
if(millis() - lastBlink > 3650 + random(0, 2000)) { lastBlink = millis(); eyesOpen = true; }
}
float blink = eyesOpen ? 0.0 : 1.0;
static int lookX = 0; static int lookY = 0;
static unsigned long nextGazeMove = 0;
if(millis() > nextGazeMove) {
if(random(0,10) < 6) { lookX = random(-5,6); lookY = random(-3,4); }
else { lookX = 0; lookY = 0; }
nextGazeMove = millis() + random(1000, 4000);
}
static float curX = 0; static float curY = 0;
curX += (lookX - curX) * 0.1;
curY += (lookY - curY) * 0.1;
static bool singing = false; static unsigned long singTimer = 0;
if(millis() > singTimer) {
singing = !singing;
singTimer = millis() + (singing ? random(2000, 5000) : random(3000, 8000));
}
int mouthH = 0;
if(singing) {
float t = millis() / 150.0;
mouthH = abs(sin(t) * 5) + random(0, 3);
}
pupilSize += (3.5 - pupilSize) * 0.1;
int by = breathVal;
drawEyeV2(35+(int)curX, 25+by+(int)curY, 12, pupilSize, (int)curX, (int)curY, blink, false);
drawEyeV2(93+(int)curX, 25+by+(int)curY, 12, pupilSize, (int)curX, (int)curY, blink, false);
int mx = 64; int my = 45+by;
if(mouthH == 0) {
display.drawLine(mx-10, my, mx-5, my+3, SH110X_WHITE);
display.drawLine(mx-5, my+3, mx+5, my+3, SH110X_WHITE);
display.drawLine(mx+5, my+3, mx+10, my, SH110X_WHITE);
} else {
display.drawRect(mx-6, my, 12, mouthH, SH110X_WHITE);
display.fillRect(mx-4, my+1, 8, mouthH-2, SH110X_BLACK);
}
}
void drawPanic() {
pupilSize = 2.0;
int shiver = random(-1,2);
drawEyeV2(35+shiver, 25, 14, 1, 0, 0, 0.0, false);
drawEyeV2(93+shiver, 25, 14, 1, 0, 0, 0.0, false);
display.drawRect(54+shiver, 45, 20, 10, SH110X_WHITE); display.fillRect(57+shiver, 47, 14, 6, SH110X_BLACK);
}
void drawSus() {
static int lx = 6;
drawEyeV2(35, 25, 12, 3, lx, 0, 0.6, true);
drawEyeV2(93, 25, 12, 3, lx, 0, 0.6, true);
display.drawLine(54, 50, 74, 50, SH110X_WHITE);
}
void drawAngry() {
drawEyeV2(35, 25, 12, 5, 0, 0, 0.2, true);
drawEyeV2(93, 25, 12, 5, 0, 0, 0.2, true);
display.drawTriangle(54, 55, 64, 48, 74, 55, SH110X_WHITE);
}
void drawLove() {
int cy = 25+breathVal;
display.fillCircle(30, cy, 6, SH110X_WHITE); display.fillCircle(40, cy, 6, SH110X_WHITE); display.fillTriangle(24, cy+2, 46, cy+2, 35, cy+14, SH110X_WHITE);
display.fillCircle(88, cy, 6, SH110X_WHITE); display.fillCircle(98, cy, 6, SH110X_WHITE); display.fillTriangle(82, cy+2, 104, cy+2, 93, cy+14, SH110X_WHITE);
display.fillCircle(64, 48+breathVal, 4, SH110X_WHITE);
}
void drawConfused() {
drawEyeV2(35, 25, 14, 3, 0, -2, 0.0, false);
drawEyeV2(93, 25, 10, 3, 0, 2, 0.5, false);
int y = 50; display.drawLine(54, y, 59, y-3, SH110X_WHITE); display.drawLine(59, y-3, 64, y+3, SH110X_WHITE); display.drawLine(64, y+3, 69, y-3, SH110X_WHITE); display.drawLine(69, y-3, 74, y, SH110X_WHITE);
}
void drawSweat() {
static int dropY = 15; dropY += 2; if(dropY > 40) dropY = 15;
drawEyeV2(35, 27, 12, 3, 0, 2, 0.4, false); drawEyeV2(93, 27, 12, 3, 0, 2, 0.4, false);
display.fillCircle(110, dropY, 3, SH110X_WHITE); display.fillTriangle(107, dropY, 113, dropY, 110, dropY-5, SH110X_WHITE);
display.drawLine(58, 52, 70, 52, SH110X_WHITE);
}
void drawTTT() { int sx=32;int w=64;int step=21;display.drawLine(sx+step,0,sx+step,63,SH110X_WHITE);display.drawLine(sx+step*2,0,sx+step*2,63,SH110X_WHITE);display.drawLine(sx,step,sx+w,step,SH110X_WHITE);display.drawLine(sx,step*2,sx+w,step*2,SH110X_WHITE);for(int i=0;i<9;i++){if(ttt_board[i]==0)continue;int r=i/3;int c=i%3;int cx=sx+c*step+10;int cy=r*step+10;if(ttt_board[i]==1){display.drawLine(cx-4,cy-4,cx+4,cy+4,SH110X_WHITE);display.drawLine(cx+4,cy-4,cx-4,cy+4,SH110X_WHITE);}else display.drawCircle(cx,cy,5,SH110X_WHITE);} }
int checkTTTWin() { int w[8][3]={{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}}; for(int i=0;i<8;i++){int a=w[i][0],b=w[i][1],c=w[i][2];if(ttt_board[a]!=0&&ttt_board[a]==ttt_board[b]&&ttt_board[a]==ttt_board[c])return ttt_board[a];} return 0; }
void updateSnake() { if(snakeGameOver||millis()-lastSnakeMove<150)return; lastSnakeMove=millis(); for(int i=snakeLen-1;i>0;i--){snakeX[i]=snakeX[i-1];snakeY[i]=snakeY[i-1];} if(snakeDir==0)snakeY[0]-=4; if(snakeDir==1)snakeX[0]+=4; if(snakeDir==2)snakeY[0]+=4; if(snakeDir==3)snakeX[0]-=4; if(snakeX[0]<0||snakeX[0]>124||snakeY[0]<0||snakeY[0]>60){snakeGameOver=true;digitalWrite(PIN_BUZZER,HIGH);delay(100);digitalWrite(PIN_BUZZER,LOW);} for(int i=1;i<snakeLen;i++)if(snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i])snakeGameOver=true; if(abs(snakeX[0]-foodX)<4&&abs(snakeY[0]-foodY)<4){snakeLen++;score+=10;if(snakeLen>=SNAKE_MAX_LEN)snakeLen=SNAKE_MAX_LEN-1;foodX=random(1,30)*4;foodY=random(1,15)*4;digitalWrite(PIN_BUZZER,HIGH);delay(20);digitalWrite(PIN_BUZZER,LOW);} }
void drawSnake() { display.drawRect(0,0,128,64,SH110X_WHITE); if(snakeGameOver){display.setCursor(35,25);display.setTextSize(1);display.print("GAME OVER");display.setCursor(40,35);display.print("SCORE:");display.print(score);return;} for(int i=0;i<snakeLen;i++)display.fillRect(snakeX[i],snakeY[i],3,3,SH110X_WHITE); display.drawCircle(foodX+1,foodY+1,2,SH110X_WHITE); }
void initSnake() { snakeLen=3; score=0; snakeDir=1; snakeGameOver=false; snakeX[0]=30; snakeY[0]=30; snakeX[1]=26; snakeY[1]=30; snakeX[2]=22; snakeY[2]=30; foodX=random(1,30)*4; foodY=random(1,15)*4; }
void initPong() { ballX=64;ballY=32;ballVX=2.5;ballVY=2.5;p1Score=0;p2Score=0;pongGameOver=false;paddle1Y=24;paddle2Y=24; }
void updatePong() {
if(pongGameOver)return; ballX+=ballVX;ballY+=ballVY;
if(ballY<=0||ballY>=64)ballVY*=-1;
if(ballX<=5){if(abs(ballY-(paddle1Y+8))<12){ballVX*=-1;ballX=6;digitalWrite(PIN_BUZZER,HIGH);delay(10);digitalWrite(PIN_BUZZER,LOW);}else{p2Score++;ballX=64;ballY=32;ballVX=2;}}
if(ballY > paddle2Y + 6) paddle2Y += 2;
else if(ballY < paddle2Y + 6) paddle2Y -= 2;
if(ballX>=118){if(abs(ballY-(paddle2Y+8))<12){ballVX*=-1;ballX=117;digitalWrite(PIN_BUZZER,HIGH);delay(10);digitalWrite(PIN_BUZZER,LOW);}else{p1Score++;ballX=64;ballY=32;ballVX=-2;}}
if(p1Score>4 || p2Score>4) pongGameOver=true;
}
void drawPong() {
if(pongGameOver){display.setCursor(35,25);display.print("GAME OVER");display.setCursor(20,35);display.print("P1: "+String(p1Score)+" P2: "+String(p2Score));return;}
display.drawRect(0,0,128,64,SH110X_WHITE); for(int i=0;i<64;i+=4)display.drawPixel(64,i,SH110X_WHITE);
display.fillRect(2,paddle1Y,3,16,SH110X_WHITE); display.fillRect(123,paddle2Y,3,16,SH110X_WHITE);
display.fillCircle((int)ballX,(int)ballY,2,SH110X_WHITE);
}
void initDino() { dinoY=50; dinoV=0; dinoGrounded=true; cactusX=128; dinoScore=0; dinoOver=false; }
void updateDino() {
if(dinoOver) return;
dinoY += (int)dinoV;
dinoV += 0.6;
if(dinoY >= 50) { dinoY=50; dinoV=0; dinoGrounded=true; }
cactusX -= 4;
if(cactusX < -10) { cactusX = 128 + random(0,40); dinoScore++; digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW); }
if(abs(10-cactusX)<5 && dinoY>40) { dinoOver=true; digitalWrite(PIN_BUZZER,HIGH); delay(500); digitalWrite(PIN_BUZZER,LOW); }
}
void drawDino() {
if(dinoOver) { display.setCursor(40,30); display.print("GAME OVER"); display.setCursor(45,40); display.print(dinoScore); return; }
display.drawLine(0,60,128,60,SH110X_WHITE);
display.fillRect(10, dinoY, 8, 10, SH110X_WHITE);
display.fillRect(cactusX, 52, 6, 8, SH110X_WHITE);
display.setCursor(110,0); display.print(dinoScore);
}
void initInvaders() {
shipX=64; bulletX=-1; bulletY=-1; invScore=0; invOver=false; invDir=1;
for(int i=0;i<5;i++) { enemyX[i]=10 + i*20; enemyY[i]=5; enemyAlive[i]=true; }
}
void updateInvaders() {
if(invOver) return;
if(bulletY != -1) {
bulletY -= 5;
if(bulletY < 0) bulletY = -1;
for(int i=0;i<5;i++) {
if(enemyAlive[i] && abs(bulletX-enemyX[i])<6 && abs(bulletY-enemyY[i])<6) {
enemyAlive[i]=false; bulletY=-1; invScore++; digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW);
}
}
}
if(millis() - lastInvMove > 200) {
bool drop = false;
for(int i=0;i<5;i++) {
if(enemyAlive[i]) {
enemyX[i] += invDir * 4;
if(enemyX[i] > 120 || enemyX[i] < 5) drop=true;
}
}
if(drop) { invDir *= -1; for(int i=0;i<5;i++) enemyY[i]+=4; }
lastInvMove=millis();
bool allDead = true; for(int i=0;i<5;i++) if(enemyAlive[i]) allDead=false;
if(allDead) { for(int i=0;i<5;i++) { enemyX[i]=10 + i*20; enemyY[i]=5; enemyAlive[i]=true; } }
for(int i=0;i<5;i++) if(enemyAlive[i] && enemyY[i] > 50) invOver=true;
}
}
void drawInvaders() {
if(invOver) { display.setCursor(40,30); display.print("GAME OVER"); return; }
display.fillTriangle(shipX, 60, shipX-4, 63, shipX+4, 63, SH110X_WHITE);
if(bulletY != -1) display.drawLine(bulletX, bulletY, bulletX, bulletY+3, SH110X_WHITE);
for(int i=0;i<5;i++) {
if(enemyAlive[i]) {
display.fillRect(enemyX[i]-3, enemyY[i]-3, 6, 6, SH110X_WHITE);
if((millis()/500)%2==0) { display.drawPixel(enemyX[i]-2,enemyY[i]+4,SH110X_WHITE); display.drawPixel(enemyX[i]+2,enemyY[i]+4,SH110X_WHITE); }
else { display.drawPixel(enemyX[i]-1,enemyY[i]+4,SH110X_WHITE); display.drawPixel(enemyX[i]+1,enemyY[i]+4,SH110X_WHITE); }
}
}
}
void initBreakout() {
paddle1Y=50;
bBallX=64; bBallY=40; bVelX=2; bVelY=-2; score=0;
for(int r=0;r<5;r++) for(int c=0;c<10;c++) brk[r][c]=true;
}
void updateBreakout() {
if(score==50) return;
bBallX += bVelX; bBallY += bVelY;
if(bBallX<=0 || bBallX>=128) bVelX*=-1;
if(bBallY<=0) bVelY*=-1;
if(bBallY>=64) { initBreakout(); score=0; }
if(bBallY>=60 && bBallY<=62 && bBallX>=paddle1Y && bBallX<=paddle1Y+20) {
bVelY*=-1; float hitPt = bBallX - (paddle1Y+10); bVelX = hitPt * 0.3;
digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW);
}
int brC = bBallX / 13; int brR = bBallY / 6;
if(brR >=0 && brR < 5 && brC >= 0 && brC < 10) {
if(brk[brR][brC]) {
brk[brR][brC]=false; score++; bVelY*=-1;
digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW);
}
}
}
void drawBreakout() {
if(score==50) { display.setCursor(40,30); display.print("WINNER!"); return; }
display.fillRect(paddle1Y, 61, 20, 3, SH110X_WHITE);
display.fillCircle((int)bBallX, (int)bBallY, 2, SH110X_WHITE);
for(int r=0;r<5;r++) for(int c=0;c<10;c++) {
if(brk[r][c]) display.fillRect(c*13, r*6, 12, 5, SH110X_WHITE);
}
display.setCursor(0,0); display.print(score);
}
void initFlappy() { dinoY=32; dinoV=0; pipeX=128; pipeGap=20; score=0; dinoOver=false; }
void updateFlappy() {
if(dinoOver) { if(millis()-lastInvMove>1000) initFlappy(); return; }
dinoV += 0.4;
dinoY += (int)dinoV;
pipeX -= 2;
if(pipeX < -10) { pipeX=128; pipeGap = random(10, 40); score++; digitalWrite(PIN_BUZZER,HIGH); delay(20); digitalWrite(PIN_BUZZER,LOW); }
if(dinoY>64 || dinoY<0) dinoOver=true;
if(pipeX<14 && pipeX>0) {
if(dinoY < pipeGap || dinoY > pipeGap+25) {
dinoOver=true; digitalWrite(PIN_BUZZER,HIGH); delay(200); digitalWrite(PIN_BUZZER,LOW); lastInvMove=millis();
}
}
}
void drawFlappy() {
if(dinoOver) { display.setCursor(40,30); display.print("GAME OVER"); display.setCursor(55,40); display.print(score); return; }
display.fillCircle(10, dinoY, 3, SH110X_WHITE);
display.fillRect(pipeX, 0, 10, pipeGap, SH110X_WHITE);
display.fillRect(pipeX, pipeGap+25, 10, 64-(pipeGap+25), SH110X_WHITE);
display.setCursor(110,0); display.print(score);
}
void initMaze() { mazeX=2; mazeY=2; }
void drawMaze() {
for(int x=0;x<128;x+=16) display.drawLine(x,0,x,64,SH110X_WHITE);
for(int y=0;y<64;y+=16) display.drawLine(0,y,128,y,SH110X_WHITE);
for(int x=0;x<8;x++) for(int y=0;y<4;y++) {
if(((x+y)%3)==0) display.fillRect(x*16+2, y*16+2, 12, 12, SH110X_WHITE);
}
display.fillCircle(mazeX, mazeY, 2, SH110X_INVERSE);
display.setTextSize(1); display.setCursor(120,55); display.print("G");
if(abs(mazeX-120)<5 && abs(mazeY-55)<5) { display.setCursor(40,30); display.print("WIN!"); }
}
void initLander() { landX=64; landY=10; landVX=0; landVY=0; landFuel=100; landCrashed=false; landLanded=false; }
void updateLander() {
if(landCrashed || landLanded) return;
landVY += 0.05;
landX += landVX; landY += landVY;
if(landY >= 60) {
if(landVY > 1.5 || abs(landVX) > 1.0) { landCrashed=true; digitalWrite(PIN_BUZZER,HIGH); delay(500); digitalWrite(PIN_BUZZER,LOW); }
else { landLanded=true; }
landY = 60; landVY=0; landVX=0;
}
}
void drawLander() {
if(landCrashed) { display.setCursor(40,30); display.print("CRASH!"); return; }
if(landLanded) { display.setCursor(40,30); display.print("LANDED!"); return; }
display.drawLine(0,63,128,63,SH110X_WHITE);
display.drawRect(50, 63, 28, 1, SH110X_WHITE);
display.drawRect((int)landX-3, (int)landY-3, 6, 6, SH110X_WHITE);
display.drawLine((int)landX-3, (int)landY+3, (int)landX-5, (int)landY+6, SH110X_WHITE);
display.drawLine((int)landX+3, (int)landY+3, (int)landX+5, (int)landY+6, SH110X_WHITE);
display.setCursor(0,0); display.print("F:"); display.print((int)landFuel);
display.setCursor(100,0); display.print("v:"); display.print(landVY);
}
void drawClock() {
unsigned long currentUnix = unixTime + (millis() - lastSyncMillis) / 1000;
int h_raw = (currentUnix / 3600) % 24;
int m = (currentUnix / 60) % 60;
int s = (currentUnix) % 60;
bool isPm = (h_raw >= 12);
int h = h_raw % 12;
if(h == 0) h = 12;
for(int x=0; x<128; x+=8) for(int y=0; y<64; y+=8) display.drawPixel(x,y,SH110X_WHITE);
display.drawRect(0, 0, 128, 64, SH110X_WHITE);
display.fillRect(0, 0, 128, 11, SH110X_WHITE);
display.setTextColor(SH110X_BLACK);
display.setTextSize(1);
display.setCursor(4, 2);
display.print(timeSynced ? "SYNC" : "NC");
display.setCursor(85, 2);
display.print(String(temp,0) + "C");
display.setTextColor(SH110X_WHITE);
display.setTextSize(4);
int tx = 8; int ty = 18;
display.setCursor(tx, ty);
if(h < 10) display.print("0"); display.print(h);
display.print(":");
if(m < 10) display.print("0"); display.print(m);
display.setTextSize(1);
display.setCursor(110, 40);
display.print(isPm ? "PM" : "AM");
int barW = map(s, 0, 59, 0, 120);
display.drawRect(4, 54, 120, 6, SH110X_WHITE);
display.fillRect(4, 54, barW, 6, SH110X_WHITE);
display.drawLine(0,0,5,0,SH110X_WHITE); display.drawLine(0,0,0,5,SH110X_WHITE);
}