How to make DIY Macropad With Animated Eyes & Productive keys

How to make DIY Macropad With Animated Eyes & Productive keys

11 min read
Quick Navigation
Build a 6-key DIY macropad with animated OLED eyes using XIAO ESP32-S3. Native USB HID — no Python script needed. Plug in and it just works.
Image

What if your macropad could actually react to you?

This is a 6-key DIY macropad built on the XIAO ESP32-S3 with a 0.9" OLED display running animated eyes that blink, look around, and change expression based on exactly which key you press. It works as a native USB HID keyboard — no Python script, no drivers, just plug in and it works like a normal keyboard. Except this one has a personality.

The case is fully 3D printed in Fusion 360. The icon keycaps — Discord, VS Code, Spotify, and media controls — are printed in white PLA with icons. Simple, clean, and surprisingly sharp looking in person.

The best part? No QMK, no VIAL, no configuration software needed on your PC. Flash once, done forever.

Scroll through the steps below — the build is simpler than it looks, and the result is something that genuinely feels like a finished product sitting on your desk. 👀

Before beginning, a huge shoutout to JLCMC for sponsoring.

Supplies

Image

Electronic Components Required:

  1. Seeed Studio XIAO ESP32-S3
  2. 0.9" OLED display
  3. Kailh mechanical switches
  4. USB-C Cable
  5. 3D Printed body & keycaps (see step 1)

Step 1: CAD & 3D Printing

Image

The entire macropad body was designed in Autodesk Fusion 360 and split into two separate parts — the main body and the lid — so both pieces can be printed flat on the bed without any supports needed. The embedded Fusion 360 file is linked below for you to download, modify, or remix as you like.

Both parts were printed in black PLA filament which gives that clean, matte, almost commercial-product finish you see in the final build. Print settings are straightforward — 15% infill is more than enough structural rigidity for a macropad since it's not a load-bearing part, just sitting on your desk. Standard 0.2mm layer height works perfectly fine here. No supports required.

For the keycaps, I didn't design these from scratch — full credit goes to Camilla on Printables for the keycap design. You can download them here: Macropad with Keycaps by Camilla. If you want sharper, cleaner keycap surfaces, Camilla recommends printing at 0.07mm layer height with ironing enabled on all top surfaces — it makes a noticeable difference. I printed mine in white PLA and then filled in the icons using a black permanent marker — simple jugaad that actually looks surprisingly clean in person.

Print the body, print the lid, print the six keycaps, and you're ready for the build.

Step 2: Elevate Your Electronic Projects - JLCMC

Image Image

JLCMC is your one-stop shop for all electronic manufacturing needs, offering an extensive catalog of nearly 600,000 SKUs that cover hardware, mechanical, electronic, and automation components. Their commitment to guaranteeing genuine products, rapid shipping (with most in-stock items dispatched within 24 hours), and competitive pricing truly sets them apart. In addition, their exceptional customer service ensures you always get exactly what you need to bring your projects to life.

For my next project, I’m planning to buy a timing belt from their Transmission Components section.

What I really like is how easy it is to customize the part. On the left side, you can select all the required options, and just below that, you get the complete specification and documentation, so you know exactly what you’re ordering.

JLCMC has recently upgraded their new-user registration benefits, increasing the value of the welcome coupon package to $123 in discount coupons. Whether you’re building DIY electronics, robotics, or mechanical projects, JLCMC has you covered with quality parts and fast delivery. Don’t miss out—visit https://jlcmc.com/?from=RL2 to explore their amazing range of products and grab your discount coupon today!

Step 3: The Build

Image
  1. Start with the lid — press all 6 Gateron mechanical switches into their designated cutouts. They should snap in with a satisfying click and sit flush. No glue needed here; the cutout tolerances hold them firmly in place.
  2. Take your 0.9" SSD1306 OLED display and apply a small amount of hot glue around the edges of the display slot on the lid, then press the display in face-first. Hold it for 30 seconds until the glue sets. Make sure it sits flat and centered — this is what people will look at most.
  3. Now for the switch wiring — and this is where this build is simpler than most macropad projects you'll find online. Normally macropads use a matrix wiring method, where rows and columns of switches share wires to save microcontroller pins. For example, a 3×2 matrix uses only 5 pins instead of 6. It's efficient but requires diode soldering on every switch and more complex firmware logic to scan properly. In this build we skip all of that entirely. Since the XIAO ESP32-S3 has enough GPIO pins available, we wire each switch directly and independently to its own dedicated pin — one wire per switch to a GPIO, and all switches share a single common ground wire. This is called direct pin wiring and it's simpler to solder, simpler to debug, and simpler to code.
  4. Solder a wire from one leg of each switch to its assigned GPIO pin and connect the other leg of all switches to a common GND on the XIAO ESP32-S3. Follow this pin mapping exactly:
    Key 1 (top-left) → D6 — Discord
    Key 2 (top-middle) → D3 — VS Code
    Key 3 (top-right) → **D2 **— Spotify
    Key 4 (bottom-left) → **D1 **— Previous Track
    Key 5 (bottom-middle) → D0 — Play/Pause
    Key 6 (bottom-right) → D8 — Next Track
  5. For the OLED display, solder four wires from the display module to the XIAO ESP32-S3 — VCC to 3.3V, GND to GND, SDA to D4, and SCL to D5. The display runs on I2C so only these two data lines are needed.
  6. Mount the XIAO ESP32-S3 into the designated slot in the base. It should sit snugly. If it feels loose, a small dab of hot glue on the sides will hold it permanently without blocking the USB-C port.
  7. Now carefully snap the lid onto the base, routing all the wires inside cleanly so nothing gets pinched between the two parts. Take your time here — neat wire routing makes the difference between a clean build and a messy one.
  8. Finally, press all 6 keycaps onto the switch stems. They should click on firmly. If any feel loose, a tiny piece of tape around the stem shank fixes it instantly.

That's the full hardware build done. ✅

Step 4: Coding

Image

Now we will upload the program to the Seeed Studio XIAO ESP32-S3.

1. Install Arduino IDE and ESP32 Board Package

  1. Install the latest version of Arduino IDE on your computer.
  2. Open Arduino IDE and install the ESP32 board package:
  3. Go to File → Preferences
  4. In Additional Board Manager URLs, add:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

  1. Then go to:
  2. Tools → Board → Boards Manager
  3. Search for ESP32 and install the ESP32 board package.

2. Select the Board and Port

  1. Open Arduino IDE and configure the board.
  2. Go to:
  3. Tools → Board → ESP32 Arduino → XIAO ESP32S3
  4. Then select the correct COM port:
  5. Tools → Port → Select the port connected to your XIAO ESP32-S3

3. Configure USB Mode — Critical Step

This is the most important setting and the one most people miss. By default the XIAO ESP32-S3 uses UART CDC mode for serial communication, which means your PC sees it as a serial device — not a keyboard. You need to switch it to USB-OTG mode so Windows/Mac recognizes it as a native HID keyboard the moment you plug it in.

In Arduino IDE go to:

  1. Tools → USB Mode → USB-OTG (TinyUSB)

Without this change, the keyboard and media key code will compile and upload fine, but absolutely nothing will happen when you press the keys—the PC simply won't receive any HID input. Set this once and you never need to touch it again for this board.

5. Install Required Libraries

  1. Install the following libraries from the Arduino Library Manager.
  2. Go to Sketch → Include Library → Manage Libraries and install:
  3. Adafruit GFX
  4. Adafruit SSD1306
  5. FluxGarage RoboEyes
  6. These libraries are used for the OLED display, and animated robot eyes.

6. Copy the Project Code

/*
 * ============================================================
 *  Code by: Shahbaz Hashmi Ansari
 *  MACROPAD — XIAO ESP32-S3  (Standalone USB HID — No Python!)
 *  6 Kailh keys (direct wiring, no matrix)
 *  0.96" SSD1306 OLED (I2C) + FluxGarage RoboEyes
 *  Works as a native USB Keyboard + Media Controller
 * ============================================================
 */

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <FluxGarage_RoboEyes.h>

#include "USB.h"
#include "USBHIDKeyboard.h"
#include "USBHIDConsumerControl.h"

// ─── USB HID ────────────────────────────────────────────────
USBHIDKeyboard     Keyboard;
USBHIDConsumerControl ConsumerControl;

// ─── OLED ───────────────────────────────────────────────────
#define SCREEN_WIDTH   128
#define SCREEN_HEIGHT   64
#define OLED_RESET      -1
#define SCREEN_ADDRESS  0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

RoboEyes<Adafruit_SSD1306> roboEyes(display);

// ─── KEY PINS ───────────────────────────────────────────────
//                      Key1  Key2  Key3  Key4  Key5  Key6
const int KEY_PINS[]  = {2,    1,    7,    43,    4,    3,};
const int NUM_KEYS    = 6;

// ─── DEBOUNCE ───────────────────────────────────────────────
#define DEBOUNCE_MS 300
unsigned long lastPress[6] = {0};

// ─── EYE RESET TIMER ────────────────────────────────────────
unsigned long eyeActionTime  = 0;
#define EYE_RESET_MS  3000   // return to idle after 3 s

// ─── FORWARD DECLARATIONS ───────────────────────────────────
void handleKey(int idx);
void resetEyes();
void doOpenPowerShell();
void doVSCodeOpen();
void doOpenSpotify();
void doPrevTrack();
void doPlayPause();
void doNextTrack();

typedef void (*ActionFunc)();
ActionFunc KEY_ACTIONS[] = {
  doOpenPowerShell,   // Key 1 — ROW 1
  doVSCodeOpen,       // Key 2 — ROW 1
  doOpenSpotify,      // Key 3 — ROW 1
  doPrevTrack,        // Key 4 — ROW 2
  doPlayPause,        // Key 5 — ROW 2
  doNextTrack         // Key 6 — ROW 2
};

// =============================================================
//                          SETUP
// =============================================================
void setup() {
  for (int i = 0; i < NUM_KEYS; i++) {
    pinMode(KEY_PINS[i], INPUT_PULLUP);
  }

  Keyboard.begin();
  ConsumerControl.begin();
  USB.begin();

  Wire.begin(5, 6);  // SDA=GPIO5, SCL=GPIO6

  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    // Halt — OLED required for RoboEyes
    while (true) { delay(500); }
  }

  // ── RoboEyes init ──────────────────────────────────────────
  roboEyes.begin(SCREEN_WIDTH, SCREEN_HEIGHT, 30); // 30 fps max
  roboEyes.setAutoblinker(ON, 3, 2);               // blink every 3±2 s
  roboEyes.setIdleMode(ON, 2, 1);                  // look around every 2±1 s
  roboEyes.setMood(DEFAULT);
  roboEyes.setCuriosity(ON);                        // outer eye grows on side-look
  roboEyes.open();
}

// =============================================================
//                         MAIN LOOP
// =============================================================
void loop() {
  // ── Poll keys ──
  for (int i = 0; i < NUM_KEYS; i++) {
    if (digitalRead(KEY_PINS[i]) == LOW) {
      unsigned long now = millis();
      if (now - lastPress[i] > DEBOUNCE_MS) {
        lastPress[i] = now;
        handleKey(i);
      }
    }
  }

  // ── Return eyes to idle state after action timeout ──
  if (eyeActionTime > 0 && millis() - eyeActionTime > EYE_RESET_MS) {
    eyeActionTime = 0;
    resetEyes();
  }

  // ── Drive eye animations (non-blocking) ──
  roboEyes.update();
}

// =============================================================
//  Handle key — execute action then arm eye reset timer
// =============================================================
void handleKey(int idx) {
  KEY_ACTIONS[idx]();
  eyeActionTime = millis();
}

// =============================================================
//  Reset eyes to relaxed idle state
// =============================================================
void resetEyes() {
  roboEyes.setMood(DEFAULT);
  roboEyes.setPosition(DEFAULT);
  roboEyes.setCuriosity(ON);
  roboEyes.setAutoblinker(ON, 3, 2);
  roboEyes.setIdleMode(ON, 2, 1);
}

// =============================================================
//  KEY 1 — PowerShell (Admin)
//  Eye: ANGRY — squinting, focused, ready for battle
// =============================================================
void doOpenPowerShell() {
  roboEyes.setAutoblinker(OFF, 3, 2);
  roboEyes.setIdleMode(OFF, 2, 1);
  roboEyes.setMood(ANGRY);
  roboEyes.setPosition(DEFAULT);

  Keyboard.press(KEY_LEFT_GUI);
  Keyboard.press('x');
  delay(150);
  Keyboard.releaseAll();
  delay(600);
  Keyboard.press('a');
  delay(100);
  Keyboard.releaseAll();
}

// =============================================================
//  KEY 2 — VS Code  (open app + Command Palette)
//  Fix: Win+R → "code" → Enter launches VS Code reliably.
//       Waits for load, then fires Ctrl+Shift+P.
//  Eye: HAPPY + curious — excited coder ready to ship
// =============================================================
void doVSCodeOpen() {
  roboEyes.setAutoblinker(OFF, 3, 2);
  roboEyes.setIdleMode(OFF, 2, 1);
  roboEyes.setMood(HAPPY);
  roboEyes.setCuriosity(ON);
  roboEyes.setPosition(N);   // eyes look up — let's code!

  // Open Run dialog
  Keyboard.press(KEY_LEFT_GUI);
  Keyboard.press('r');
  delay(150);
  Keyboard.releaseAll();
  delay(400);

  // Type VS Code CLI command
  Keyboard.print("code");
  delay(100);
  Keyboard.press(KEY_RETURN);
  delay(100);
  Keyboard.releaseAll();

  // Wait for VS Code to focus/open, then fire command palette
  delay(2000);
  Keyboard.press(KEY_LEFT_CTRL);
  Keyboard.press(KEY_LEFT_SHIFT);
  Keyboard.press('p');
  delay(100);
  Keyboard.releaseAll();
}

// =============================================================
//  KEY 3 — Spotify
//  Eye: HAPPY + anim_laugh — bouncing with the beat
// =============================================================
void doOpenSpotify() {
  roboEyes.setAutoblinker(OFF, 3, 2);
  roboEyes.setIdleMode(OFF, 2, 1);
  roboEyes.setMood(HAPPY);
  roboEyes.anim_laugh();   // eyes bounce up & down

  Keyboard.press(KEY_LEFT_GUI);
  delay(100);
  Keyboard.releaseAll();
  delay(500);
  Keyboard.print("Spotify");
  delay(700);
  Keyboard.press(KEY_RETURN);
  delay(100);
  Keyboard.releaseAll();
}

// =============================================================
//  KEY 4 — Previous Track
//  Eye: look hard left — rewind!
// =============================================================
void doPrevTrack() {
  roboEyes.setAutoblinker(OFF, 3, 2);
  roboEyes.setIdleMode(OFF, 2, 1);
  roboEyes.setMood(DEFAULT);
  roboEyes.setCuriosity(ON);
  roboEyes.setPosition(W);   // look left

  ConsumerControl.press(CONSUMER_CONTROL_SCAN_PREVIOUS);
  delay(100);
  ConsumerControl.release();
}

// =============================================================
//  KEY 5 — Play / Pause
//  Eye: satisfying slow blink — chill
// =============================================================
void doPlayPause() {
  roboEyes.setAutoblinker(OFF, 3, 2);
  roboEyes.setIdleMode(OFF, 2, 1);
  roboEyes.setMood(DEFAULT);
  roboEyes.setPosition(DEFAULT);
  roboEyes.close();
  delay(200);
  roboEyes.open();

  ConsumerControl.press(CONSUMER_CONTROL_PLAY_PAUSE);
  delay(100);
  ConsumerControl.release();
}

// =============================================================
//  KEY 6 — Next Track
//  Eye: look hard right — skip!
// =============================================================
void doNextTrack() {
  roboEyes.setAutoblinker(OFF, 3, 2);
  roboEyes.setIdleMode(OFF, 2, 1);
  roboEyes.setMood(DEFAULT);
  roboEyes.setCuriosity(ON);
  roboEyes.setPosition(E);   // look right

  ConsumerControl.press(CONSUMER_CONTROL_SCAN_NEXT);
  delay(100);
  ConsumerControl.release();
}

Step 5: Satisfying Click Sound

Words can't do this justice — just press play.

These are Kailh Blue mechanical switches — clicky, tactile, and genuinely satisfying to type on. Every keypress gives you that crisp audible feedback that just feels right. It's half the reason to build this over a membrane alternative.

Watch the video above and try not to smile. 😄

Step 6: Working Video and Tutorial

And that's it — your macropad is alive. 👀

Plug it in, watch the eyes blink open, press a key, and see it react. That moment never gets old honestly. What started as a simple productivity tool ended up becoming something that actually has character sitting on your desk.

If you build this, I'd genuinely love to see it. Drop a photo in the comments—especially if you remix the case, change the keycap icons, or add your own eye expressions to the code. That's the best part of open builds like this.

Got stuck somewhere? Leave a comment and I'll help you debug it.

For business or collaboration inquiries, reach out via email at shahbazhashmi006@gmail.com.

Follow me here on Instructables so you don't miss the next build, and if you want to see this macropad actually in action, the full build video is on my YouTube channel.

YouTube: roboattic Lab

See you in the next one. 🔧

Related Topics:iot project

Related Articles

Build a DIY AI Pin: Real-Life Jarvis with ESP32S3iot project
August 25, 2025

Build a DIY AI Pin: Real-Life Jarvis with ESP32S3

Build a Jarvis! This 9-step guide uses ESP32S3 and Gemini for a wearable assistant.

Read the full iot project tutorial: See Project Details
Face Recognition Based Attendance System Using XIAO ESP32S3 Sense Boardiot project
January 07, 2024

Face Recognition Based Attendance System Using XIAO ESP32S3 Sense Board

In this project, we will be using XIAO ESP32S3 Sense Board as our camera input and we will be using OpenCV & Visual Studio for the face detection and as the face is detected it will record the attendance with date and time in CSV file.

Read the full iot project tutorial: See Project Details
How to Build a Desktop Companion Robot | ESP32 S3iot project
February 18, 2026

How to Build a Desktop Companion Robot | ESP32 S3

Build a DIY Desktop Companion Robot with Seeed Studio XIAO ESP32-S3. Create animated eyes that react to PC activity like music and typing.

Read the full iot project tutorial: Follow Tutorial