Community project
Voice-Interactive DeskBuddy Avatar
Voice-Interactive DeskBuddy Avatar turns an ESP32 into an animated desktop companion that responds to sound and motion. The project combines a color touchscreen display with audio input via an I²S microphone and motion detection through a 3-axis accelerometer, creating an interactive avatar that reacts to loud sounds with animations and can be shaken to trigger responses.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for connecting the microphone and accelerometer modules to the ESP32. The included firmware handles audio capture, motion detection, touchscreen controls, and animated reactions, along with bonus features like weather display and calendar integration.
Wiring diagram

Gather all the parts
Assemble it in 5 steps
1. Place the CYD board safely
Put the ESP32-2432S028 CYD board on a non-metal desk with its color screen facing up. Leave its USB socket reachable so you can power and flash it later.
- The screen, touch panel, and small speaker are already built into this board.
- Do not rest the underside on loose wires or metal tools, because they can touch together and damage the board.
2. Wire the microphone
Connect the INMP441 module: VDD to the CYD 3V3 pin (power), GND to GND (ground), SCK to GPIO22 (microphone timing), WS to GPIO27 (microphone timing), SD to GPIO35 (sound data), and L/R to GND (selects the listening channel).
- Read the tiny labels on the microphone board before connecting each wire.
- Make sure VDD and GND are not swapped — swapped power can damage the microphone.
3. Wire the shake sensor
Connect the ADXL345 module: VCC to the CYD 3V3 pin (power), GND to GND (ground), SDA to GPIO27 (motion data), SCL to GPIO22 (motion timing), CS to 3V3 (selects the shared data wires), and SDO to GND (sets its normal address). Leave INT1 and INT2 unconnected.
- Mount or tape the sensor firmly to the CYD so it moves with the board when you shake it.
- Make sure VCC and GND are not swapped — swapped power can damage the shake sensor.
4. Check the shared wires
It is normal for both small modules to use the same GPIO22 and GPIO27 connections: GPIO22 is shared timing and GPIO27 is shared data. Make sure every module also has its own wire to the CYD GND pin.
- Use a breadboard rail or a small splitter only if it keeps the connections snug; loose jumper wires cause intermittent behavior.
- Do not connect either module to 5V — these modules are powered from the CYD 3V3 pin.
5. Use the screen controls
After DeskBuddy starts, tap the colored DeskBuddy bar at the very top of the face screen to change between SOUND ON and SILENT. In SILENT mode, the speaker stays quiet while touch, shake, weather, and face reactions still work. Tap the face below the top bar to change its mood, and swipe sideways to change pages.
- Turn sound back on by tapping the same top bar again; it gives a short chirp when sound returns.
- The silent control only affects DeskBuddy sounds; it does not mute the board's USB connection.
Review all connections
1. Connections between "inmp441_mic" and "ESP32"
2. Connections between "adxl345_1" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <time.h>
#include <SPI.h>
#include <Wire.h>
#include <driver/i2s.h>
#include <XPT2046_Touchscreen.h>
#include <Arduino_GFX_Library.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>
#include "secrets.h"
#define MIC_BCLK_PIN 1
#define MIC_LRCLK_PIN 3
#define MIC_DATA_PIN 35
#define ADXL_SDA_PIN 27
#define ADXL_SCL_PIN 22
enum Page { FACE, CLOCK, WEATHER, MOON, CALENDAR };
void beep(int hz, int ms);
void happyAnimation();
void dizzyAnimation();
String two(int n);
String nowText();
String moonText();
void label(int x, int y, const String &s, int size = 2);
void drawFace();
void drawPage();
void fetchWeather();
void fetchCalendar();
void handleTouch();
void setupMicrophone();
bool heardLoudSound();
void checkShake();
static constexpr int TFT_CS = 15, TFT_DC = 2, TFT_MOSI = 13, TFT_SCK = 14;
static constexpr int TOUCH_CS = 33, TOUCH_IRQ = 36, TOUCH_SCK = 25, TOUCH_MISO = 39, TOUCH_MOSI = 32;
static constexpr int SPEAKER_PIN = 26;
static constexpr int SOUND_TRIGGER_LEVEL = 18000;
static constexpr float SHAKE_DELTA_G = 1.15f;
// RGB565 colors for the CYD's color TFT.
static constexpr uint16_t C_NAVY = 0x10A2, C_CARD = 0x21A6, C_TEXT = 0xFFFF;
static constexpr uint16_t C_CYAN = 0x07FF, C_MINT = 0x7FF0, C_YELLOW = 0xFFE0;
static constexpr uint16_t C_PINK = 0xF81F, C_ORANGE = 0xFD20, C_PURPLE = 0x801F;
static constexpr uint16_t C_BLUE = 0x001F, C_DARK = 0x0841;
Arduino_DataBus *bus = new Arduino_ESP32SPI(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, -1, HSPI);
Arduino_GFX *gfx = new Arduino_ILI9341(bus, -1, 1, true);
SPIClass touchSPI(VSPI);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);
Adafruit_ADXL345_Unified accel(345);
Page page = FACE;
String mood = "neutral", weatherTemp = "-- C", weatherText = "Waiting for Wi-Fi", calendarText = "Add iCal URL in secrets.h";
unsigned long weatherAt = 0, calendarAt = 0, clockAt = 0, noiseAt = 0, soundAt = 0, shakeAt = 0, dizzyUntil = 0;
bool pressing = false, accelReady = false, silentMode = false;
int startX = 0, startY = 0;
float lastAccelG = 1.0f;
void beep(int hz, int ms) { if (!silentMode) tone(SPEAKER_PIN, hz, ms); }
void happyAnimation() { mood = "happy"; dizzyUntil = 0; beep(880, 60); delay(75); beep(1320, 75); }
void dizzyAnimation() { mood = "dizzy"; dizzyUntil = millis() + 2200; beep(420, 65); delay(80); beep(310, 115); }
String two(int n) { return n < 10 ? "0" + String(n) : String(n); }
String nowText() {
struct tm t;
if (!getLocalTime(&t, 20)) return "Waiting for time";
return two(t.tm_hour) + ":" + two(t.tm_min) + " " + two(t.tm_mday) + "/" + two(t.tm_mon + 1) + "/" + String(t.tm_year + 1900);
}
String moonText() {
struct tm t;
if (!getLocalTime(&t, 20)) return "Moon needs clock";
int y = t.tm_year + 1900, m = t.tm_mon + 1, d = t.tm_mday;
if (m < 3) { y--; m += 12; }
long days = 365L * y + y / 4 - y / 100 + y / 400 + (153L * (m - 3) + 2) / 5 + d - 730550;
int p = ((days % 29) + 29) % 29;
if (p < 2 || p > 27) return "New moon";
if (p < 7) return "Waxing crescent";
if (p < 9) return "First quarter";
if (p < 14) return "Waxing gibbous";
if (p < 17) return "Full moon";
if (p < 22) return "Waning gibbous";
if (p < 24) return "Last quarter";
return "Waning crescent";
}
void label(int x, int y, const String &s, int size, uint16_t color = C_TEXT, uint16_t background = C_NAVY) {
gfx->setTextColor(color, background); gfx->setTextSize(size); gfx->setCursor(x, y); gfx->print(s);
}
void drawFace() {
uint16_t faceColor = C_YELLOW, background = C_NAVY, accent = C_ORANGE;
if (mood == "happy") { faceColor = C_MINT; background = C_BLUE; accent = C_CYAN; }
else if (mood == "sleepy") { faceColor = C_PURPLE; background = 0x180B; accent = C_PINK; }
else if (mood == "sad") { faceColor = C_CYAN; background = 0x000C; accent = C_BLUE; }
else if (mood == "dizzy") { faceColor = C_ORANGE; background = 0x780F; accent = C_YELLOW; }
gfx->fillScreen(background);
gfx->fillRoundRect(10, 8, 300, 30, 10, accent);
label(20, 16, "DeskBuddy", 2, C_DARK, accent);
label(185, 19, silentMode ? "SILENT" : "SOUND ON", 1, C_DARK, accent);
label(12, 218, "top bar: sound | tap mood | swipe", 1, C_TEXT, background);
gfx->fillCircle(160, 123, 80, C_DARK);
gfx->fillCircle(160, 120, 78, faceColor);
if (mood == "dizzy") {
gfx->drawLine(108, 96, 132, 120, C_DARK); gfx->drawLine(132, 96, 108, 120, C_DARK);
gfx->drawLine(188, 96, 212, 120, C_DARK); gfx->drawLine(212, 96, 188, 120, C_DARK);
gfx->drawLine(126, 158, 145, 166, C_DARK); gfx->drawLine(145, 166, 175, 166, C_DARK); gfx->drawLine(175, 166, 194, 158, C_DARK);
} else if (mood == "sleepy") {
gfx->drawLine(105, 108, 135, 108, C_DARK); gfx->drawLine(185, 108, 215, 108, C_DARK); gfx->drawLine(126, 160, 194, 160, C_DARK);
} else {
gfx->fillCircle(120, 108, 12, C_DARK); gfx->fillCircle(200, 108, 12, C_DARK);
gfx->fillCircle(116, 104, 3, C_TEXT); gfx->fillCircle(196, 104, 3, C_TEXT);
if (mood == "sad") { gfx->drawLine(126, 166, 145, 150, C_DARK); gfx->drawLine(145, 150, 175, 150, C_DARK); gfx->drawLine(175, 150, 194, 166, C_DARK); }
else { gfx->drawLine(126, 150, 145, 166, C_DARK); gfx->drawLine(145, 166, 175, 166, C_DARK); gfx->drawLine(175, 166, 194, 150, C_DARK); }
}
label(112, 195, mood, 2, accent, background);
}
void drawPage() {
if (page == FACE) { drawFace(); return; }
gfx->fillScreen(C_NAVY); String title, body, detail; uint16_t accent = C_CYAN;
if (page == CLOCK) { title = "CLOCK / DATE"; body = nowText(); detail = "Time comes from Wi-Fi"; accent = C_MINT; }
if (page == WEATHER) { title = "OPEN-METEO WEATHER"; body = weatherTemp; detail = weatherText; accent = C_YELLOW; }
if (page == MOON) { title = "MOON PHASE"; body = moonText(); detail = "Based on current date"; accent = C_PURPLE; }
if (page == CALENDAR) { title = "GOOGLE CALENDAR"; body = calendarText; detail = "Private iCal link only"; accent = C_PINK; }
gfx->fillRoundRect(10, 10, 300, 35, 10, accent);
label(20, 20, title, 2, C_DARK, accent);
gfx->fillRoundRect(14, 55, 292, 135, 14, C_CARD);
gfx->drawRoundRect(14, 55, 292, 135, 14, accent);
label(27, 82, body, 2, C_TEXT, C_CARD); label(27, 165, detail, 1, accent, C_CARD);
label(18, 218, "swipe left / right", 1, C_TEXT, C_NAVY);
}
void fetchWeather() {
if (WiFi.status() != WL_CONNECTED) { weatherText = "Wi-Fi not connected"; return; }
HTTPClient h;
String u = "http://api.open-meteo.com/v1/forecast?latitude=" + String(WEATHER_LATITUDE, 4) + "&longitude=" + String(WEATHER_LONGITUDE, 4) + "¤t=temperature_2m,weather_code&timezone=auto";
h.setTimeout(7000);
if (h.begin(u) && h.GET() == 200) {
JsonDocument d;
if (deserializeJson(d, h.getString()) == DeserializationError::Ok) {
weatherTemp = String((float)(d["current"]["temperature_2m"] | 0.0), 1) + " C";
int c = d["current"]["weather_code"] | -1;
weatherText = c == 0 ? "Clear sky" : c <= 3 ? "Partly cloudy" : c <= 48 ? "Foggy" : c <= 67 ? "Rain" : c <= 77 ? "Snow" : "Showers or storm";
}
}
h.end();
}
void fetchCalendar() {
if (String(GOOGLE_CALENDAR_ICAL_URL).length() == 0) return;
if (WiFi.status() != WL_CONNECTED) { calendarText = "Wi-Fi not connected"; return; }
WiFiClientSecure c; c.setInsecure(); HTTPClient h; h.setTimeout(10000);
if (h.begin(c, GOOGLE_CALENDAR_ICAL_URL) && h.GET() == 200) {
String ics = h.getString(); int a = ics.indexOf("SUMMARY:"), b = ics.indexOf('\n', a);
calendarText = a >= 0 ? "Next: " + ics.substring(a + 8, b < 0 ? ics.length() : b) : "No events found"; calendarText.trim();
} else calendarText = "Calendar fetch failed";
h.end();
}
void handleTouch() {
if (!touch.touched()) { pressing = false; return; }
TS_Point p = touch.getPoint(); int x = constrain(map(p.x, 250, 3800, 0, 320), 0, 319), y = constrain(map(p.y, 250, 3800, 0, 240), 0, 239);
if (!pressing) { pressing = true; startX = x; startY = y; return; }
int dx = x - startX, dy = y - startY;
if (abs(dx) > 65) { page = (Page)((page + (dx < 0 ? 1 : 4)) % 5); pressing = false; beep(650, 30); drawPage(); }
else if (abs(dx) < 25 && abs(dy) < 25 && page == FACE) {
if (startY <= 45) {
silentMode = !silentMode;
if (!silentMode) beep(880, 55);
} else {
if (mood == "neutral") happyAnimation(); else if (mood == "happy") mood = "sleepy"; else if (mood == "sleepy") mood = "sad"; else mood = "neutral";
}
pressing = false; drawPage();
}
}
void setupMicrophone() {
i2s_config_t config = {};
config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX); config.sample_rate = 16000; config.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT;
config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; config.communication_format = I2S_COMM_FORMAT_I2S; config.dma_buf_count = 4; config.dma_buf_len = 128;
i2s_pin_config_t pins = {}; pins.bck_io_num = MIC_BCLK_PIN; pins.ws_io_num = MIC_LRCLK_PIN; pins.data_out_num = I2S_PIN_NO_CHANGE; pins.data_in_num = MIC_DATA_PIN;
i2s_driver_install(I2S_NUM_0, &config, 0, nullptr); i2s_set_pin(I2S_NUM_0, &pins); i2s_zero_dma_buffer(I2S_NUM_0);
}
bool heardLoudSound() {
int32_t samples[64]; size_t bytesRead = 0;
if (i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, 0) != ESP_OK || bytesRead == 0) return false;
int64_t total = 0; int count = bytesRead / sizeof(int32_t);
for (int i = 0; i < count; i++) total += abs(samples[i] >> 14);
return count > 0 && (total / count) > SOUND_TRIGGER_LEVEL;
}
void checkShake() {
if (!accelReady || millis() - shakeAt < 70) return;
shakeAt = millis(); sensors_event_t event; accel.getEvent(&event);
float g = sqrtf(event.acceleration.x * event.acceleration.x + event.acceleration.y * event.acceleration.y + event.acceleration.z * event.acceleration.z) / SENSORS_GRAVITY_STANDARD;
if (fabsf(g - lastAccelG) > SHAKE_DELTA_G && millis() > dizzyUntil + 700) { dizzyAnimation(); if (page == FACE) drawPage(); }
lastAccelG = g;
}
void setup() {
pinMode(SPEAKER_PIN, OUTPUT); randomSeed(esp_random());
gfx->begin(); touchSPI.begin(TOUCH_SCK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS); touch.begin(touchSPI); touch.setRotation(1);
Wire.begin(ADXL_SDA_PIN, ADXL_SCL_PIN); accelReady = accel.begin(); if (accelReady) { accel.setRange(ADXL345_RANGE_16_G); sensors_event_t event; accel.getEvent(&event); lastAccelG = sqrtf(event.acceleration.x * event.acceleration.x + event.acceleration.y * event.acceleration.y + event.acceleration.z * event.acceleration.z) / SENSORS_GRAVITY_STANDARD; }
setupMicrophone(); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); configTime(0, 0, "pool.ntp.org", "time.nist.gov"); drawPage();
}
void loop() {
handleTouch(); checkShake();
if (dizzyUntil && millis() > dizzyUntil) { dizzyUntil = 0; mood = "neutral"; if (page == FACE) drawPage(); }
if (millis() - soundAt > 1500 && heardLoudSound()) { soundAt = millis(); happyAnimation(); if (page == FACE) drawPage(); }
if (WiFi.status() == WL_CONNECTED && millis() - weatherAt > 900000UL) { fetchWeather(); weatherAt = millis(); if (page == WEATHER) drawPage(); }
if (WiFi.status() == WL_CONNECTED && millis() - calendarAt > 1800000UL) { fetchCalendar(); calendarAt = millis(); if (page == CALENDAR) drawPage(); }
if (millis() - noiseAt > 60000UL) { noiseAt = millis(); if (random(12) == 0) beep(random(360, 1000), 35); }
if (page == CLOCK && millis() - clockAt > 30000UL) { clockAt = millis(); drawPage(); }
delay(12);
}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.




