roboattic Lab
Published © GPL3+

DIY Macropad with REACTIVE Animated Eyes! πŸ‘€

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.

BeginnerFull instructions provided5 hours22

Things used in this project

Hardware components

Seeed Studio XIAO ESP32S3 Sense
Seeed Studio XIAO ESP32S3 Sense
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion

Story

Read more

Custom parts and enclosures

Macropad Base

Macropad Lid

Schematics

Circuit Diagram

Code

macropad.ino

C/C++
/*
 * ============================================================
 *  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 32 s
  roboEyes.setIdleMode(ON, 2, 1);                  // look around every 21 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();
}

Credits

roboattic Lab
22 projects β€’ 17 followers
YouTube Content Creator Robotics Enthusiast

Comments