Community project
Interactive Desktop Robot Buddy
Generated with AIBuild an interactive desktop robot buddy with expressive animations, touch interaction, and voice capabilities. This charming companion features an animated OLED face that displays different moods and reactions, articulated arms controlled by servos, and audio I/O powered by an ESP32 microcontroller. The guide includes a complete wiring diagram, parts list, 3D-printable enclosure files, and step-by-step assembly instructions to bring your robot buddy to life.
The firmware enables the robot to respond to touch input, detect sounds, play audio feedback, and capture voice samples to repeat back. Customize the personality by adjusting eye animations, arm poses, and reaction timings. This project combines mechanical assembly with embedded programming to create an engaging interactive device perfect for learning about microcontrollers, audio processing, and creative robotics.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| SH1106 OLED1.3 in / 128x64 / I2C | 1 | 1.3 inch 128x64 OLED display with I2C interface using the SH1106 controller |
| SG90 ServoSG90 | 1 | Micro servo motor (SG90) |
| SG90 ServoSG90 | 1 | Micro servo motor (SG90) |
| TTP223 Capacitive Touch Sensor Moduleinteraction pad | 1 | Single-pad capacitive touch sensor module based on the TTP223 IC. Outputs a digital HIGH/LOW signal on touch/release. Operates at 2.0–5.5V (3.3V compatible). No firmware library required — output is read as a standard digital GPIO input. Default mode is momentary (active HIGH on touch); solder pads on module allow toggling to active-LOW or self-locking (toggle) mode. |
| USB-C 5V Adapter5 V, 3 A minimum | 1 | USB-C wall adapter delivering regulated 5 V to the board's USB or VBUS rail. Default wired power source for desktop / stationary projects. |
| MAX98357A I2S Class-D Mono Amplifier BreakoutMAX98357A | 1 | I2S-input Class-D mono audio amplifier IC on a compact breakout board. Accepts I2S digital audio input (BCLK, LRC, DIN) and drives a small speaker or transducer directly. No I2C/SPI control bus is needed. The amplifier supply range is 2.5V-5.5V, and the I2S input pins are compatible with 3.3V logic. SD/MODE controls shutdown and channel selection; GAIN selects 3 dB, 6 dB, 9 dB, 12 dB, or 15 dB gain. |
| 8Ω Speaker8 Ω, 1 W minimum | 1 | Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback. |
| HiLetgo INMP441 I²S Microphone ModuleINMP441 | 1 | Omnidirectional 24-bit I²S MEMS microphone module based on the TDK InvenSense INMP441 IC. Outputs digital audio over a 3-wire I²S bus (SCK/BCLK, WS/LRCLK, SD/DOUT). Supply 1.8–3.3 V; native 3.3 V operation with no level shifting required. L/R channel select pin: tie to GND for left-channel mono output or VDD for right-channel mono output. Use the ESP32 Arduino core or ESP-IDF I2S peripheral API; no separate PlatformIO library is required. |
Assembly
8 stepsExport Abi’s cool enclosure parts
Open `data/abi_desktop_buddy_housing.scad` in OpenSCAD. Set `part_to_render` to `front`, render and export an STL; repeat for `rear`. Optionally set it to `accessories` to print the removable bow and ear inserts in a contrasting color. The front shell includes the cute-tech ear/headphone silhouette, raised OLED visor, feet, speaker grille, microphone holes, touch recess, and arm openings.
- Tip: Use 0.20 mm layers, 3 walls/perimeters, and 15–20% infill.
- Tip: A pink front shell, dark rear cover, and lavender bow/ear accents match the preview colors, but any colors work.
- Tip: Measure your actual generic modules before printing and adjust the parameters near the top of the SCAD file if needed.
- ⚠ Print the front shell with supports only if your slicer identifies the ear/visor overhangs; avoid supports inside the OLED opening and servo cradles.
- ⚠ The printed shell is mechanical only: keep all electronics disconnected while fitting parts.
Fit the face, arms, and cosmetic accents
Mount the SH1106 OLED behind the raised front visor. Place each SG90 in its internal side cradle, center the arm position before fastening its horn, and confirm that both arms move freely through the side slots. Glue or use thin foam tape to attach the optional bow and inner-ear inserts after the electronics test passes.
- Tip: Keep the arms short and lightweight so the SG90 gears are not overloaded.
- Tip: Use removable foam tape for the decorative bow so it can be replaced easily.
- Tip: The raised visor hides the OLED board edge and gives the face a cleaner finished look.
- ⚠ Disconnect power before fitting or removing servo horns.
- ⚠ Do not use glue near the OLED glass, servo spline, or moving arm slots.
Wire the OLED face
Connect OLED VCC to ESP32 3V3, GND to ESP32 GND, SDA to GPIO21, and SCL to GPIO22. Seat the display so its glass is centered in the visor opening.
- Tip: Keep I²C wires short and routed away from servo power wiring.
- Tip: The common OLED I²C address is 0x3C.
- ⚠ Use 3.3 V for the OLED unless its board specifically documents safe 5 V logic operation.
Install the touch interaction pad
Connect TTP223 VCC to ESP32 3V3, GND to common GND, and OUT to GPIO27. Mount its sensing face immediately behind the front circular touch recess so Abi can be tapped or petted.
- Tip: Use thin double-sided tape or a small non-metallic bracket.
- Tip: Test touch sensitivity before closing the rear shell.
- ⚠ Do not place the touch electrode against metal hardware or thick conductive paint.
Wire both SG90 arm servos
Connect the left servo signal wire to GPIO25 and right servo signal wire to GPIO26. Connect both red wires to the external regulated +5 V supply and both brown/black wires to that supply GND.
- Tip: Use a regulated 5 V supply rated for at least 3 A.
- Tip: Route the servo leads so they cannot touch the horn or arm travel slots.
- ⚠ Never power servo red wires from ESP32 3V3.
- ⚠ External 5 V supply GND must connect to ESP32 GND.
- ⚠ If 3.3 V control pulses are unreliable, add a proper 3.3 V-to-5 V signal buffer for GPIO25 and GPIO26.
Install the speaker and audio amplifier
Mount the 8 Ω speaker behind the front honeycomb grille, using its rim rather than its cone for adhesive. Connect MAX98357A VIN to external +5 V, GND to common GND, BCLK to GPIO18, LRC/WS to GPIO19, and DIN to GPIO23. Connect the speaker only between SPK+ and SPK−.
- Tip: Keep speaker wires short or twisted to reduce noise.
- Tip: The honeycomb grille is designed to leave the speaker cone unobstructed.
- ⚠ Never connect either speaker terminal to GND; MAX98357A uses bridged outputs.
- ⚠ Disconnect power before changing speaker wiring.
Install the INMP441 microphone
Connect INMP441 VDD to ESP32 3V3, GND to common GND, SCK to GPIO18, WS to GPIO19, SD to GPIO32, and L/R to GND. Align its sound port behind the small front microphone-hole array, not behind the speaker grille.
- Tip: The microphone shares I²S clocks GPIO18 and GPIO19 with the amplifier.
- Tip: For Abi’s echo mode, point the microphone outward and keep it several centimetres from the speaker.
- ⚠ Use 3.3 V only for INMP441 VDD.
- ⚠ Do not cover the microphone sound port with glue, tape, or fabric.
Close Abi and check the finished desktop buddy
Tie ESP32 GND, external 5 V adapter GND, servo grounds, amplifier GND, OLED GND, touch-pad GND, and microphone GND together. Confirm no wire rubs on the arms, slide on the rear shell, and fasten it with four M3 screws into the front-shell posts. The rear cover keeps the USB, external 5 V cable, and ventilation accessible.
- Tip: Leave the bow off until all electronics and arm movement have been tested.
- Tip: Touch the pad for the greeting after you deploy the existing firmware.
- Tip: The rear ventilation slots help avoid trapping heat around the electronics.
- ⚠ Verify external 5 V polarity before connecting it.
- ⚠ Use an enclosed, certified power adapter and keep mains wiring out of the enclosure.
- ⚠ Do not close the case until both arms have been tested through their complete travel.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | face_oled VCC | power |
| GND | face_oled GND | ground |
| GPIO 21 | face_oled SDA | i2c |
| GPIO 22 | face_oled SCL | i2c |
| EXT | left_arm_servo VCC → USB-C 5V Adapter +5V | power |
| GND | left_arm_servo GND | ground |
| EXT | right_arm_servo VCC → USB-C 5V Adapter +5V | power |
| GND | right_arm_servo GND | ground |
| 3V3 | touch_pad VCC | power |
| GND | touch_pad GND | ground |
| GPIO 27 | touch_pad SIG | digital |
| EXT | servo_supply +5V → USB-C mains adapter output | power |
| GND | servo_supply GND | ground |
| GPIO 25 | left_arm_servo SIGNAL | pwm |
| GPIO 26 | right_arm_servo SIGNAL | pwm |
| EXT | audio_amp VIN → USB-C 5V Adapter +5V | power |
| GND | audio_amp GND | ground |
| GPIO 18 | audio_amp BCLK | data |
| GPIO 19 | audio_amp LRC | data |
| GPIO 23 | audio_amp DIN | data |
| EXT | audio_amp SPK+ → 8Ω Speaker POS | data |
| EXT | audio_amp SPK- → 8Ω Speaker NEG | data |
| 3V3 | microphone VDD | power |
| GND | microphone GND | ground |
| GPIO 32 | microphone SD | data |
| GND | microphone L/R | ground |
| EXT | microphone SCK → MAX98357A I2S Class-D Mono Amplifier Breakout BCLK | digital |
| EXT | microphone WS → MAX98357A I2S Class-D Mono Amplifier Breakout LRC | digital |
Firmware
ESP32#include <Arduino.h>
#include <Wire.h>
#include <driver/i2s.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <ESP32Servo.h>
enum Reaction { IDLE, GREETING, SURPRISED, IDLE_POSE, LISTENING, REPEATING };
enum Mood { CALM, HAPPY, SHY, CURIOUS, SLEEPY };
enum EyeAnimation { EYE_NEUTRAL, EYE_LOOK_AROUND, EYE_SHY_GLANCE, EYE_WINK, EYE_HEARTS, EYE_DOUBLE_BLINK };
// Forward declarations
// Forward declarations
void drawHeart(int x, int y);
void setArms(int leftAngle, int rightAngle);
void drawLashes(int x, int y);
void drawFace();
void setupAudioAndMicrophone();
void writeTone(uint16_t frequency, uint16_t durationMs);
void playGreetingChirp();
void playSurprisedChirp();
void playHappyChirp();
void captureAndRepeatVoice();
void startGreeting(uint32_t now);
void startSoundReaction(uint32_t now);
void startIdlePose(uint32_t now);
void finishReaction(uint32_t now);
void updateReaction(uint32_t now);
void updateBlink(uint32_t now);
void updateEyeAnimation(uint32_t now);
bool heardLoudSound();
constexpr int OLED_SDA = 21, OLED_SCL = 22;
constexpr int LEFT_SERVO_PIN = 25, RIGHT_SERVO_PIN = 26, TOUCH_PIN = 27;
constexpr int I2S_BCLK_PIN = 18, I2S_LRC_PIN = 19, I2S_SPEAKER_DATA_PIN = 23, I2S_MIC_DATA_PIN = 32;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint32_t SAMPLE_RATE = 16000;
constexpr uint32_t VOICE_CAPTURE_MS = 1400;
constexpr size_t VOICE_SAMPLES = SAMPLE_RATE * VOICE_CAPTURE_MS / 1000;
constexpr int32_t CLAP_THRESHOLD = 180000;
constexpr int LEFT_REST = 82, RIGHT_REST = 98;
Adafruit_SH1106G display(128, 64, &Wire, -1);
Servo leftArm, rightArm;
static int32_t voiceBuffer[VOICE_SAMPLES];
Reaction reaction = IDLE;
Mood mood = CALM;
bool eyesClosed = false, previousTouch = false;
uint8_t waveStep = 0, idlePose = 0;
uint32_t lastBlinkMs = 0, nextBlinkDelayMs = 3000, blinkStartedMs = 0, lastTouchMs = 0;
uint32_t lastReactionStepMs = 0, lastSoundReactionMs = 0, nextIdleActionMs = 0;
EyeAnimation eyeAnimation = EYE_NEUTRAL;
uint32_t eyeAnimationStartedMs = 0, lastEyeFrameMs = 0, nextEyeAnimationMs = 0;
float pupilOffsetX = 0.0f, pupilOffsetY = 0.0f;
void setArms(int leftAngle, int rightAngle) {
leftArm.write(constrain(leftAngle, 0, 180));
rightArm.write(constrain(rightAngle, 0, 180));
}
void drawLashes(int x, int y) {
display.drawLine(x - 11, y - 8, x - 14, y - 13, SH110X_WHITE);
display.drawLine(x - 4, y - 10, x - 5, y - 15, SH110X_WHITE);
display.drawLine(x + 5, y - 9, x + 8, y - 14, SH110X_WHITE);
}
void drawHeart(int x, int y) {
display.fillCircle(x - 3, y - 2, 3, SH110X_BLACK);
display.fillCircle(x + 3, y - 2, 3, SH110X_BLACK);
display.fillTriangle(x - 6, y - 1, x + 6, y - 1, x, y + 7, SH110X_BLACK);
}
void drawFace() {
display.clearDisplay();
display.setTextColor(SH110X_WHITE);
display.setTextSize(1);
display.setCursor(52, 0);
display.print("Abi");
if (reaction == LISTENING) {
display.drawCircle(36, 29, 12, SH110X_WHITE);
display.drawCircle(93, 29, 12, SH110X_WHITE);
display.fillCircle(36, 29, 4, SH110X_WHITE);
display.fillCircle(93, 29, 4, SH110X_WHITE);
drawLashes(36, 29); drawLashes(93, 29);
display.setCursor(37, 49); display.print("Listening...");
} else if (reaction == REPEATING) {
display.fillRoundRect(22, 18, 27, 22, 10, SH110X_WHITE);
display.fillRoundRect(79, 18, 27, 22, 10, SH110X_WHITE);
display.fillCircle(36, 29, 6, SH110X_BLACK);
display.fillCircle(93, 29, 6, SH110X_BLACK);
drawLashes(36, 29); drawLashes(93, 29);
display.setCursor(25, 49); display.print("Abi says hi!");
} else if (eyesClosed || mood == SLEEPY) {
display.drawLine(24, 28, 48, mood == SLEEPY ? 30 : 28, SH110X_WHITE);
display.drawLine(80, mood == SLEEPY ? 30 : 28, 104, 28, SH110X_WHITE);
drawLashes(36, 29); drawLashes(93, 29);
} else if (reaction == SURPRISED) {
display.drawCircle(36, 29, 12, SH110X_WHITE); display.drawCircle(93, 29, 12, SH110X_WHITE);
display.fillCircle(36, 29, 5, SH110X_WHITE); display.fillCircle(93, 29, 5, SH110X_WHITE);
drawLashes(36, 29); drawLashes(93, 29);
} else {
display.fillRoundRect(22, 18, 27, 22, 10, SH110X_WHITE);
display.fillRoundRect(79, 18, 27, 22, 10, SH110X_WHITE);
int lpX = 36 + static_cast<int>(pupilOffsetX);
int rpX = 93 + static_cast<int>(pupilOffsetX);
int lpY = 29 + static_cast<int>(pupilOffsetY);
int rpY = 29 + static_cast<int>(pupilOffsetY);
if (eyeAnimation == EYE_HEARTS) {
drawHeart(lpX, lpY - 1); drawHeart(rpX, rpY - 1);
} else {
display.fillCircle(lpX, lpY, 6, SH110X_BLACK);
if (eyeAnimation != EYE_WINK) display.fillCircle(rpX, rpY, 6, SH110X_BLACK);
display.fillCircle(lpX - 2, lpY - 2, 2, SH110X_WHITE);
if (eyeAnimation != EYE_WINK) display.fillCircle(rpX - 2, rpY - 2, 2, SH110X_WHITE);
}
if (eyeAnimation == EYE_WINK) {
display.fillRect(79, 18, 27, 22, SH110X_BLACK);
display.drawLine(80, 29, 104, 29, SH110X_WHITE);
}
drawLashes(36, 29); drawLashes(93, 29);
}
if (reaction != LISTENING && reaction != REPEATING) {
if (mood == HAPPY || reaction == GREETING) {
display.fillCircle(17, 42, 4, SH110X_WHITE); display.fillCircle(111, 42, 4, SH110X_WHITE);
} else {
display.drawCircle(17, 42, 4, SH110X_WHITE); display.drawCircle(111, 42, 4, SH110X_WHITE);
}
display.drawLine(14, 42, 20, 42, SH110X_WHITE); display.drawLine(108, 42, 114, 42, SH110X_WHITE);
if (reaction == SURPRISED) display.drawCircle(64, 51, 5, SH110X_WHITE);
else if (reaction == GREETING || mood == HAPPY) {
display.drawRoundRect(43, 47, 42, 9, 5, SH110X_WHITE); display.drawLine(51, 50, 77, 50, SH110X_WHITE);
} else if (mood == SLEEPY) {
display.drawLine(57, 51, 71, 51, SH110X_WHITE); display.setCursor(105, 3); display.print("z");
} else {
display.drawLine(52, 50, 76, 50, SH110X_WHITE); display.drawPixel(51, 49, SH110X_WHITE); display.drawPixel(77, 49, SH110X_WHITE);
}
}
display.display();
}
void setupAudioAndMicrophone() {
i2s_config_t config = {};
config.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX);
config.sample_rate = SAMPLE_RATE;
config.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT;
config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
config.dma_buf_count = 6;
config.dma_buf_len = 128;
config.tx_desc_auto_clear = true;
i2s_driver_install(I2S_NUM_0, &config, 0, nullptr);
i2s_pin_config_t pins = {};
pins.bck_io_num = I2S_BCLK_PIN; pins.ws_io_num = I2S_LRC_PIN;
pins.data_out_num = I2S_SPEAKER_DATA_PIN; pins.data_in_num = I2S_MIC_DATA_PIN;
i2s_set_pin(I2S_NUM_0, &pins);
i2s_zero_dma_buffer(I2S_NUM_0);
}
void writeTone(uint16_t frequency, uint16_t durationMs) {
uint32_t samples = (SAMPLE_RATE * durationMs) / 1000, phase = 0;
int32_t buffer[128];
uint32_t phaseStep = (static_cast<uint64_t>(frequency) << 32) / SAMPLE_RATE;
for (uint32_t sent = 0; sent < samples;) {
uint32_t count = min(static_cast<uint32_t>(128), samples - sent);
for (uint32_t i = 0; i < count; ++i) {
buffer[i] = (phase & 0x80000000UL) ? -360000000 : 360000000;
phase += phaseStep;
}
size_t written = 0;
i2s_write(I2S_NUM_0, buffer, count * sizeof(int32_t), &written, portMAX_DELAY);
sent += count;
}
}
void playGreetingChirp() { writeTone(880, 70); writeTone(1320, 100); i2s_zero_dma_buffer(I2S_NUM_0); }
void playSurprisedChirp() { writeTone(1560, 65); writeTone(1040, 90); i2s_zero_dma_buffer(I2S_NUM_0); }
void playHappyChirp() { writeTone(660, 50); writeTone(880, 50); writeTone(1320, 80); i2s_zero_dma_buffer(I2S_NUM_0); }
void captureAndRepeatVoice() {
reaction = LISTENING;
mood = CALM;
setArms(70, 110);
drawFace();
delay(180);
size_t captured = 0;
while (captured < VOICE_SAMPLES) {
size_t bytesRead = 0;
size_t request = min(sizeof(int32_t) * static_cast<size_t>(128), (VOICE_SAMPLES - captured) * sizeof(int32_t));
i2s_read(I2S_NUM_0, voiceBuffer + captured, request, &bytesRead, portMAX_DELAY);
captured += bytesRead / sizeof(int32_t);
}
reaction = REPEATING;
drawFace();
const float pitchRatio = 1.28f;
int32_t output[128];
float sourceIndex = 0.0f;
while (sourceIndex < captured - 1) {
size_t count = 0;
while (count < 128 && sourceIndex < captured - 1) {
size_t base = static_cast<size_t>(sourceIndex);
float fraction = sourceIndex - base;
int64_t mixed = static_cast<int64_t>(voiceBuffer[base]) + static_cast<int64_t>((voiceBuffer[base + 1] - voiceBuffer[base]) * fraction);
output[count++] = static_cast<int32_t>(mixed * 0.72f);
sourceIndex += pitchRatio;
}
size_t written = 0;
i2s_write(I2S_NUM_0, output, count * sizeof(int32_t), &written, portMAX_DELAY);
}
i2s_zero_dma_buffer(I2S_NUM_0);
}
void startGreeting(uint32_t now) {
reaction = GREETING; mood = HAPPY; waveStep = 0; lastReactionStepMs = now;
eyeAnimation = EYE_WINK; eyeAnimationStartedMs = now;
setArms(55, 125); drawFace(); playGreetingChirp();
}
void startSoundReaction(uint32_t now) {
reaction = SURPRISED; mood = CALM; lastReactionStepMs = now; lastSoundReactionMs = now;
setArms(45, 135); drawFace(); playSurprisedChirp();
}
void startIdlePose(uint32_t now) {
reaction = IDLE_POSE; idlePose = esp_random() % 3; lastReactionStepMs = now;
eyeAnimationStartedMs = now;
if (idlePose == 0) { mood = CURIOUS; eyeAnimation = EYE_LOOK_AROUND; setArms(70, 112); }
else if (idlePose == 1) { mood = SHY; eyeAnimation = EYE_SHY_GLANCE; setArms(62, 118); }
else { mood = HAPPY; eyeAnimation = EYE_HEARTS; setArms(62, 118); playHappyChirp(); }
drawFace();
}
void finishReaction(uint32_t now) {
reaction = IDLE; setArms(LEFT_REST, RIGHT_REST);
eyeAnimation = EYE_NEUTRAL;
mood = (esp_random() % 4 == 0) ? SLEEPY : CALM;
nextIdleActionMs = now + 8000 + (esp_random() % 9000);
drawFace();
}
void updateReaction(uint32_t now) {
if (reaction == GREETING) {
if (now - lastReactionStepMs < 260) return;
lastReactionStepMs = now;
if (waveStep < 6) { rightArm.write((waveStep % 2 == 0) ? 55 : 125); ++waveStep; return; }
finishReaction(now);
} else if (reaction == SURPRISED && now - lastReactionStepMs >= 700) {
captureAndRepeatVoice();
lastSoundReactionMs = millis();
finishReaction(millis());
} else if (reaction == IDLE_POSE && now - lastReactionStepMs >= 1400) {
finishReaction(now);
}
}
void updateBlink(uint32_t now) {
if (reaction == SURPRISED || reaction == LISTENING || reaction == REPEATING || mood == SLEEPY || eyeAnimation == EYE_WINK) return;
if (!eyesClosed && now - lastBlinkMs >= nextBlinkDelayMs) {
eyesClosed = true; blinkStartedMs = now;
if (esp_random() % 5 == 0) { eyeAnimation = EYE_DOUBLE_BLINK; eyeAnimationStartedMs = now; }
drawFace();
}
if (eyesClosed && now - blinkStartedMs >= 115) {
eyesClosed = false; lastBlinkMs = now; nextBlinkDelayMs = 2500 + (esp_random() % 2500); drawFace();
}
}
void updateEyeAnimation(uint32_t now) {
if (now - lastEyeFrameMs < 16) return; // 60 Hz animation scheduler.
lastEyeFrameMs = now;
uint32_t elapsed = now - eyeAnimationStartedMs;
float targetX = 0.0f, targetY = 0.0f;
bool redraw = false;
if (eyeAnimation == EYE_LOOK_AROUND) {
float phase = (elapsed % 1200) / 1200.0f;
targetX = phase < 0.5f ? -4.0f + phase * 16.0f : 4.0f - (phase - 0.5f) * 16.0f;
redraw = true;
} else if (eyeAnimation == EYE_SHY_GLANCE) {
targetX = -3.0f; targetY = 3.0f; redraw = true;
} else if (eyeAnimation == EYE_WINK) {
targetX = -1.0f; redraw = true;
if (elapsed > 520) { eyeAnimation = EYE_HEARTS; eyeAnimationStartedMs = now; }
} else if (eyeAnimation == EYE_HEARTS) {
redraw = true;
if (reaction != IDLE_POSE && elapsed > 420) { eyeAnimation = EYE_NEUTRAL; }
} else if (eyeAnimation == EYE_DOUBLE_BLINK && elapsed > 230 && elapsed < 340) {
eyesClosed = true; redraw = true;
} else if (eyeAnimation == EYE_DOUBLE_BLINK && elapsed >= 340) {
eyesClosed = false; eyeAnimation = EYE_NEUTRAL; redraw = true;
}
pupilOffsetX += (targetX - pupilOffsetX) * 0.28f;
pupilOffsetY += (targetY - pupilOffsetY) * 0.28f;
if (redraw) drawFace();
}
bool heardLoudSound() {
int32_t samples[128]; size_t bytesRead = 0;
i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, 0);
size_t count = bytesRead / sizeof(int32_t);
if (count < 32) return false;
int64_t totalMagnitude = 0;
for (size_t i = 0; i < count; ++i) totalMagnitude += llabs(static_cast<int64_t>(samples[i] >> 8));
return (totalMagnitude / static_cast<int64_t>(count)) > CLAP_THRESHOLD;
}
void setup() {
pinMode(TOUCH_PIN, INPUT);
Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(OLED_ADDRESS, true)) while (true) delay(1000);
leftArm.setPeriodHertz(50); rightArm.setPeriodHertz(50);
leftArm.attach(LEFT_SERVO_PIN, 500, 2400); rightArm.attach(RIGHT_SERVO_PIN, 500, 2400);
setArms(LEFT_REST, RIGHT_REST);
setupAudioAndMicrophone();
lastBlinkMs = millis(); nextIdleActionMs = lastBlinkMs + 7000;
nextEyeAnimationMs = lastBlinkMs + 5000;
drawFace();
}
void loop() {
uint32_t now = millis();
bool touched = digitalRead(TOUCH_PIN) == HIGH;
if (touched && !previousTouch && now - lastTouchMs > 250 && reaction == IDLE) {
lastTouchMs = now; startGreeting(now);
}
previousTouch = touched;
if (reaction == IDLE && now - lastSoundReactionMs > 1600 && heardLoudSound()) startSoundReaction(now);
else if (reaction == IDLE && now >= nextIdleActionMs) startIdlePose(now);
updateReaction(now);
updateBlink(now);
updateEyeAnimation(now);
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.