Parts
- ×1Seeed XIAO ESP32-C3Has a BOOT button on the board that we use as the trigger.
- ×11.3 inch I2C OLED (SH1106, 128×64)Four pins: VCC, GND, SDA, SCL.
- ×4Jumper wiresOr thin wire to solder for the final keychain.
- ×1USB-C data cableFor power and uploading. An optional small LiPo can go on the battery pads for portable use.
Wire it up
Four wires to the OLED. The trigger button is already on the board.
- 3V3 to VCC
- GND to GND
- D4 (GPIO6) to SDA
- D5 (GPIO7) to SCL
Software setup
-
Add ESP32 support
In Arduino IDE 2.x, open Boards Manager and install esp32 by Espressif. If it isn't listed, add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File → Preferences → Additional boards manager URLs. -
Install three libraries
Adafruit GFX Library, Adafruit SH110X and FluxGarage RoboEyes. If RoboEyes isn't in the Library Manager, add its ZIP from GitHub.
-
Pick the board
Board:
XIAO_ESP32C3. Set USB CDC On Boot toEnabledso the Serial Monitor works.
Build
-
Wire it
Connect the four wires from the diagram, then plug in the USB-C cable.
-
Upload the sketch
Paste the full code below and upload. If it can't connect, hold BOOT, tap RESET, release BOOT, and try again.
-
Try it
The eyes blink and wander. Each BOOT press plays happy, curious, annoyed, then sad, for 2.5 seconds each before returning to idle.
I2C_ADDRESS to match.The full sketch
Paste this into the Arduino IDE. Change EMOTION_HOLD_MS to adjust how long each emotion plays.
/*
Robot Eyes Keychain Companion — Seeed XIAO ESP32-C3 + 1.3" SH1106 OLED
-------------------------------------------------------------------------
- Uses FluxGarage_RoboEyes library for smooth autonomous eye movements.
- Native 1.3" SH1106 support via Adafruit_SH110X template.
- Onboard BOOT Button (GPIO9) cycles expressions and triggers laugh animations.
- Every emotion plays for EMOTION_HOLD_MS (2.5 s), then returns to idle.
- No servos or external wiring needed.
Wiring (XIAO ESP32C3):
OLED VCC -> 3V3 OLED SDA -> D4 (GPIO6)
OLED GND -> GND OLED SCL -> D5 (GPIO7)
Trigger -> Onboard BOOT Button (GPIO9)
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <FluxGarage_RoboEyes.h>
// ===================== PINS (XIAO ESP32-C3) =====================
#define OLED_SDA D4 // GPIO6
#define OLED_SCL D5 // GPIO7
#define BUTTON_PIN 9 // Onboard BOOT Button (GPIO9)
// ===================== DISPLAY =====================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define I2C_ADDRESS 0x3C // Default for 1.3" SH1106
// ===================== TIMING =====================
#define EMOTION_HOLD_MS 2500 // how long each emotion plays before returning to idle
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
RoboEyes<Adafruit_SH1106G> roboEyes(display);
// ===================== EMOTIONS =====================
enum Emotion {
EMO_IDLE,
EMO_HAPPY,
EMO_CURIOUS,
EMO_ANNOYED,
EMO_SAD
};
Emotion currentEmotion = EMO_IDLE;
unsigned long emotionStartTime = 0;
// Button cycle order (independent of the auto-return timeout)
const Emotion emotionCycle[] = { EMO_HAPPY, EMO_CURIOUS, EMO_ANNOYED, EMO_SAD };
const uint8_t CYCLE_LEN = sizeof(emotionCycle) / sizeof(emotionCycle[0]);
uint8_t cycleIdx = 0;
// Button State
unsigned long lastButtonPress = 0;
bool lastButtonState = HIGH;
// ===================== EMOTION SYSTEM =====================
void setEmotion(Emotion e) {
currentEmotion = e;
emotionStartTime = millis();
// Clear anything a previous emotion may have left on
roboEyes.setHFlicker(OFF, 0);
switch (e) {
case EMO_IDLE:
roboEyes.setMood(DEFAULT);
roboEyes.setPosition(DEFAULT);
roboEyes.setIdleMode(ON, 2, 2);
roboEyes.setCuriosity(ON);
break;
case EMO_HAPPY:
roboEyes.setMood(HAPPY);
roboEyes.setIdleMode(OFF, 0, 0);
roboEyes.anim_laugh();
break;
case EMO_CURIOUS:
roboEyes.setMood(DEFAULT);
roboEyes.setCuriosity(ON);
roboEyes.setPosition(NE);
roboEyes.setIdleMode(OFF, 0, 0);
break;
case EMO_ANNOYED:
roboEyes.setMood(ANGRY);
roboEyes.setIdleMode(OFF, 0, 0);
roboEyes.setHFlicker(ON, 2);
break;
case EMO_SAD:
roboEyes.setMood(TIRED);
roboEyes.setIdleMode(OFF, 0, 0);
roboEyes.setPosition(S);
break;
}
}
void updateEmotionLogic() {
// Auto-return to IDLE after EMOTION_HOLD_MS
if (currentEmotion != EMO_IDLE && millis() - emotionStartTime >= EMOTION_HOLD_MS) {
setEmotion(EMO_IDLE);
}
}
// ===================== BUTTON =====================
void handleButton() {
bool state = digitalRead(BUTTON_PIN);
// Active LOW detection on BOOT button (GPIO9)
if (state == LOW && lastButtonState == HIGH && (millis() - lastButtonPress > 250)) {
lastButtonPress = millis();
// Each press advances to the next emotion, even after returning to idle
setEmotion(emotionCycle[cycleIdx]);
cycleIdx = (cycleIdx + 1) % CYCLE_LEN;
}
lastButtonState = state;
}
// ===================== SETUP =====================
void setup() {
Serial.begin(115200);
// Configure onboard button
pinMode(BUTTON_PIN, INPUT_PULLUP);
// I2C bus setup for XIAO ESP32-C3
Wire.begin(OLED_SDA, OLED_SCL);
Wire.setClock(400000); // 400 kHz fast I2C mode
// Initialize OLED (SH1106)
if (!display.begin(I2C_ADDRESS, true)) {
Serial.println(F("SH1106 not found - check wiring/address (try 0x3D)"));
while (true);
}
// Initialize RoboEyes
roboEyes.begin(SCREEN_WIDTH, SCREEN_HEIGHT, 60);
roboEyes.setAutoblinker(ON, 3, 2);
roboEyes.setIdleMode(ON, 2, 2);
roboEyes.setMood(DEFAULT);
roboEyes.setCuriosity(ON);
setEmotion(EMO_IDLE);
}
// ===================== MAIN LOOP =====================
void loop() {
roboEyes.update();
handleButton();
updateEmotionLogic();
}