MySwitchPad ist ein komplett selbstgebautes und individuell konfigurierbares Makro-Pad. Es eignet sich ideal für Shortcuts beim Programmieren, Makros beim Streamen oder im Alltag für Mediensteuerung. Das MySwitchPad bietet volle Flexibilität in einem kompakten 3D gedruckten Gehäuse.
Hardware
Das Herzstück des Pads bildet ein ESP32 (CP2102 Chip) mit USB-C-Anschluss, der alle Komponenten verbindet und für die Stromversorgung zuständig ist. Ausgestattet ist das Board mit 12 mechanischen Switches und 3 Drehreglern (KY-040) samt passenden Keycaps. Zusätzlich sorgt ein RGB LED Ring mit 12 LEDs für die Beleuchtung im inneren des Gehäuses.
Hier ist eine Übersicht der Verkabelung, die ich mit dem Tool Wokwi erstellt habe:

Da die Switches einzeln zu viele Anschlüsse belegen würden, sind sie in einer Matrix verschaltet. Auf der Abbildung fehlen jedoch noch die verbauten Dioden, damit die Matrixschaltung bei gleichzeitigen Tastendrücken korrekt funktioniert. Durch diese Schaltung reduziert sich die Anzahl an benötigten Pins auf 7. Mit der Matrix-Schaltung, den drei Drehreglern und dem RGB Ring sind alle nutzbaren Pins des ESP32 vollständig belegt. Die verbleibenden freien Pins sind für den ESP selbst reserviert (Reset-Pins) oder nicht praktikabel für mein Projekt (Input-Only).
Das Case und die Keycaps (mit Logos) für die Switches habe ich modelliert und schließlich 3D gedruckt. Hier ist ein Bild des gedruckten Produkts:


Bei Interesse an den 3D-Druck Dateien kann man mich gerne über den Kontakt im Impressum kontaktieren :)
Firmware
Das Pad fungiert als Bluetooth-Tastatur. Daher ist es (außer zum Flashen) nicht erforderlich, dass das Pad direkt mit dem Gerät verbunden ist. Durch die Bluetooth Verbindung wird das Pad als echte Tastatur durch das verbundene Gerät wahrgenommen.
Die Firmware gibt es in zwei Ausführungen:
- Als Standardversion, wobei RGB und Belegung der Tasten und Drehregler in der Firmware angepasst werden muss
- Mit Web-Serial, wodurch diese Konfigurationen über eine Demo auf diesem Blog vorgenommen werden kann (in Arbeit)
BleKey-Library:
Die für die Bluetooth-Kommunikation genutzte Bibliothek habe ich selbst entwickelt. Sie ist als Open-Source-Projekt unter BleKey | GitHub zu finden. Ursprünglich hatte ich eine andere Bibliothek verwendet, die jedoch seit Jahren nicht mehr gewartet wurde. Nachdem ich die Bibliothek auf den neusten Stand gebracht hatte und weitere Verbesserungen und Erweiterungen vornahm, bemerkte ich schnell, dass ich dabei war, die komplette Bibliothek umzuschreiben. Daraufhin begann ich meine eigene Bibliothek zu entwickeln, die ich auf diese Art von Projekten zuschnitt. Mein Hauptfokus lag hierbei auf eine simple Benutzung und Übersichtlichkeit der Bibliothek. Seht euch gerne die Bibliothek an. Über einen Star würde ich mich sehr freuen.
Standard-Fimware:
Um die Tastenbelegung zu ändern, müssen lediglich die Konstanten unter und in der Firmware angepasst werden. Die Farbe des LED Ring lässt sich über die Variable im Setup-Bereich anpassen. Alle verfügbaren Tasten finden sich in einer übersichtlichen Liste in meiner Bibliothek BleKey.
// ESP32 Firmware for MySwitchPad
#include <Adafruit_NeoPixel.h>
#include <ESP32Encoder.h>
#include <BleKey.h>
// ==========================================
// HARDWARE CONFIGURATION
// ==========================================
// Pixel Ring with 12 pixels (VIN, GND, D12)
#define PIN 12
#define NUMPIXELS 12
Adafruit_NeoPixel strip(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
// Buttons Matrix 3x4
// Rows: D13, D14, D32 | Cols: D27, D26, D25, D33
const int rowPins[3] = {13, 14, 32};
const int colPins[4] = {27, 26, 25, 33};
bool buttonState[3][4] = {false};
unsigned long lastDebounceTime[3][4] = {0};
const unsigned long debounceDelay = 50;
// Encoder 1 (SW: D23, DT: D22, CLK: D21)
#define ENC1_SW 23
#define ENC1_DT 22
#define ENC1_CLK 21
// Encoder 2 (SW: D19, DT: D18, CLK: D5)
#define ENC2_SW 19
#define ENC2_DT 18
#define ENC2_CLK 5
// Encoder 3 (SW: TX2, DT: RX2, CLK: D4)
// Note: TX2 == GPIO 17, RX2 == GPIO 16
#define ENC3_SW 17
#define ENC3_DT 16
#define ENC3_CLK 4
ESP32Encoder encoder1;
ESP32Encoder encoder2;
ESP32Encoder encoder3;
int32_t lastEncCount[3] = {0, 0, 0};
const int encSwPins[3] = {ENC1_SW, ENC2_SW, ENC3_SW};
bool encButtonState[3] = {false, false, false};
unsigned long lastEncDebounceTime[3] = {0, 0, 0};
// ==========================================
// SOFTWARE CONFIGURATION
// ==========================================
bool bluetoothConnected = false;
uint32_t myColor;
unsigned long lastBreathTime = 0;
int currentLightLevel = 20;
int breathDirection = 5;
// Bluetooth / HID
BleKey bleKeyboard;
// HID keycodes for Buttons
const Key btButtonKeys[3][4] = {
{KEY_F13, KEY_F14, KEY_F15, KEY_F16},
{KEY_F17, KEY_F18, KEY_F19, KEY_F20},
{KEY_F21, KEY_F22, KEY_F23, KEY_F24}
};
// HID keycodes for Encoders (CW, CCW, Button)
const Key btEncoders[3][3] = {
{KEY_MEDIA_BRIGHTNESS_UP, KEY_MEDIA_BRIGHTNESS_DOWN, Key(0)},
{KEY_MEDIA_NEXT_TRACK, KEY_MEDIA_PREVIOUS_TRACK, KEY_MEDIA_PLAY_PAUSE},
{KEY_MEDIA_VOLUME_UP, KEY_MEDIA_VOLUME_DOWN, KEY_MEDIA_MUTE}
};
// ==========================================
// SETUP
// ==========================================
void setup() {
bleKeyboard.begin("MySwitchPad");
bleKeyboard.setBatteryLevel(100);
// Setup Button Matrix (3x4)
for (int i = 0; i < 3; i++) {
pinMode(rowPins[i], INPUT_PULLUP);
}
for (int j = 0; j < 4; j++) {
pinMode(colPins[j], INPUT);
}
// Setup Encoders
ESP32Encoder::useInternalWeakPullResistors = puType::up; // Hardware-Timer
encoder1.attachSingleEdge(ENC1_DT, ENC1_CLK);
encoder2.attachSingleEdge(ENC2_DT, ENC2_CLK);
encoder3.attachSingleEdge(ENC3_DT, ENC3_CLK);
// Hardware-Glitch-Filter
encoder1.setFilter(1023);
encoder2.setFilter(1023);
encoder3.setFilter(1023);
encoder1.setCount(0);
encoder2.setCount(0);
encoder3.setCount(0);
for (int i = 0; i < 3; i++) {
pinMode(encSwPins[i], INPUT_PULLUP);
}
// Setup NeoPixel
strip.begin();
strip.setBrightness(100);
myColor = strip.Color(110, 0, 190);
strip.show();
}
// ==========================================
// MAIN LOOP
// ==========================================
void loop() {
if (!bluetoothConnected) {
handleBluetooth();
updateBreathAnim(myColor, 15);
} else {
checkButtons();
checkEncoders();
checkConnection();
}
}
// ==========================================
// FUNCTIONS
// ==========================================
void handleBluetooth() {
if (bleKeyboard.isConnected()) {
bluetoothConnected = true;
resetEncoders();
strip.fill(myColor);
strip.show();
}
}
void resetEncoders() {
encoder1.setCount(0);
encoder2.setCount(0);
encoder3.setCount(0);
lastEncCount[0] = 0;
lastEncCount[1] = 0;
lastEncCount[2] = 0;
}
void checkConnection() {
if (!bleKeyboard.isConnected()) {
bluetoothConnected = false;
currentLightLevel = 20;
breathDirection = 5;
}
}
void checkButtons() {
for (int i = 0; i < 4; i++) {
// Set Low on current column
pinMode(colPins[i], OUTPUT);
digitalWrite(colPins[i], LOW);
for (int j = 0; j < 3; j++) {
bool currentState = (digitalRead(rowPins[j]) == LOW); // LOW = pressed
// Debounce-Check
if (currentState != buttonState[j][i]) {
if (millis() - lastDebounceTime[j][i] > debounceDelay) {
buttonState[j][i] = currentState;
lastDebounceTime[j][i] = millis();
if (currentState == true) {
Key key = btButtonKeys[j][i];
if (key.code() != 0) {
bleKeyboard.tap(key);
}
}
}
}
}
// No diodes: float the column (INPUT). Do not drive it HIGH — two pressed keys would short a HIGH column to a LOW one.
pinMode(colPins[i], INPUT);
}
}
void checkEncoders() {
int32_t encCounts[3] = {encoder1.getCount(), encoder2.getCount(), encoder3.getCount()};
for (int i = 0; i < 3; i++) {
if (encCounts[i] != lastEncCount[i]) {
int32_t delta = encCounts[i] - lastEncCount[i];
const Key key = (delta > 0) ? btEncoders[i][0] : btEncoders[i][1];
if (key.code() != 0) {
bleKeyboard.tap(key);
}
lastEncCount[i] = encCounts[i];
}
// Check Encoder-Taster
bool currentButtonState = (digitalRead(encSwPins[i]) == LOW); // LOW = pressed
if (currentButtonState != encButtonState[i]) {
if (millis() - lastEncDebounceTime[i] > debounceDelay) {
encButtonState[i] = currentButtonState;
lastEncDebounceTime[i] = millis();
if (currentButtonState == true) {
const Key key = btEncoders[i][2];
if (key.code() != 0) {
bleKeyboard.tap(key);
}
}
}
}
}
}
// ==========================================
// LIGHT
// ==========================================
void updateBreathAnim(uint32_t color, int speed) {
if (millis() - lastBreathTime >= speed) {
lastBreathTime = millis();
currentLightLevel += breathDirection;
if (currentLightLevel >= 255) {
currentLightLevel = 255;
breathDirection = -5;
} else if (currentLightLevel <= 20) {
currentLightLevel = 20;
breathDirection = 5;
}
float factor = (float)currentLightLevel / 255.0;
strip.fill(factorColor(color, factor));
strip.show();
}
}
uint32_t factorColor(uint32_t color, float factor) {
uint8_t r = (uint8_t)(color >> 16);
uint8_t g = (uint8_t)(color >> 8);
uint8_t b = (uint8_t)color;
return strip.Color(r * factor, g * factor, b * factor);
}
Web-Serial:
An einer Firmware Version inklusive Web-Serial Schnittstelle arbeite ich aktuell noch. Für die Konfiguration wird der ESP32 dann kurz mit dem PC per Kabel verbunden. Die zugehörige Konfigurations-Page werde ich später als Demo auf diesem Blog bereitstellen.
Zusätzliche Software
Um das Makro-Pad im Alltag effektiv zu nutzen, lässt es sich hervorragend mit zusätzlicher Software auf dem PC kombinieren. So könnt ihr beispielsweise bestimmte Anwendungen direkt per Tastendruck starten oder die Lautstärke einzelner Programme präzise über die Drehregler steuern. Je nach verwendetem Betriebssystem und persönlichem Workflow finden sich im Netz zahlreiche praktische Tools, die genau diese Brücke zwischen Tastendruck und gewünschter Aktion schlagen.
Momentan verwende ich AutoHotKey für die Anwendungssteuerung und Volumey für deren Lautstärke unter Windows.