In diesem Tutorial wird gezeigt, wie der bereits vorgestellte Roboterarm nun mit einem Game Controller gesteuert werden kann. da dort die Grundlagen der Servosteuerung und den Aufbau soweit vorgestellt worden sind, zeige ich nun, wie du den Roboterarm mit einem Bluetooth-Gamecontroller – in unserem Fall dem 2BDVX BG-04 Ultra – präzise und intuitiv steuern kannst.

1. Voraussetzungen
Um dieses Projekt umzusetzen, benötigst du folgende Komponenten und Kenntnisse:
- Hardware:
- Das ESP32 Entwicklungsboard (Kein ESP32-C3 oder S3, da mein Controller nur Bluetooth Classic unterstützt.) mit Erweiterungsboard
- Der fertiggestellte Roboterarm mit MG90s und SG90-Servos (oder ähnlichen).
- Wireless Controller 2BDVX BG-04 Ultra (oder ein kompatibler Bluetooth Classic Controller). Herstellerdetails: FCC ID 2BDVX-BG-04
- Separate 5V-Stromversorgung für die Servos!
- Jumperkabel.
- Software:
- Arduino IDE (mit installiertem ESP32 Board Support).
- Arduino IDE (mit installiertem ESP32 Board Support).
- Verdrahtung:
2. Einrichtung der Bluepad32 Library
Da unser Controller über Bluetooth Classic kommuniziert, nutzen wir die Bibliothek Bluepad32.
Erst zusätzliche Boardverwaltung URL in Einstellungen eintragen
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
Dann unter Boardverwaltung! die Bibliothek „esp32_bluepad32“ installieren. Solltet ihr noch nie mit ESP32 in der Arduino IDE gearbeitet haben dann auch noch die esp32 von Espressif

Quelle und Details zur Bibliothek einrichtung, ist hier vollumfassend erklärt: ricardoquesada/bluepad32
Nach der installation gibt es einen weiteren Boardverwaltungsauswahl Punkt. Hier das angeschlossenen ESP32 Dev Modul auswählen

Controller-Test (Optional, aber empfohlen)
Um sicherzustellen, dass dein Controller korrekt erkannt wird, solltest du den beiliegende Test-Sketch Controller hochladen.

Sobald du den Seriellen Monitor öffnest und Ihren Controller in den Kopplungsmodus mit „Share und (X) PS-Taste“ versetzt, wird der ESP32 die Verbindung herstellen und die Joystick-Werte angezeigt.

3. Der Steuerungscode: Bluepad32 trifft Servo
Da die Testverbindung erfolgreich war, können wir nun die Steuerungssignale des Controllers direkt mit der Logik zur Ansteuerung des Roboterarms verknüpfen.
Der folgende Sketch kombiniert die Logik des Gamecontrollers (ausgelesen durch Bluepad32) mit der Steuerung der vier Roboterarm-Achsen (Basis, Schulter, Ellenbogen, Greifer) aus dem vorherigen Tutorial über die ESP32Servo.h-Bibliothek.
Roboterarm-Steuerung mit ESP32 und BG-04 Controller
Dieser Sketch geht von einem Standard-4-DOF-Roboterarm (DOF=Degrees of Freedom) aus, der folgende Achsen über die Joysticks und Schultertasten steuert:
| Armfunktion | Controller-Steuerung |
|---|---|
| Basis (Rotation) | Linker Joystick, Horizontal (X) |
| Schulter (Auf/Ab) | Linker Joystick, Vertikal (Y) |
| Ellenbogen (Auf/Ab) | Rechter Joystick, Vertikal (RY) |
| Greifer (Öffnen/Schließen) | Tasten L1 (Öffnen) und R1 (Schließen) |
- Servos und Stromversorgung: SG90-Servos benötigen 5V. Versorgen Sie die Servos immer über eine separate, externe 5V-Stromversorgung (nicht den ESP32-Pin!). Verbinde auch unbedingt die GND (Masse) dieser externen Stromversorgung mit dem GND des ESP32, um eine gemeinsame Referenz zu gewährleisten.
- Pins anpassen: Passen die definierten GPIO-Pins an die tatsächliche Verkabelung Ihres Roboterarms an.
- Invertierung und Min/Max: Die Joystick-Y-Achsen (vertikal) sind oft invertiert (nach oben = negativer Wert). Dies ist im Code berücksichtigt (map(joystickY, 512, -512, …)). Möglicherweise müssen Sie die Minimal- und Maximalwinkel (z. B. 20 und 160 Grad) im Code an Ihren spezifischen Roboterarm anpassen.
Quellcode:
#include <Bluepad32.h>
#include <ESP32Servo.h>
// =======================
// 1. PIN-KONFIGURATION
// =======================
#define PIN_SERVO_BASIS 16 // Basis Rotation (Linker Stick X)
#define PIN_SERVO_SCHULTER 15 // Schulter/Unterarm (Linker Stick Y)
#define PIN_SERVO_ELLENBOGEN 14 // Ellenbogen/Oberarm (Rechter Stick RY)
#define PIN_SERVO_GREIFER 13 // Greifer/Klaue (L1/R1 Tasten)
// =======================
// 2. SERVO-WINKEL & GESCHWINDIGKEIT
// =======================
#define SERVO_MIN_WINKEL 20
#define SERVO_MAX_WINKEL 160
#define GREIFER_WINKEL_OFFEN 140
#define GREIFER_WINKEL_GESCHL 10
// GESCHWINDIGKEITEN: Größere Zahl = schnellere Bewegung
#define ALLGEMEIN_STEP_SIZE 2 // Schrittweite für Basis und Ellenbogen (z.B. 2 Grad)
#define SCHULTER_STEP_SIZE 1 // Schrittweite für die Schulter
// ZUCKEN/JITTER-UNTERDRÜCKUNG
// Wenn die Joystick-Achse diesen Wert unterschreitet, wird sie als 0 behandelt.
#define JOYSTICK_TOLERANZ 10 // Joystick-Einheiten (-512 bis 512)
// =======================
// 3. GLOBALE OBJEKTE & WINKEL-SPEICHER
// =======================
ControllerPtr myControllers[BP32_MAX_GAMEPADS];
Servo servoBasis;
Servo servoSchulter;
Servo servoEllenbogen;
Servo servoGreifer;
// Startwert Winkel
int aktuellerBasisWinkel = 90;
int aktuellerSchulterWinkel = 90;
int aktuellerEllenbogenWinkel = 90;
int aktuellerGreiferWinkel = GREIFER_WINKEL_OFFEN;
// =======================
// 4. BLUEPAD32 CALLBACKS
// =======================
void onConnectedController(ControllerPtr ctl) {
bool foundEmptySlot = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == nullptr) {
Serial.printf("CALLBACK: Controller verbunden, Index=%d\n", i);
ControllerProperties properties = ctl->getProperties();
Serial.printf("Controller Modell: %s, VID=0x%04x, PID=0x%04x\n", ctl->getModelName().c_str(), properties.vendor_id,
properties.product_id);
myControllers[i] = ctl;
foundEmptySlot = true;
break;
}
}
if (!foundEmptySlot) {
Serial.println("CALLBACK: Controller verbunden, aber kein leerer Slot gefunden");
}
}
void onDisconnectedController(ControllerPtr ctl) {
bool foundController = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == ctl) {
Serial.printf("CALLBACK: Controller getrennt von Index=%d\n", i);
myControllers[i] = nullptr;
foundController = true;
break;
}
}
if (!foundController) {
Serial.println("CALLBACK: Controller getrennt, aber nicht in myControllers gefunden");
}
}
// =======================
// 5. STEUERUNGSFUNKTION (Mit Toleranz und Sanfter Bewegung)
// =======================
void controlRobotArm(ControllerPtr ctl) {
// Joystick-Werte mit Toleranz abfragen
int joystickX = ctl->axisX();
int joystickY = ctl->axisY();
int joystickRY = ctl->axisRY();
// Toleranz anwenden: Wenn der Wert nahe 0 ist, auf 0 setzen, um Zucken zu vermeiden
if (abs(joystickX) < JOYSTICK_TOLERANZ) joystickX = 0;
if (abs(joystickY) < JOYSTICK_TOLERANZ) joystickY = 0;
if (abs(joystickRY) < JOYSTICK_TOLERANZ) joystickRY = 0;
// -----------------------------------------------------------
// I. ANALOGE STEUERUNG (Zielwinkel berechnen)
// -----------------------------------------------------------
// Zielwinkel MUSS IMMER berechnet werden, auch wenn die Achse 0 ist, um den "Haltepunkt" zu kennen
int zielBasisWinkel = map(joystickX, -512, 512, SERVO_MIN_WINKEL, SERVO_MAX_WINKEL);
int zielSchulterWinkel = map(joystickY, 512, -512, SERVO_MIN_WINKEL, SERVO_MAX_WINKEL);
int zielEllenbogenWinkel = map(joystickRY, 512, -512, SERVO_MIN_WINKEL, SERVO_MAX_WINKEL);
// -----------------------------------------------------------
// II. DIGITALE STEUERUNG (Greifer)
// -----------------------------------------------------------
if (ctl->l1()) {
aktuellerGreiferWinkel = GREIFER_WINKEL_OFFEN;
}
else if (ctl->r1()) {
aktuellerGreiferWinkel = GREIFER_WINKEL_GESCHL;
}
servoGreifer.write(aktuellerGreiferWinkel);
// -----------------------------------------------------------
// III. SANFTE BEWEGUNG (Inkrementelle Anpassung)
// -----------------------------------------------------------
// Funktion zur schrittweisen Bewegung
auto moveServo = [](int& aktuellerWinkel, int zielWinkel, int schritt) {
if (aktuellerWinkel != zielWinkel) {
if (aktuellerWinkel < zielWinkel) {
// Erhöhen
aktuellerWinkel = aktuellerWinkel + schritt;
// Sicherstellen, dass wir das Ziel nicht überschreiten
if (aktuellerWinkel > zielWinkel) aktuellerWinkel = zielWinkel;
} else {
// Verringern
aktuellerWinkel = aktuellerWinkel - schritt;
// Sicherstellen, dass wir das Ziel nicht unterschreiten
if (aktuellerWinkel < zielWinkel) aktuellerWinkel = zielWinkel;
}
// Winkel auf den zulässigen Bereich beschränken
aktuellerWinkel = constrain(aktuellerWinkel, SERVO_MIN_WINKEL, SERVO_MAX_WINKEL);
return true; // Bewegung durchgeführt
}
return false; // Keine Bewegung notwendig
};
// BASIS
if (moveServo(aktuellerBasisWinkel, zielBasisWinkel, ALLGEMEIN_STEP_SIZE)) {
servoBasis.write(aktuellerBasisWinkel);
}
// SCHULTER (Nutzt den LANGSAMEREN Schritt)
if (moveServo(aktuellerSchulterWinkel, zielSchulterWinkel, SCHULTER_STEP_SIZE)) {
servoSchulter.write(aktuellerSchulterWinkel);
}
// ELLENBOGEN
if (moveServo(aktuellerEllenbogenWinkel, zielEllenbogenWinkel, ALLGEMEIN_STEP_SIZE)) {
servoEllenbogen.write(aktuellerEllenbogenWinkel);
}
// -----------------------------------------------------------
// IV. SERIELLE AUSGABE
// -----------------------------------------------------------
Serial.printf("Basis: %3d° | Schulter: %3d° | Ellenbogen: %3d° | Greifer: %3d°\n",
aktuellerBasisWinkel, aktuellerSchulterWinkel, aktuellerEllenbogenWinkel, aktuellerGreiferWinkel);
}
void processControllers() {
for (auto myController : myControllers) {
if (myController && myController->isConnected() && myController->hasData()) {
if (myController->isGamepad()) {
controlRobotArm(myController);
}
}
}
}
// =======================
// 6. SETUP & LOOP (Unverändert)
// =======================
void setup() {
Serial.begin(115200);
Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
// Servos an die GPIO-Pins binden
servoBasis.attach(PIN_SERVO_BASIS);
servoSchulter.attach(PIN_SERVO_SCHULTER);
servoEllenbogen.attach(PIN_SERVO_ELLENBOGEN);
servoGreifer.attach(PIN_SERVO_GREIFER);
// Setze Servos auf Startposition (90 Grad Mitte)
servoBasis.write(90);
servoSchulter.write(90);
servoEllenbogen.write(90);
servoGreifer.write(GREIFER_WINKEL_OFFEN);
// Initialisiere die aktuellen Winkel-Speicher
aktuellerBasisWinkel = 90;
aktuellerSchulterWinkel = 90;
aktuellerEllenbogenWinkel = 90;
// Setup the Bluepad32 callbacks
BP32.setup(&onConnectedController, &onDisconnectedController);
BP32.enableVirtualDevice(false);
}
void loop() {
bool dataUpdated = BP32.update();
// Wichtig: Aufruf in jedem Loop für die schrittweise Bewegung
processControllers();
// Kleines Delay für Fließfähigkeit (5ms)
delay(5);
}Youtube Video:
