Community project

Portable AI Companion Buddy

ESP32
Photo of Portable AI Companion Buddy
Generated with AI

Glitchlits

Published October 21, 2026

Build a portable AI companion with a friendly round display face and voice interaction capabilities. This project combines an ESP32 microcontroller with a 1.28-inch circular TFT screen, I2S audio amplifier, and speaker to create an interactive device that can display emotions and play audio responses.

This guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions to connect the round LCD screen, audio amplifier, and speaker to the ESP32. You'll also get the firmware code needed to drive the display animations, manage audio playback, and handle the device's interactive features.

Wiring diagram

Gather all the parts

QtyComponent
1

GC9A01 Round TFT LCD 1.28 inch 240x240

1.28 inch round IPS TFT LCD display module with GC9A01/GC9A01A driver IC. 240x240 RGB resolution, 4-wire SPI interface, and 3.3V/5V module input. Module-side labels are VCC, GND, DIN, CLK, CS, DC, RST, and BL/BLK. The display is write-only over SPI, so no MISO line is required for the LCD. Supported by Adafruit GC9A01A and TFT_eSPI libraries in Arduino-compatible firmware projects.

1

MAX98357A I2S Class-D Mono Amplifier Breakout

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.

1

8Ω Speaker

8 Ω, 1 W

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.

Assemble it in 5 steps

1. Fit the Sense camera board

Attach the XIAO ESP32S3 Sense expansion board and its camera to the XIAO exactly as the board maker shows. The tiny camera ribbon and socket are fragile; press the ribbon straight in with its contacts facing the socket contacts.

  • Do this before connecting USB power so you do not bend the camera connector.
  • Do not force or twist the camera ribbon — damage here stops the buddy from seeing.

2. Wire the round face screen

Connect the GC9A01 display: VCC → 3V3 (power), GND → GND (ground), SCK/CLK → D8/GPIO7 (clock), MOSI/DIN → D10/GPIO9 (picture data), CS → D7/GPIO44 (select), DC → D1/GPIO2 (screen command/data), RST → D2/GPIO3 (screen reset), and BL/BLK → 3V3 (backlight power).

  • Use short wires; the screen pins are normally printed along one edge of the module.
  • The display is a 3.3 V device here, so do not connect its VCC or BL pin to 5 V.
  • Make sure VCC and GND are not swapped — swapped power can damage the screen.

3. Wire the voice amplifier

Connect the MAX98357A amplifier: VIN → 5V (power), GND → GND (ground), BCLK → D4/GPIO5 (audio timing), LRC/LRCLK → D5/GPIO6 (audio timing), and DIN → D9/GPIO8 (voice data). Leave SD and GAIN alone on a normal breakout board.

  • Keep these three signal wires short and away from the camera ribbon to reduce noise.
  • The amplifier VIN pin needs 5 V for useful speaker volume; do not connect it to a GPIO pin.

4. Connect the speaker to the amplifier

Connect the speaker’s positive terminal to SPK+ on the MAX98357A and its other terminal to SPK− on the MAX98357A (voice output).

  • If the speaker has a marked red or + terminal, use it for SPK+.
  • Mount the speaker with its front facing an opening in the case so speech is not muffled.
  • Never connect either speaker wire to GND or directly to the XIAO — the amplifier drives both speaker wires.

5. Power and package the buddy

Place the boards in a non-metal case with clear openings for the camera, microphone hole, display, and speaker. Plug the XIAO into USB for initial power and use.

  • A short USB power bank cable makes it portable; the optional battery pack can be added as a later power-design change.
  • Keep the amplifier and speaker away from the microphone opening to reduce feedback.
  • Do not let loose wire ends touch each other inside the case — a short can damage the board or your USB power source.

Review all connections

1. Connections between "face_display" and "ESP32"

Functionface_displayESP32
powerVCC3V3
groundGNDGND
spiSCKGPIO 7
spiMOSIGPIO 9
digitalDCGPIO 2
digitalRSTGPIO 3
powerBL3V3
spiCSGPIO 44

2. Connections between "voice_amp" and "ESP32"

Functionvoice_ampESP32
powerVIN5V
groundGNDGND
dataBCLKGPIO 5
dataLRCGPIO 6
dataDINGPIO 8
dataSPK+ → 8Ω Speaker POSEXT
dataSPK- → 8Ω Speaker NEGEXT

Deploy the firmware

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
#include <esp_camera.h>
#include <driver/i2s.h>
#define TFT_SCK 7
#define TFT_MOSI 9
#define TFT_CS 44
#define TFT_DC 2
#define TFT_RST 3
#define AUDIO_BCLK 5
#define AUDIO_LRCK 6
#define AUDIO_DOUT 8
#define PDM_CLK 42
#define PDM_DATA 41
constexpr uint32_t RATE=16000; constexpr size_t CLIP=RATE;
Adafruit_GC9A01A tft(TFT_CS,TFT_DC,TFT_RST); Preferences prefs;
String bridge,token,shown=""; uint32_t faceTick=0,camTick=0,micTick=0;
void face(String s){if(s==shown)return;shown=s;uint16_t bg=GC9A01A_BLACK,e=GC9A01A_CYAN,m=GC9A01A_WHITE;if(s=="happy"){bg=0x03E0;e=GC9A01A_WHITE;}if(s=="listen"){bg=0x001F;e=GC9A01A_YELLOW;}if(s=="speak"){bg=0x780F;e=GC9A01A_WHITE;m=GC9A01A_YELLOW;}if(s=="error"){bg=0x7800;e=GC9A01A_WHITE;}tft.fillScreen(bg);tft.fillCircle(75,88,25,e);tft.fillCircle(165,88,25,e);tft.fillCircle(75,88,9,GC9A01A_BLACK);tft.fillCircle(165,88,9,GC9A01A_BLACK);if(s=="speak")tft.fillRoundRect(85,150,70,42,20,m);else if(s=="listen"){tft.drawCircle(120,165,30,m);tft.drawCircle(120,165,20,m);}else { tft.drawCircle(120,148,42,m); tft.fillRect(72,106,96,42,bg); }tft.setTextColor(GC9A01A_WHITE,bg);tft.setCursor(57,215);tft.print(s);}
bool cameraStart(){camera_config_t c={};c.ledc_channel=LEDC_CHANNEL_0;c.ledc_timer=LEDC_TIMER_0;c.pin_d0=48;c.pin_d1=18;c.pin_d2=15;c.pin_d3=17;c.pin_d4=16;c.pin_d5=14;c.pin_d6=12;c.pin_d7=11;c.pin_xclk=10;c.pin_pclk=13;c.pin_vsync=38;c.pin_href=47;c.pin_sccb_sda=40;c.pin_sccb_scl=39;c.pin_pwdn=-1;c.pin_reset=-1;c.xclk_freq_hz=20000000;c.pixel_format=PIXFORMAT_JPEG;c.frame_size=FRAMESIZE_QVGA;c.jpeg_quality=15;c.fb_count=1;c.fb_location=CAMERA_FB_IN_PSRAM;return esp_camera_init(&c)==ESP_OK;}
void speakerStart(){i2s_config_t c={};c.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_TX);c.sample_rate=RATE;c.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT;c.channel_format=I2S_CHANNEL_FMT_ONLY_LEFT;c.communication_format=I2S_COMM_FORMAT_I2S;c.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1;c.dma_buf_count=8;c.dma_buf_len=256;c.tx_desc_auto_clear=true;i2s_driver_install(I2S_NUM_1,&c,0,nullptr);i2s_pin_config_t p={};p.bck_io_num=AUDIO_BCLK;p.ws_io_num=AUDIO_LRCK;p.data_out_num=AUDIO_DOUT;p.data_in_num=I2S_PIN_NO_CHANGE;i2s_set_pin(I2S_NUM_1,&p);}
void micStart(){i2s_config_t c={};c.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_RX|I2S_MODE_PDM);c.sample_rate=RATE;c.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT;c.channel_format=I2S_CHANNEL_FMT_ONLY_LEFT;c.communication_format=I2S_COMM_FORMAT_I2S;c.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1;c.dma_buf_count=4;c.dma_buf_len=512;i2s_driver_install(I2S_NUM_0,&c,0,nullptr);i2s_pin_config_t p={};p.bck_io_num=I2S_PIN_NO_CHANGE;p.ws_io_num=PDM_CLK;p.data_out_num=I2S_PIN_NO_CHANGE;p.data_in_num=PDM_DATA;i2s_set_pin(I2S_NUM_0,&p);}
void network(){prefs.begin("buddy",false);bridge=prefs.getString("url","http://buddy-bridge.local:8787");token=prefs.getString("token","");WiFiManager w;WiFiManagerParameter u("url","Bridge URL",bridge.c_str(),120),t("token","Bridge access token",token.c_str(),96);w.addParameter(&u);w.addParameter(&t);w.setConfigPortalTimeout(180);if(!w.autoConnect("Buddy-Setup","buddy-setup"))ESP.restart();bridge=String(u.getValue());token=String(t.getValue());while(bridge.endsWith("/"))bridge.remove(bridge.length()-1);prefs.putString("url",bridge);prefs.putString("token",token);}
void auth(HTTPClient &h){if(token.length())h.addHeader("Authorization","Bearer "+token);}
void cameraPost(){if(WiFi.status()!=WL_CONNECTED)return;camera_fb_t*f=esp_camera_fb_get();if(!f)return;HTTPClient h;if(h.begin(bridge+"/v1/buddy/camera")){h.addHeader("Content-Type","image/jpeg");auth(h);h.POST(f->buf,f->len);h.end();}esp_camera_fb_return(f);}
void facePoll(){if(WiFi.status()!=WL_CONNECTED)return;HTTPClient h;if(h.begin(bridge+"/v1/buddy/face")){auth(h);if(h.GET()==HTTP_CODE_OK){String s=h.getString();s.trim();if(s.length()<16)face(s);}h.end();}}
void audioPoll(){if(WiFi.status()!=WL_CONNECTED)return;HTTPClient h;h.setTimeout(2000);if(!h.begin(bridge+"/v1/buddy/audio.pcm"))return;auth(h);if(h.GET()!=HTTP_CODE_OK){h.end();return;}face("speak");WiFiClient*s=h.getStreamPtr();uint8_t b[512];uint32_t wait=millis();while(h.connected()||s->available()){if(s->available()){size_t n=s->readBytes(b, min((size_t)s->available(), sizeof(b)));size_t out;i2s_write(I2S_NUM_1,b,n,&out,portMAX_DELAY);wait=millis();}else if(millis()-wait>1200)break;else delay(5);}h.end();}
void micPost(){if(WiFi.status()!=WL_CONNECTED)return;static int16_t data[CLIP];size_t got=0;i2s_read(I2S_NUM_0,data,sizeof(data),&got,50/portTICK_PERIOD_MS);size_t n=got/2;if(n<256)return;uint32_t energy=0;for(size_t i=0;i<n;i++)energy+=abs(data[i]);if(energy/n<900)return;face("listen");while(n<CLIP){size_t b=0;i2s_read(I2S_NUM_0,data+n,(CLIP-n)*2,&b,portMAX_DELAY);n+=b/2;}HTTPClient h;if(h.begin(bridge+"/v1/buddy/microphone.pcm")){h.addHeader("Content-Type","audio/L16;rate=16000;channels=1");auth(h);h.POST((uint8_t*)data,sizeof(data));h.end();}}
void setup(){Serial.begin(115200);SPI.begin(TFT_SCK,-1,TFT_MOSI,TFT_CS);tft.begin();tft.setRotation(0);face("boot");speakerStart();micStart();if(!cameraStart())face("error");network();face("happy");}
void loop(){uint32_t now=millis();if(now-faceTick>1000){faceTick=now;facePoll();}if(now-camTick>5000){camTick=now;cameraPost();}if(now-micTick>300){micTick=now;micPost();}audioPoll();delay(10);}

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.

Open in Schematik