Community project

AI Companion Head

ESP32
Photo of AI Companion Head
Generated with AI

M_Two

Last updated August 11, 2026

Build an AI Companion Head that displays animated faces on an OLED screen and responds with synthesized speech through a speaker. This ESP32-based project receives commands from a PC over Wi-Fi, allowing the companion to show expressions and play audio generated by an AI model running on the host computer.

The guide provides a complete wiring diagram connecting the SSD1306 display, INMP441 microphone, MAX98357A amplifier, speaker, and MicroSD card module to the ESP32. Assembly steps cover safe wiring practices, firmware configuration for Wi-Fi and audio settings, and customization of facial animations through the provided C++ code.

Wiring diagram

Wiring diagram for AI Companion Head

Gather all the parts

QtyComponent
1

SSD1306 OLED

128×64 monochrome I2C

0.96 inch 128x64 OLED display with I2C interface

1

HiLetgo INMP441 I²S Microphone Module

I2S microphone

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.

1

MAX98357A I2S Class-D Mono Amplifier Breakout

mono I2S amplifier

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.

1

MicroSD Card Module

SPI microSD adapter

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

Assemble it in 6 steps

1. Keep USB disconnected while wiring

Build the circuit on a breadboard or secure wiring harness first. Use the ESP32 USB port as the only power source for this version.

  • Use short wires for I²S audio connections to reduce noise.
  • All modules must share ESP32 GND.
  • Do not power the INMP441 microphone from 5 V.
  • Do not connect either speaker terminal to GND; the MAX98357A speaker outputs are a paired amplifier output.

2. Wire the OLED screen

Connect OLED VCC to ESP32 3V3 and OLED GND to ESP32 GND. Connect OLED SDA to GPIO21 and OLED SCL to GPIO22.

  • This assumes the selected 128×64 I²C SSD1306 OLED with address 0x3C.
  • If the screen remains blank, confirm its VCC/GND labels and I²C address before changing any wiring.
  • Use 3.3 V for the OLED to keep its I²C signals safe for the ESP32.

3. Wire the I²S microphone

Connect INMP441 VDD to 3V3, GND to GND, and L/R to GND (left channel). Connect SCK to GPIO32, WS to GPIO33, and SD to GPIO16.

  • Place the microphone away from the speaker so it is less likely to hear its own reply audio.
  • INMP441 VDD must be 3.3 V maximum.

4. Wire amplifier and speaker

Connect MAX98357A VIN to ESP32 5V/VIN, and GND to ESP32 GND. Connect BCLK to GPIO26, LRC to GPIO25, and DIN to GPIO27. Connect amplifier SPK+ to speaker POS and amplifier SPK- to speaker NEG.

  • The amplifier is supplied from USB's 5 V rail for useful speaker volume.
  • Keep the two speaker leads together and separate from microphone wires where possible.
  • Never connect SPK+ or SPK- to the ESP32, 5 V, or GND.
  • Start with a 1 W or higher 8 Ω speaker.

5. Wire the microSD adapter

Connect SD adapter VCC to 3V3 and GND to GND. Connect MISO to GPIO19, MOSI to GPIO23, SCK to GPIO18, and CS to GPIO4. Insert a FAT32-formatted microSD card after wiring.

  • Use an adapter designed for 3.3 V SPI logic, or one explicitly marked safe with 3.3 V ESP32 signals.
  • Do not use a 5 V-only SD module with 3.3 V ESP32 signal pins.

6. Power the body and connect the PC

Connect USB power and use Schematik’s Deploy button. The ESP32 creates Wi-Fi named Companion-Body. Connect the PC running the companion program to that network, then let the PC send display-state, text, and audio commands to the ESP32 at 192.168.4.1.

  • The network password and other body settings are in src/companion_settings.h.
  • The PC program is the brain: it performs AI, speech recognition, text-to-speech, and decides what the OLED shows.
  • Change the default Wi-Fi password in companion_settings.h before regular use.
  • This SSD1306 screen is monochrome. The PC can select face animations and text, but it cannot mirror a colour picture or normal GIF exactly.

Review all connections

1. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "mic_1" and "ESP32"

Functionmic_1ESP32
powerVDD3V3
groundGNDGND
groundL/RGND
digitalSCKGPIO 32
digitalWSGPIO 33
dataSDGPIO 16

3. Connections between "amp_1" and "ESP32"

Functionamp_1ESP32
powerVIN5V
groundGNDGND
dataBCLKGPIO 26
dataLRCGPIO 25
dataDINGPIO 27
dataSPK+8Ω Speaker POSEXT
dataSPK-8Ω Speaker NEGEXT

4. Connections between "sd_1" and "ESP32"

Functionsd_1ESP32
powerVCC3V3
groundGNDGND
spiMISOGPIO 19
spiMOSIGPIO 23
spiSCKGPIO 18
spiCSGPIO 4

Deploy the firmware

// PC Mirror Companion — ESP32 body firmware
// GENERATE: the PC's AI sends state/text/audio commands below.
// MODIFY: edit face and animation drawing in companion_body.cpp.
// SETTINGS: edit Wi-Fi and audio values in companion_settings.h.
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include "driver/i2s.h"
#include "companion_settings.h"
#include "companion_body.h"


// Forward declarations
void startSpeaker();
void sendJson(const String &json);
void handleStatus();
void handleSet();
void handleSpeak();
void handleRoot();

constexpr int AMP_BCLK = 26;
constexpr int AMP_LRCLK = 25;
constexpr int AMP_DIN = 27;
WebServer server(BODY_HTTP_PORT);

void startSpeaker() {
  i2s_config_t config = {};
  config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
  config.sample_rate = PCM_SAMPLE_RATE;
  config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
  config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
  config.communication_format = I2S_COMM_FORMAT_I2S;
  config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
  config.dma_buf_count = 4;
  config.dma_buf_len = 128;
  config.tx_desc_auto_clear = true;
  i2s_driver_install(I2S_NUM_1, &config, 0, nullptr);
  i2s_pin_config_t pins = {};
  pins.bck_io_num = AMP_BCLK;
  pins.ws_io_num = AMP_LRCLK;
  pins.data_out_num = AMP_DIN;
  pins.data_in_num = I2S_PIN_NO_CHANGE;
  i2s_set_pin(I2S_NUM_1, &pins);
  i2s_zero_dma_buffer(I2S_NUM_1);
}

void sendJson(const String &json) {
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.send(200, "application/json", json);
}
void handleStatus() {
  String json = "{\"state\":\"" + bodyStateName() + "\",\"text\":\"" + bodyText() +
                "\",\"ip\":\"" + WiFi.softAPIP().toString() + "\"}";
  sendJson(json);
}
void handleSet() {
  if (!server.hasArg("state") && !server.hasArg("text")) {
    server.send(400, "text/plain", "Use state and/or text query values.");
    return;
  }
  if (server.hasArg("state")) bodySetState(server.arg("state"));
  if (server.hasArg("text")) bodySetText(server.arg("text"));
  if (server.hasArg("contrast")) bodySetContrast((uint8_t)constrain(server.arg("contrast").toInt(), 0, 255));
  handleStatus();
}
void handleSpeak() {
  // PC TTS sends raw signed 16-bit, 16 kHz, mono PCM in the POST body.
  if (server.method() != HTTP_POST) {
    server.send(405, "text/plain", "POST raw 16-bit mono PCM data.");
    return;
  }
  bodySetState("talking");
  const String &raw = server.arg("plain");
  const uint8_t *bytes = (const uint8_t *)raw.c_str();
  for (size_t offset = 0; offset + 1 < raw.length(); offset += 2) {
    int16_t sample = (int16_t)((uint16_t)bytes[offset] | ((uint16_t)bytes[offset + 1] << 8));
    int16_t stereo[2] = {sample, sample};
    size_t written;
    i2s_write(I2S_NUM_1, stereo, sizeof(stereo), &written, portMAX_DELAY);
  }
  bodySetState("idle");
  sendJson("{\"ok\":true}");
}
void handleRoot() {
  server.send(200, "text/html", "<h2>PC Mirror Companion</h2><p>PC control endpoint ready.</p><p>GET /set?state=listening&text=Hello</p><p>GET /status</p>");
}

void setup() {
  Serial.begin(115200);
  bodyBegin();
  startSpeaker();
  WiFi.mode(WIFI_AP);
  WiFi.softAP(BODY_AP_NAME, BODY_AP_PASSWORD);
  bodySetText("Connect PC to Wi-Fi");
  server.on("/", HTTP_GET, handleRoot);
  server.on("/status", HTTP_GET, handleStatus);
  server.on("/set", HTTP_GET, handleSet);
  server.on("/speak", HTTP_POST, handleSpeak);
  server.begin();
}
void loop() {
  server.handleClient();
  bodyTick();
}

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