Community project

Vibration Feedback Macro Keyboard

alienstig

Published July 14, 2026

ESP322 components5 assembly steps
Remix this project

This project builds a macro keyboard with visual feedback using an ESP32, a 4×4 matrix keypad, and a traffic light module. Each key press triggers a specific macro command while the traffic light provides real-time status indication, making it ideal for streamlining repetitive tasks in creative workflows, gaming, or productivity applications.

The guide includes a complete wiring diagram showing how to connect the keypad rows and columns to the ESP32 GPIO pins and how to wire the red, yellow, and green LEDs to their respective pins. You'll receive a full parts list, step-by-step assembly instructions, and ready-to-deploy firmware that handles keypad scanning, light state management, and USB communication.

Wiring diagram

Interactive · read-only
Wiring diagram for Vibration Feedback Macro Keyboard

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
4x4 Matrix Keypad1A 16-button (4 rows × 4 columns) membrane matrix keypad that uses 8 digital I/O lines (4 row + 4 column) to scan all keys. No dedicated power rail is required — rows and columns are driven directly by GPIO. Compatible with the Arduino Keypad library.
Traffic Light1Three-channel red/yellow/green traffic-light LED module. Common Arduino modules expose one shared ground and three digital control pins; many boards include current-limiting resistors, while discrete-LED builds should still use one resistor per LED.

Assembly

5 steps
  1. 准备材料

    准备好以下材料:ESP32 DevKit v1 开发板、4×4 矩阵薄膜键盘模块、红绿灯三色 LED 模块(内置限流电阻)、若干杜邦线(母对母、母对公)、面包板(可选)。

    • Tip: 大部分淘宝/京东购买的红绿灯模块已内置限流电阻,上面丝印写有 R/Y/G 和 GND。如果使用散装 LED 请自行添加 220 Ω 限流电阻。
  2. 连接交通灯模块

    将红绿灯 LED 模块按以下方式连接到 ESP32: - GND → ESP32 GND - RED → ESP32 GPIO16 - YELLOW → ESP32 GPIO17 - GREEN → ESP32 GPIO18

    • Tip: 可以先只接一个 LED 测试,确认方向正确后再接其余两个。
    • 注意极性,不要接反 GND;GPIO 为 3.3V 逻辑,确认模块兼容。
  3. 连接 4×4 键盘

    键盘排针从左至右通常标注 R1 R2 R3 R4 C1 C2 C3 C4(共 8 根)。按以下对应关系接线: - R1 → GPIO32 - R2 → GPIO33 - R3 → GPIO25 - R4 → GPIO26 - C1 → GPIO27 - C2 → GPIO14 - C3 → GPIO13 - C4 → GPIO4

    • Tip: 薄膜键盘排线顺序视品牌而定,若按键错乱可在代码中调整 keys[][] 映射表。
    • Tip: 无需额外上拉电阻,Keypad 库内部已设置 INPUT_PULLUP。
  4. USB 供电与上电检查

    用 Micro-USB / Type-C 数据线将 ESP32 连接到电脑。上电后确认 ESP32 上的电源 LED 亮起,三色灯无异常发光(此时应全灭)。

    • 上电前再次检查接线,特别是电源和 GND,避免短路损坏开发板。
  5. 部署固件并验证

    在 Schematik 中点击 Deploy 按钮编译并烧录固件。烧录完成后打开串口监视器(115200 baud),会看到欢迎菜单。按下键盘上的 1、2、3 分别点亮红、黄、绿灯;按 A 启动自动循环模式,每 3 秒切换一次。

    • Tip: 按 # 可随时在串口打印当前状态。
    • Tip: 按 B 停止自动循环。
    • Tip: 若灯颜色与预期不符,检查 RED/YELLOW/GREEN 接线是否对调。

Firmware

ESP32
firmware.cppDeploy to device
#include <Arduino.h>
#include <Keypad.h>

// ─── Traffic Light Pins ───────────────────────────────────────────────────────
#define RED_PIN    16
#define YELLOW_PIN 17
#define GREEN_PIN  18

// ─── Keypad Pins ──────────────────────────────────────────────────────────────
#define R1_PIN 32
#define R2_PIN 33
#define R3_PIN 25
#define R4_PIN 26
#define C1_PIN 27
#define C2_PIN 14
#define C3_PIN 13
#define C4_PIN  4

// ─── Keypad Layout ────────────────────────────────────────────────────────────

// Hoisted type definitions
enum LightState { STATE_RED, STATE_YELLOW, STATE_GREEN, STATE_OFF };


// Forward declarations
void setLight(LightState state);
void printStatus(LightState state);
LightState autoStep(int step);

const byte ROWS = 4;
const byte COLS = 4;

char keys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};

byte rowPins[ROWS] = { R1_PIN, R2_PIN, R3_PIN, R4_PIN };
byte colPins[COLS] = { C1_PIN, C2_PIN, C3_PIN, C4_PIN };

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// ─── Traffic Light State ──────────────────────────────────────────────────────

LightState currentState = STATE_OFF;

void setLight(LightState state) {
  digitalWrite(RED_PIN,    LOW);
  digitalWrite(YELLOW_PIN, LOW);
  digitalWrite(GREEN_PIN,  LOW);

  switch (state) {
    case STATE_RED:    digitalWrite(RED_PIN,    HIGH); break;
    case STATE_YELLOW: digitalWrite(YELLOW_PIN, HIGH); break;
    case STATE_GREEN:  digitalWrite(GREEN_PIN,  HIGH); break;
    case STATE_OFF:    /* all off */                   break;
  }
  currentState = state;
}

void printStatus(LightState state) {
  switch (state) {
    case STATE_RED:    Serial.println("[RED]    🔴  停止 / STOP");    break;
    case STATE_YELLOW: Serial.println("[YELLOW] 🟡  注意 / CAUTION"); break;
    case STATE_GREEN:  Serial.println("[GREEN]  🟢  通行 / GO");      break;
    case STATE_OFF:    Serial.println("[OFF]    ⚫  全灭 / ALL OFF");  break;
  }
}

// ─── Auto Cycle Variables ─────────────────────────────────────────────────────
bool autoCycle    = false;
unsigned long autoTimer = 0;
const unsigned long AUTO_INTERVAL = 3000; // 3 s per phase
int autoCycleStep = 0;                    // 0=RED 1=YELLOW 2=GREEN

LightState autoStep(int step) {
  switch (step % 3) {
    case 0: return STATE_RED;
    case 1: return STATE_YELLOW;
    case 2: return STATE_GREEN;
    default: return STATE_OFF;
  }
}

// ─── Setup ────────────────────────────────────────────────────────────────────
void setup() {
  Serial.begin(115200);
  delay(200);

  pinMode(RED_PIN,    OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(GREEN_PIN,  OUTPUT);

  setLight(STATE_OFF);

  Serial.println("========================================");
  Serial.println("  🚦 Vibe Coding Keyboard  🎹");
  Serial.println("========================================");
  Serial.println("Keys:");
  Serial.println("  1 = 🔴 RED      2 = 🟡 YELLOW   3 = 🟢 GREEN");
  Serial.println("  0 / * = ⚫ OFF");
  Serial.println("  A = 🔄 Auto Cycle (3 s/phase)");
  Serial.println("  B = ⏹  Stop Auto Cycle");
  Serial.println("  # = Print current status");
  Serial.println("========================================");
}

// ─── Loop ─────────────────────────────────────────────────────────────────────
void loop() {
  // Auto-cycle tick
  if (autoCycle && millis() - autoTimer >= AUTO_INTERVAL) {
    autoTimer = millis();
    autoCycleStep++;
    LightState next = autoStep(autoCycleStep);
    setLight(next);
    Serial.print("[AUTO] → ");
    printStatus(next);
  }

  // Keypad scan
  char key = keypad.getKey();
  if (!key) return;

  Serial.print("Key pressed: ");
  Serial.println(key);

  switch (key) {
    case '1':
      autoCycle = false;
      setLight(STATE_RED);
      printStatus(STATE_RED);
      break;

    case '2':
      autoCycle = false;
      setLight(STATE_YELLOW);
      printStatus(STATE_YELLOW);
      break;

    case '3':
      autoCycle = false;
      setLight(STATE_GREEN);
      printStatus(STATE_GREEN);
      break;

    case '0':
    case '*':
      autoCycle = false;
      setLight(STATE_OFF);
      printStatus(STATE_OFF);
      break;

    case 'A':
      autoCycle    = true;
      autoCycleStep = 0;
      autoTimer    = millis() - AUTO_INTERVAL; // fire immediately
      Serial.println("[AUTO] Cycle started — RED→YELLOW→GREEN, 3 s each");
      break;

    case 'B':
      autoCycle = false;
      setLight(STATE_OFF);
      Serial.println("[AUTO] Cycle stopped.");
      break;

    case '#':
      Serial.print("Current: ");
      printStatus(currentState);
      break;

    default:
      // Other keys (4-9, C, D) do nothing — extend as you like
      break;
  }
}

“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.

Open in Schematik