Community project

Ik Wil Een Deskbuddy Maken Met Een Gezichtje

ESP32
Photo of Ik Wil Een Deskbuddy Maken Met Een Gezichtje
Generated with AI

Quinten Huijnen

Published August 27, 2026

Build a charming desk companion with an animated face displayed on a 3.2-inch TFT screen. This project uses an ESP32 to drive the ILI9341 display, showing expressive animations and system monitoring information via Bluetooth connectivity from a PC.

The guide includes a complete wiring diagram for connecting the TFT display to the ESP32, a full parts list, and firmware that animates facial expressions with yawning, displays CPU and GPU temperatures received over Bluetooth, and cycles through different information screens. Assembly takes just a few minutes of connecting power and data lines, then uploading the code to bring your deskbuddy to life.

Wiring diagram

Wiring diagram for Ik Wil Een Deskbuddy Maken Met Een Gezichtje

Gather all the parts

QtyComponent
1

3.2-inch ILI9341 SPI TFT zonder aanraking

3.2 inch, ILI9341, 320×240, zonder touch, circa 8.1 cm diagonaal

Een 3,2-inch kleurenscherm waarop het DeskBuddy-gezicht en de pc-temperaturen zichtbaar zijn.

Assemble it in 4 steps

1. Leg het scherm en de XIAO klaar

Leg de Seeed Studio XIAO ESP32S3, een 3,2-inch ILI9341 SPI-kleurenscherm zonder aanraking, zeven dunne jumperdraden en een USB-C-kabel neer. Kies een scherm waarop `VCC`, `GND`, `CS`, `RST`, `DC` of `RS`, `MOSI` of `SDI` en `SCK` staat.

  • Dit scherm is ongeveer 8,1 cm van hoek tot hoek.
  • Een variant zonder touch is goedkoper en heeft geen onnodige aansluitingen.
  • Sluit de USB-C-kabel nog niet aan terwijl je de draden plaatst; een draad op de verkeerde plek kan het scherm beschadigen.

2. Sluit de voeding van het scherm aan

Verbind `VCC` van het scherm met `3V3` op de XIAO met een rode draad (voeding). Verbind `GND` van het scherm met `GND` op de XIAO met een zwarte draad (grond). Heeft jouw scherm een losse `LED`- of `BL`-pin voor de schermlamp, verbind die dan ook met `3V3` (schermlamp).

  • `BL` betekent hetzelfde als `LED` op veel schermprintjes.
  • Gebruik 3V3 voor dit scherm, niet 5V.
  • Maak zeker dat VCC en GND niet zijn verwisseld — verkeerd aangesloten voeding kan het scherm beschadigen.

3. Sluit de beeld-draden aan

Verbind `SCK` van het scherm met GPIO12 op de XIAO (klok), `MOSI` of `SDI` met GPIO11 (beeldgegevens), `CS` met GPIO10 (scherm kiezen), `DC` of `RS` met GPIO4 (beeld of opdracht) en `RST` met GPIO3 (scherm opnieuw starten).

  • Volg de woorden op jouw schermprint: `SDI` is hetzelfde als MOSI en `RS` is hetzelfde als DC.
  • Dit scherm heeft geen touch-pinnen en ook geen MISO-draad nodig.
  • Verwissel SCK en MOSI niet; dan kan het scherm leeg blijven of rommel tonen.

4. Controleer en geef stroom

Vergelijk elke draad met de labels hierboven. Steek daarna de USB-C-kabel in de XIAO en in een USB-poort van je pc of een degelijke USB-wandadapter (voeding). Het scherm moet het DeskBuddy-gezicht tonen en de XIAO verschijnt via Bluetooth als `DeskBuddy-PC-Temp`.

  • Zet het scherm later in een behuizing of achter een venster, zodat de dunne draden niet losgetrokken worden.
  • De DeskBuddy werkt nu alleen via USB-C; je hebt geen batterij of laadmodule nodig.
  • Controleer de voeding eerst goed voordat je USB-C aansluit; omgewisselde voeding kan het scherm beschadigen.

Review all connections

1. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 11
spiSCKGPIO 12
digitalCSGPIO 10
digitalDCGPIO 4
digitalRSTGPIO 3

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

enum DisplayPhase { FACE_PHASE, CPU_PHASE, GPU_PHASE };


// Forward declarations
bool isYawning(unsigned long now);
void drawSweatDrop(int x, int y);
void drawFaceRegion();
void drawFaceHeaderAndTemperature();
void drawTemperatureScreen(const char *label, float temperatureC);
void redrawScreen();
void requestPcTemperature(const char *request);
void setupBluetooth();
void updateFaceAnimation(unsigned long now);
void updateSchedule(unsigned long now);

constexpr int TFT_SCK_PIN = 12;
constexpr int TFT_MOSI_PIN = 11;
constexpr int TFT_CS_PIN = 10;
constexpr int TFT_DC_PIN = 4;
constexpr int TFT_RST_PIN = 3;
constexpr int SCREEN_WIDTH = 320;
constexpr int SCREEN_HEIGHT = 240;
constexpr uint16_t BACKGROUND = ILI9341_BLACK;
constexpr uint16_t FEATURE_COLOR = ILI9341_BLUE;
constexpr uint16_t SWEAT_COLOR = ILI9341_CYAN;
constexpr uint16_t TEXT_COLOR = ILI9341_WHITE;
constexpr char SERVICE_UUID[] = "a9c1c100-7d3e-4c2e-9bf1-6e6846e10001";
constexpr char REQUEST_UUID[] = "a9c1c100-7d3e-4c2e-9bf1-6e6846e10002";
constexpr char TEMPERATURE_UUID[] = "a9c1c100-7d3e-4c2e-9bf1-6e6846e10003";
constexpr unsigned long FACE_PERIOD_MS = 5UL * 60UL * 1000UL;
constexpr unsigned long CPU_SCREEN_MS = 20UL * 1000UL;
constexpr unsigned long GPU_SCREEN_MS = 20UL * 1000UL;
constexpr unsigned long COOL_YAWN_DELAY_MS = 10UL * 60UL * 1000UL;
constexpr unsigned long MOUTH_MOVE_MS = 420UL;
constexpr unsigned long YAWN_FRAME_MS = 700UL;

Adafruit_ILI9341 display(TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN);
BLECharacteristic *requestCharacteristic = nullptr;
bool bluetoothConnected = false, eyesClosed = false, mouthOpen = false;
DisplayPhase phase = FACE_PHASE;
unsigned long phaseStartedMs = 0, blinkEndsMs = 0, nextBlinkMs = 0, mouthEndsMs = 0, nextMouthMoveMs = 0, coolSinceMs = 0;
float cpuTemperatureC = NAN, gpuTemperatureC = NAN;

bool isYawning(unsigned long now) { return coolSinceMs != 0 && now - coolSinceMs >= COOL_YAWN_DELAY_MS; }
void drawSweatDrop(int x, int y) { display.fillTriangle(x, y - 13, x - 9, y + 5, x + 9, y + 5, SWEAT_COLOR); display.fillCircle(x, y + 5, 9, SWEAT_COLOR); }
void drawFaceRegion() {
  display.fillRect(0, 28, SCREEN_WIDTH, 185, BACKGROUND);
  // A soft circular outline keeps the face cute while the background stays black.
  display.drawCircle(160, 122, 84, FEATURE_COLOR);
  if (eyesClosed) { display.drawLine(116, 111, 140, 111, FEATURE_COLOR); display.drawLine(180, 111, 204, 111, FEATURE_COLOR); }
  else { display.fillCircle(128, 111, 10, FEATURE_COLOR); display.fillCircle(192, 111, 10, FEATURE_COLOR); }
  if (isYawning(millis())) { if (mouthOpen) display.fillRoundRect(137, 149, 46, 38, 18, FEATURE_COLOR); else display.drawRoundRect(143, 160, 34, 18, 8, FEATURE_COLOR); }
  else if (mouthOpen) display.fillRoundRect(143, 156, 34, 18, 10, FEATURE_COLOR);
  else { display.drawLine(128, 158, 141, 170, FEATURE_COLOR); display.drawLine(141, 170, 179, 170, FEATURE_COLOR); display.drawLine(179, 170, 192, 158, FEATURE_COLOR); }
  if (!isnan(gpuTemperatureC) && gpuTemperatureC >= 55.0f) drawSweatDrop(259, 93);
  if (!isnan(gpuTemperatureC) && gpuTemperatureC >= 70.0f) drawSweatDrop(272, 130);
  if (!isnan(gpuTemperatureC) && gpuTemperatureC >= 80.0f) drawSweatDrop(52, 90);
}
void drawFaceHeaderAndTemperature() {
  display.fillRect(0, 0, SCREEN_WIDTH, 28, BACKGROUND); display.setTextColor(TEXT_COLOR, BACKGROUND); display.setTextSize(2); display.setCursor(8, 6); display.print(bluetoothConnected ? "GPU gezicht" : "Wachten op pc");
  display.fillRect(0, 213, SCREEN_WIDTH, 27, BACKGROUND); display.setCursor(8, 218);
  if (isnan(gpuTemperatureC)) display.print("GPU: wachten op waarde"); else { display.print("GPU "); display.print(gpuTemperatureC, 1); display.print(" C"); }
}
void drawTemperatureScreen(const char *label, float temperatureC) {
  display.fillScreen(BACKGROUND); display.setTextColor(TEXT_COLOR); display.setTextSize(4); display.setCursor((SCREEN_WIDTH - strlen(label) * 24) / 2, 45); display.print(label); display.setTextSize(5); display.setCursor(38, 130);
  if (isnan(temperatureC)) display.print("--.- C"); else { display.print(temperatureC, 1); display.print(" C"); }
}
void redrawScreen() { if (phase == FACE_PHASE) { drawFaceHeaderAndTemperature(); drawFaceRegion(); } else if (phase == CPU_PHASE) drawTemperatureScreen("CPU", cpuTemperatureC); else drawTemperatureScreen("GPU", gpuTemperatureC); }
void requestPcTemperature(const char *request) { if (bluetoothConnected && requestCharacteristic != nullptr) { requestCharacteristic->setValue(request); requestCharacteristic->notify(); } }
class DeskBuddyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer *) override { bluetoothConnected = true; redrawScreen(); } void onDisconnect(BLEServer *server) override { bluetoothConnected = false; redrawScreen(); delay(150); server->startAdvertising(); } };
class TemperatureCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *characteristic) override {
  String received = characteristic->getValue().c_str(); received.trim(); int separator = received.indexOf(':'); if (separator < 1) return;
  String source = received.substring(0, separator), value = received.substring(separator + 1); value.trim(); float temperatureC = value.toFloat(); if (value.length() == 0 || temperatureC < 0.0f || temperatureC > 150.0f) return; source.toUpperCase();
  if (source == "CPU") cpuTemperatureC = temperatureC; else if (source == "GPU") { gpuTemperatureC = temperatureC; if (temperatureC >= 55.0f) coolSinceMs = 0; else if (coolSinceMs == 0) coolSinceMs = millis(); } else return; redrawScreen();
} };
void setupBluetooth() {
  BLEDevice::init("DeskBuddy-PC-Temp"); BLEServer *server = BLEDevice::createServer(); server->setCallbacks(new DeskBuddyServerCallbacks()); BLEService *service = server->createService(SERVICE_UUID);
  requestCharacteristic = service->createCharacteristic(REQUEST_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); requestCharacteristic->addDescriptor(new BLE2902()); requestCharacteristic->setValue("READY");
  BLECharacteristic *temperatureCharacteristic = service->createCharacteristic(TEMPERATURE_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE); temperatureCharacteristic->setCallbacks(new TemperatureCallbacks()); temperatureCharacteristic->setValue(""); service->start();
  BLEAdvertising *advertising = BLEDevice::getAdvertising(); advertising->addServiceUUID(SERVICE_UUID); advertising->setScanResponse(true); advertising->start();
}
void updateFaceAnimation(unsigned long now) {
  if (phase != FACE_PHASE) return; bool yawning = isYawning(now);
  if (!eyesClosed && now >= nextBlinkMs) { eyesClosed = true; blinkEndsMs = now + 140; redrawScreen(); } else if (eyesClosed && now >= blinkEndsMs) { eyesClosed = false; nextBlinkMs = now + random(3500, 8000); redrawScreen(); }
  if (yawning) { if (now >= nextMouthMoveMs) { mouthOpen = !mouthOpen; nextMouthMoveMs = now + YAWN_FRAME_MS; redrawScreen(); } }
  else if (!mouthOpen && now >= nextMouthMoveMs) { mouthOpen = true; mouthEndsMs = now + MOUTH_MOVE_MS; nextMouthMoveMs = now + random(5000, 11000); redrawScreen(); } else if (mouthOpen && now >= mouthEndsMs) { mouthOpen = false; redrawScreen(); }
}
void updateSchedule(unsigned long now) {
  unsigned long elapsed = now - phaseStartedMs;
  if (phase == FACE_PHASE && elapsed >= FACE_PERIOD_MS) { phase = CPU_PHASE; phaseStartedMs = now; eyesClosed = false; mouthOpen = false; requestPcTemperature("GET_CPU_TEMP"); redrawScreen(); }
  else if (phase == CPU_PHASE && elapsed >= CPU_SCREEN_MS) { phase = GPU_PHASE; phaseStartedMs = now; requestPcTemperature("GET_GPU_TEMP"); redrawScreen(); }
  else if (phase == GPU_PHASE && elapsed >= GPU_SCREEN_MS) { phase = FACE_PHASE; phaseStartedMs = now; nextBlinkMs = now + random(3500, 8000); nextMouthMoveMs = now + random(5000, 11000); redrawScreen(); }
}
void setup() { SPI.begin(TFT_SCK_PIN, -1, TFT_MOSI_PIN, TFT_CS_PIN); display.begin(); display.setRotation(1); display.fillScreen(BACKGROUND); randomSeed(esp_random()); phaseStartedMs = millis(); nextBlinkMs = phaseStartedMs + random(3500, 8000); nextMouthMoveMs = phaseStartedMs + random(5000, 11000); redrawScreen(); setupBluetooth(); requestPcTemperature("GET_GPU_TEMP"); }
void loop() { unsigned long now = millis(); updateFaceAnimation(now); updateSchedule(now); }

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