Community project
AI Companion Head

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
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 |
|---|---|---|
| SSD1306 OLED128×64 monochrome I2C | 1 | 0.96 inch 128x64 OLED display with I2C interface |
| HiLetgo INMP441 I²S Microphone ModuleI2S microphone | 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. |
| MAX98357A I2S Class-D Mono Amplifier Breakoutmono I2S amplifier | 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 | 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. |
| MicroSD Card ModuleSPI microSD adapter | 1 | 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. |
Assembly
6 stepsKeep 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.
- Tip: Use short wires for I²S audio connections to reduce noise.
- Tip: 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.
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.
- Tip: This assumes the selected 128×64 I²C SSD1306 OLED with address 0x3C.
- Tip: 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.
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.
- Tip: 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.
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.
- Tip: The amplifier is supplied from USB's 5 V rail for useful speaker volume.
- Tip: 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.
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.
- Tip: 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.
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.
- Tip: The network password and other body settings are in src/companion_settings.h.
- Tip: 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.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | oled_1 VCC | power |
| GND | oled_1 GND | ground |
| GPIO 21 | oled_1 SDA | i2c |
| GPIO 22 | oled_1 SCL | i2c |
| 3V3 | mic_1 VDD | power |
| GND | mic_1 GND | ground |
| GND | mic_1 L/R | ground |
| 5V | amp_1 VIN | power |
| GND | amp_1 GND | ground |
| GPIO 26 | amp_1 BCLK | data |
| GPIO 25 | amp_1 LRC | data |
| GPIO 27 | amp_1 DIN | data |
| EXT | amp_1 SPK+ → 8Ω Speaker POS | data |
| EXT | amp_1 SPK- → 8Ω Speaker NEG | data |
| 3V3 | sd_1 VCC | power |
| GND | sd_1 GND | ground |
| GPIO 19 | sd_1 MISO | spi |
| GPIO 23 | sd_1 MOSI | spi |
| GPIO 18 | sd_1 SCK | spi |
| GPIO 32 | mic_1 SCK | digital |
| GPIO 33 | mic_1 WS | digital |
| GPIO 16 | mic_1 SD | data |
| GPIO 4 | sd_1 CS | spi |
Firmware
ESP32// 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();
}“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.