// ╔══════════════════════════════════════════════════════════════╗
// ║ ZERO RING — WOKWI SIMULATOR (SINGLE FILE) ║
// ║ ESP32 + SSD1306 OLED 128x64 + MPU6050 + Buzzer + Gemini ║
// ╚══════════════════════════════════════════════════════════════╝
//
// HOW TO GET YOUR FREE GEMINI API KEY:
// 1. Go to: https://aistudio.google.com
// 2. Sign in with any Google account
// 3. Click "Get API Key" -> Create API Key
// 4. Paste it below where it says PASTE_YOUR_KEY_HERE
// FREE - No credit card. Uses gemini-2.5-flash (1500 req/day free)
//
// WIRING (matches diagram.json exactly):
//
// OLED SSD1306 128x64:
// VIN <- esp:3V3
// GND <- esp:GND.1
// DATA <- esp:D21 (SDA)
// CLK <- esp:D22 (SCL)
//
// MPU6050:
// VCC <- esp:3V3
// GND <- esp:GND.1
// SDA <- esp:D21
// SCL <- esp:D22
// AD0 <- esp:GND.1 (sets I2C address to 0x68)
//
// BUZZER (passive):
// 1 <- esp:D18
// 2 <- esp:GND.2
//
// diagram.json:
// {
// "version": 1,
// "author": "Zero Ring",
// "editor": "wokwi",
// "parts": [
// { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 120, "left": 0, "attrs": {} },
// { "type": "wokwi-ssd1306", "id": "oled1", "top": -100, "left": -120, "attrs": { "i2cAddress": "0x3c" } },
// { "type": "wokwi-mpu6050", "id": "mpu1", "top": -100, "left": 160, "attrs": {} },
// { "type": "wokwi-buzzer", "id": "bz1", "top": 260, "left": 220, "attrs": { "volume": "0.4" } }
// ],
// "connections": [
// [ "esp:3V3", "oled1:VIN", "red", [] ],
// [ "esp:GND.1", "oled1:GND", "black", [] ],
// [ "esp:D21", "oled1:DATA", "blue", [] ],
// [ "esp:D22", "oled1:CLK", "green", [] ],
// [ "esp:3V3", "mpu1:VCC", "red", [] ],
// [ "esp:GND.1", "mpu1:GND", "black", [] ],
// [ "esp:D21", "mpu1:SDA", "blue", [] ],
// [ "esp:D22", "mpu1:SCL", "green", [] ],
// [ "esp:GND.1", "mpu1:AD0", "black", [] ],
// [ "esp:D18", "bz1:1", "orange", [] ],
// [ "esp:GND.2", "bz1:2", "black", [] ]
// ]
// }
//
// LIBRARIES — add in Wokwi via libraries.txt:
// Adafruit SSD1306
// Adafruit GFX Library
// Adafruit MPU6050
// Adafruit BusIO
// ArduinoJson
//
// HOW TO USE:
// Run in Wokwi -> open Serial Monitor
// Type any message + Enter -> Gemini AI replies
// Type 'sensor' -> print MPU6050 data
// Type 'happy' / 'sleep' -> change face
// Shake MPU6050 slider -> double-tap demo + buzzer
// ==============================================================
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// ===== CONFIG - EDIT THIS =====
const char* GEMINI_API_KEY = "AIzaSyAEMCLinDoOxDAvNl-XRROgfGjq9seWzTk";
// ==============================
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
// GPIO numbers match esp:D21, esp:D22, esp:D18 in diagram.json
#define SDA_PIN 21
#define SCL_PIN 22
#define BUZZER_PIN 18
#define SCREEN_W 128
#define SCREEN_H 64
#define OLED_ADDR 0x3C // matches "i2cAddress": "0x3c" in diagram.json
// -1 = no hardware RESET pin (not wired in diagram.json)
Adafruit_SSD1306 oled(SCREEN_W, SCREEN_H, &Wire, -1);
Adafruit_MPU6050 mpu;
bool mpuOK = false;
bool wifiOK = false;
enum AppState { S_IDLE, S_THINKING, S_REPLY, S_GESTURE, S_ERROR };
AppState appState = S_IDLE;
unsigned long stateTimer = 0;
String currentReply = "";
String serialBuf = "";
unsigned long lastBlinkAt = 0;
bool blinkClosed = false;
float lastAz = 0;
unsigned long lastTapMs = 0;
// ===== BUZZER =====
void buzzerTone(int hz, int ms) {
ledcAttach(BUZZER_PIN, hz, 8);
ledcWrite(BUZZER_PIN, 127);
delay(ms);
ledcWrite(BUZZER_PIN, 0);
ledcDetach(BUZZER_PIN);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
}
void beepBoot() { buzzerTone(700,70); delay(40); buzzerTone(1100,70); delay(40); buzzerTone(1600,120); }
void beepWake() { buzzerTone(900,60); delay(35); buzzerTone(1300,80); }
void beepReply() { buzzerTone(1500,50); delay(30); buzzerTone(1100,50); delay(30); buzzerTone(700,90); }
void beepGesture() { buzzerTone(500,40); delay(20); buzzerTone(900,40); delay(20); buzzerTone(1200,60); }
void beepError() { buzzerTone(300,250); delay(80); buzzerTone(250,300); }
// ===== OLED FACES =====
void faceHappy() {
if (blinkClosed) return;
oled.clearDisplay();
oled.fillRoundRect(14, 18, 36, 20, 7, SSD1306_WHITE);
oled.fillRoundRect(78, 18, 36, 20, 7, SSD1306_WHITE);
oled.drawLine(32, 50, 42, 58, SSD1306_WHITE);
oled.drawLine(42, 58, 86, 58, SSD1306_WHITE);
oled.drawLine(86, 58, 96, 50, SSD1306_WHITE);
oled.display();
}
void faceThinking() {
oled.clearDisplay();
oled.fillRoundRect(14, 24, 36, 8, 3, SSD1306_WHITE);
oled.fillRoundRect(78, 18, 36, 20, 7, SSD1306_WHITE);
int d = (millis() / 400) % 4;
for (int i = 0; i < d; i++) oled.fillCircle(38 + i*18, 56, 4, SSD1306_WHITE);
oled.display();
}
void faceListening() {
oled.clearDisplay();
oled.fillCircle(32, 26, 14, SSD1306_WHITE);
oled.fillCircle(96, 26, 14, SSD1306_WHITE);
int t = (millis() / 100) % 5;
int barH[5] = {8, 14, 22, 14, 8};
for (int i = 0; i < 5; i++) {
int h = barH[(i + t) % 5];
int x = 34 + i * 12;
oled.fillRect(x, 64 - h, 9, h, SSD1306_WHITE);
}
oled.display();
}
void faceExcited() {
oled.clearDisplay();
oled.fillRoundRect(8, 12, 44, 32, 10, SSD1306_WHITE);
oled.fillRoundRect(76, 12, 44, 32, 10, SSD1306_WHITE);
oled.drawRoundRect(48, 52, 32, 12, 5, SSD1306_WHITE);
oled.display();
}
void faceBlink() {
oled.clearDisplay();
oled.fillRect(14, 28, 36, 4, SSD1306_WHITE);
oled.fillRect(78, 28, 36, 4, SSD1306_WHITE);
oled.display();
}
void faceSleeping() {
oled.clearDisplay();
oled.fillRect(14, 28, 36, 4, SSD1306_WHITE);
oled.fillRect(78, 28, 36, 4, SSD1306_WHITE);
int zy = 42 - ((millis() / 500) % 14);
oled.setTextSize(1); oled.setCursor(58, zy + 8); oled.print("z");
oled.setTextSize(2); oled.setCursor(64, zy - 4); oled.print("Z");
oled.display();
}
void faceGesture() {
oled.clearDisplay();
oled.setTextSize(2); oled.setCursor(22, 8); oled.print("TAP!");
oled.setTextSize(1); oled.setCursor(8, 36); oled.print("Double tap OK!");
oled.setCursor(8, 52); oled.print("Gesture detected");
oled.display();
}
void showReply(String txt) {
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 0); oled.print(">> AI:");
oled.setCursor(0, 10);
oled.setTextWrap(true);
if ((int)txt.length() > 100) txt = txt.substring(0, 97) + "...";
oled.print(txt);
oled.display();
}
void showBoot() {
oled.clearDisplay();
oled.setTextSize(2); oled.setCursor(14, 8); oled.print("ZERO");
oled.setCursor(14, 30); oled.print("RING");
oled.setTextSize(1); oled.setCursor(10, 54); oled.print("Wokwi Simulator");
oled.display();
}
void showWifiConnect(int d) {
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 8); oled.print("Connecting WiFi");
oled.setCursor(0, 22); oled.print(WIFI_SSID);
oled.setCursor(0, 40); oled.print("Please wait");
oled.setCursor(0, 54);
for (int i = 0; i < d; i++) oled.print(".");
oled.display();
}
void showWifiOK() {
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(8, 8); oled.print("WiFi Connected!");
oled.setCursor(8, 22); oled.print("Gemini AI Ready");
oled.setCursor(8, 38); oled.print("Type in Serial");
oled.setCursor(8, 50); oled.print("Monitor to chat");
oled.display();
}
// ===== BLINK (non-blocking) =====
void handleBlink() {
if (appState != S_IDLE) return;
unsigned long now = millis();
if (!blinkClosed && now - lastBlinkAt > 4500) {
blinkClosed = true;
lastBlinkAt = now;
faceBlink();
} else if (blinkClosed && now - lastBlinkAt > 100) {
blinkClosed = false;
lastBlinkAt = now;
}
}
// ===== DOUBLE-TAP DETECTION =====
bool checkDoubleTap() {
if (!mpuOK) return false;
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float az = a.acceleration.z;
float delta = fabsf(az - lastAz);
lastAz = az;
if (delta > 5.5f) {
unsigned long now = millis();
if (lastTapMs != 0 && (now - lastTapMs) < 700) {
lastTapMs = 0;
return true;
}
lastTapMs = now;
}
return false;
}
void printSensorData() {
if (!mpuOK) { Serial.println("[MPU] Sensor not available"); return; }
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
Serial.println("+----- MPU6050 Sensor Data -----------");
Serial.printf("| Accel X:%6.2f Y:%6.2f Z:%6.2f m/s2\n",
a.acceleration.x, a.acceleration.y, a.acceleration.z);
Serial.printf("| Gyro X:%6.2f Y:%6.2f Z:%6.2f deg/s\n",
g.gyro.x, g.gyro.y, g.gyro.z);
Serial.printf("| Temp %.1f C\n", temp.temperature);
Serial.println("+-------------------------------------");
}
// ===== GEMINI API CALL =====
String askGemini(const String& userMsg) {
String apiKey = String(GEMINI_API_KEY);
if (apiKey == "PASTE_YOUR_KEY_HERE" || apiKey.length() < 10) {
return "No API key! Edit GEMINI_API_KEY at top of sketch.";
}
if (!wifiOK || WiFi.status() != WL_CONNECTED) {
return "WiFi offline. Reconnect and restart.";
}
String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + apiKey;
HTTPClient http;
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.setTimeout(20000);
String sysPrompt = "You are ZERO, an AI inside a wearable smart ring. Reply in under 35 words. Be helpful and friendly.";
String fullPrompt = sysPrompt + " User says: " + userMsg;
fullPrompt.replace("\\", "\\\\");
fullPrompt.replace("\"", "\\\"");
fullPrompt.replace("\n", "\\n");
fullPrompt.replace("\r", "");
String body = "{\"contents\":[{\"parts\":[{\"text\":\"" + fullPrompt + "\"}]}],"
"\"generationConfig\":{\"maxOutputTokens\":120,\"temperature\":0.75}}";
Serial.println("[AI] Sending to Gemini 2.5 Flash...");
int code = http.POST(body);
String raw = http.getString();
http.end();
Serial.printf("[AI] HTTP response code: %d\n", code);
if (code != 200) {
Serial.println("[AI] Error body: " + raw.substring(0, 300));
if (code == 400) return "Bad request. Is your API key correct?";
if (code == 403) return "API key rejected. Check aistudio.google.com";
if (code == 429) return "Rate limit hit. Wait a moment then retry.";
return "HTTP error " + String(code);
}
DynamicJsonDocument doc(4096);
DeserializationError err = deserializeJson(doc, raw);
if (err) {
Serial.println("[AI] JSON parse error: " + String(err.c_str()));
return "JSON error. Try again.";
}
JsonVariant tv = doc["candidates"][0]["content"]["parts"][0]["text"];
if (tv.isNull()) {
Serial.println("[AI] No text in response:");
Serial.println(raw.substring(0, 300));
return "No text in AI response.";
}
String reply = tv.as<String>();
reply.trim();
return reply;
}
// ===== SETUP =====
void setup() {
Serial.begin(115200);
delay(400);
Serial.println();
Serial.println("╔══════════════════════════════════╗");
Serial.println("║ ZERO RING - Wokwi Simulator ║");
Serial.println("║ Gemini 2.5 Flash (Free API) ║");
Serial.println("╚══════════════════════════════════╝");
Serial.println();
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// I2C on GPIO 21 (SDA / oled1:DATA / mpu1:SDA)
// GPIO 22 (SCL / oled1:CLK / mpu1:SCL)
Wire.begin(SDA_PIN, SCL_PIN);
Wire.setClock(400000);
// OLED — powered via oled1:VIN (3V3), so use SSD1306_EXTERNALVCC
if (!oled.begin(SSD1306_EXTERNALVCC, OLED_ADDR)) {
Serial.println("[OLED] INIT FAILED! Check DATA=D21 CLK=D22 VIN=3V3 wiring.");
} else {
Serial.println("[OLED] OK - 128x64 at 0x3C");
oled.setTextColor(SSD1306_WHITE);
oled.clearDisplay();
showBoot();
}
// MPU6050 — AD0 tied to GND.1 -> address 0x68
if (!mpu.begin()) {
Serial.println("[MPU6050] INIT FAILED! Check SDA=D21 SCL=D22 AD0=GND wiring.");
mpuOK = false;
} else {
Serial.println("[MPU6050] OK - address 0x68");
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
mpuOK = true;
}
beepBoot();
delay(1200);
// WiFi — channel 6 works well for Wokwi-GUEST
Serial.println("[WiFi] Connecting to Wokwi-GUEST...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD, 6);
int dots = 0;
unsigned long t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 18000) {
showWifiConnect((dots % 4) + 1);
dots++;
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
wifiOK = true;
Serial.println("\n[WiFi] Connected! IP: " + WiFi.localIP().toString());
showWifiOK();
beepWake();
delay(2200);
} else {
wifiOK = false;
Serial.println("\n[WiFi] FAILED. AI chat disabled. Sensors still work.");
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 10); oled.print("WiFi FAILED");
oled.setCursor(0, 28); oled.print("Sensor-only mode");
oled.display();
beepError();
delay(2000);
}
appState = S_IDLE;
stateTimer = millis();
lastBlinkAt = millis();
faceHappy();
Serial.println();
Serial.println("COMMANDS:");
Serial.println(" Type anything + Enter -> Ask Gemini AI");
Serial.println(" sensor -> Print MPU6050 data");
Serial.println(" happy -> Happy face");
Serial.println(" sleep -> Sleep face");
Serial.println(" excited -> Excited face");
Serial.println(" listen -> Listening face");
Serial.println(" Shake MPU slider -> Double-tap demo");
Serial.println();
Serial.println("Ready! Type your message...");
Serial.println();
}
// ===== LOOP =====
void loop() {
unsigned long now = millis();
// -- READ SERIAL INPUT --
while (Serial.available()) {
char c = (char)Serial.read();
if (c == '\n' || c == '\r') {
String line = serialBuf;
serialBuf = "";
line.trim();
if (line.length() == 0) continue;
if (line.equalsIgnoreCase("sensor")) {
printSensorData(); continue;
}
if (line.equalsIgnoreCase("happy")) {
appState = S_IDLE; blinkClosed = false; faceHappy();
Serial.println("[FACE] Happy"); continue;
}
if (line.equalsIgnoreCase("sleep")) {
appState = S_IDLE; faceSleeping();
Serial.println("[FACE] Sleeping"); continue;
}
if (line.equalsIgnoreCase("excited")) {
appState = S_IDLE; faceExcited();
Serial.println("[FACE] Excited"); continue;
}
if (line.equalsIgnoreCase("listen")) {
appState = S_IDLE; faceListening();
Serial.println("[FACE] Listening"); continue;
}
// AI call
if (appState == S_IDLE || appState == S_REPLY) {
Serial.println("[YOU] " + line);
beepWake();
appState = S_THINKING;
stateTimer = now;
faceThinking();
String reply = askGemini(line);
currentReply = reply;
Serial.println();
Serial.println("+------ ZERO AI --------------------");
int p = 0;
while (p < (int)reply.length()) {
int e = min(p + 58, (int)reply.length());
Serial.println("| " + reply.substring(p, e));
p = e;
}
Serial.println("+-----------------------------------");
Serial.println();
beepReply();
showReply(reply);
if (mpuOK) printSensorData();
appState = S_REPLY;
stateTimer = now;
} else {
Serial.println("[BUSY] Still processing... wait a moment.");
}
} else {
serialBuf += c;
}
}
// -- DOUBLE-TAP CHECK every 80ms --
static unsigned long lastTapCheck = 0;
if (now - lastTapCheck > 80) {
lastTapCheck = now;
if (checkDoubleTap() && appState == S_IDLE) {
Serial.println("[GESTURE] Double-tap detected!");
beepGesture();
faceGesture();
printSensorData();
appState = S_GESTURE;
stateTimer = now;
}
}
// -- STATE UPDATES --
switch (appState) {
case S_IDLE:
handleBlink();
if (!blinkClosed) faceHappy();
break;
case S_THINKING:
if (now - stateTimer > 150) {
stateTimer = now;
faceThinking();
}
break;
case S_REPLY:
if (now - stateTimer > 6000) {
appState = S_IDLE;
stateTimer = now;
blinkClosed = false;
faceHappy();
Serial.println("Ready for next message...");
Serial.println();
}
break;
case S_GESTURE:
if (now - stateTimer > 2500) {
appState = S_IDLE;
stateTimer = now;
blinkClosed = false;
faceHappy();
}
break;
default:
appState = S_IDLE;
break;
}
delay(25);
}