Einen Roboterarm per Software steuern: Pose, Bewegungsablauf, Teaching und Web-Oberfläche
Einen Roboterarm per Software steuern: Pose, Bewegungsablauf, Teaching und Web-Oberfläche
Artikel-Übersicht
Ein Roboterarm, der sich im Demo-Video elegant bewegt, ist schnell gekauft. Ein Arm, der sich mit eigener Software zuverlässig, ruckfrei und ohne Schaden bewegt, ist deutlich mehr Arbeit – und zwar fast nie dort, wo man sie erwartet. Die Kinematik ist gelöst, die Bibliotheken existieren. Die Zeit geht in Fragen wie diese: Was passiert, wenn ich die Motoren abschalte? Warum kriecht der Arm beim Halten nach unten? Warum ruckelt er bei langsamer Fahrt, aber nicht bei schneller? Und wie verhindere ich, dass ein Klick im Browser den Greifer gegen seinen Anschlag fährt?
Dieser Artikel geht den Weg von unten nach oben durch – am konkreten Beispiel des Seeed Studio reBot Arm B601-RS, einem 6-achsigen Open-Source-Arm mit RobStride-Motoren am CAN-Bus. Die Prinzipien gelten aber für praktisch jeden Arm mit intelligenten Servomotoren. Wir starten bei der Kommunikationskette und den Regelmodi, fahren dann eine einzelne Pose an, halten sie, verketten Posen zu Abläufen, bringen dem Arm im Teaching Mode Bewegungen bei, glätten die Aufnahmen und planen schließlich in Millimetern statt in Gelenkwinkeln. Danach kommen Greifer, Ruckeln und Überwachung, und am Ende die Web-Oberfläche, die das alles bedienbar macht.
Die Grundlage sind eigene Messungen und Fehler aus dem Aufbau einer kompletten Steuerung mit Python, FastAPI und einer Browser-Oberfläche. Wo im Text Zahlen stehen, stammen sie aus diesem Aufbau oder aus der Herstellerdokumentation. Wenn dich die Architekturfragen hinter der Web-Schicht interessieren, lohnt ein Blick in API-Design: Schnittstellen entwerfen, die man nicht bereut und Clean Architecture – beides spielt hier konkret eine Rolle.
Der Arm: reBot Arm B601-RS im Überblick
Der B601-RS ist ein 6-achsiger Arm mit zusätzlichem Greifer, in dem sieben RobStride-Servomotoren stecken, die über einen gemeinsamen CAN-Bus angesprochen werden. Jeder Motor hat eine eigene Steuerelektronik mit Encoder, Getriebe und Regler. Die Software auf dem Rechner berechnet also keine Ströme, sondern schickt Sollwerte, und der Motor regelt selbst. Das ist die Architektur fast aller modernen Hobby- und Forschungsarme.
| Eigenschaft | Wert |
|---|---|
| Freiheitsgrade | 6 Gelenke + Greifer (7 Motoren) |
| Motoren | 3 × RobStride RS06 (J1–J3), 4 × RobStride RS00 (J4–J6, Greifer) |
| Reichweite | ca. 754 mm mit Greifer |
| Nennnutzlast | 2,5 kg |
| Versorgung | 48 V DC (empfohlen: MeanWell 48 V / 12,5 A) |
| Kommunikation | CAN-Bus, 1 Mbit/s, Motor-IDs 1–7 in Reihe verkettet |
| Software von Seeed | motorbridge (Python/CLI), Pinocchio/MeshCat-Beispiele, LeRobot-Integration |
Wichtig ist der Buchstabe am Ende: Den Arm gibt es auch als B601-DM mit Damiao-Motoren. Beispiele aus dem Netz für die DM-Variante funktionieren auf der RS-Variante nicht – das Protokoll ist ein anderes.
3D-Darstellung des reBot Arm B601-RS in einer leicht angewinkelten Pose; rote Linien markieren die Drehachsen der sechs Gelenke J1 bis J6 und den Greifer J7, beschriftet mit Motortyp RS06 oder RS00 und Funktion von Basis-Drehung bis Handgelenk-Drehung
Die sieben Antriebe des B601-RS: Basis (J1), Schulter (J2) und Ellbogen (J3) tragen die schweren RS06, Unterarmdrehung (J4), Handgelenk (J5, J6) und Greifer (J7) die leichten RS00.
Diese Aufteilung hat praktische Folgen, die später immer wieder auftauchen: Die RS06 an Schulter und Ellbogen tragen fast das ganze Armgewicht und sind deshalb für Schwerkraft und Ruckeln entscheidend. Die RS00 am Handgelenk vertragen deutlich kleinere Regelparameter. Und der Greifer ist mechanisch ein eigenes Kapitel.
Die Kommunikationskette: vom Browser bis zum Motor
Zwischen Mausklick und Motorbewegung liegen fünf Ebenen, und an jeder Ebene gibt es genau eine Regel, die man nicht brechen darf. Das Gesamtbild sieht so aus:
┌──────────────┐ HTTP / WebSocket ┌───────────────────────────┐
│ Browser │ ─────────────────► │ Web-Server (FastAPI) │
│ (Bedienung) │ ◄───────────────── │ REST-Kommandos, 10-Hz- │
└──────────────┘ Zustand 10 Hz │ Zustandsstrom │
└────────────┬──────────────┘
│ Kommando-Queue
┌────────────▼──────────────┐
│ Robot-Service (1 Thread) │
│ Trajektorien, Halten, │
│ Watchdog, 200-Hz-Takt │
└────────────┬──────────────┘
│ motorbridge (Python)
┌────────────▼──────────────┐
│ USB-CAN-Adapter │
│ (PCAN, 1 Mbit/s, 120 Ω) │
└────────────┬──────────────┘
│ CAN-Bus, Daisy-Chain
┌───────┬───────┬───────┬────────────┴──┬───────┬───────┐
J1 J2 J3 J4 J5 J6 Greifer
(RS06) (RS06) (RS06) (RS00) (RS00) (RS00) (RS00)Die Regeln dazu:
- Browser: schickt nur Wünsche, nie Sollwerte. Er darf langsam sein, abstürzen oder die Verbindung verlieren, ohne dass sich der Arm anders verhält.
- Web-Server: übersetzt Wünsche in Kommandos und stellt sie in eine Warteschlange. Er greift selbst nie auf den Bus zu.
- Robot-Service: genau ein Thread besitzt den CAN-Kanal. Alles, was Motoren betrifft, läuft hier durch.
- Adapter: Der Kanal ist exklusiv. Zwei Programme gleichzeitig am selben PCAN-Adapter scheitern mit einem Initialisierungsfehler – das ist eher ein Schutz als ein Problem.
- Motoren: regeln selbst, führen aber den letzten Befehl stur weiter aus. Was du zuletzt geschickt hast, gilt.
CAN-Bus in der Praxis
Der Bus läuft mit 1 Mbit/s, am Kettenanfang sitzt der USB-Adapter mit zugeschaltetem 120-Ω-Abschlusswiderstand. Unter Linux erscheint der Adapter als SocketCAN-Schnittstelle:
sudo ip link set can0 type can bitrate 1000000
sudo ip link set can0 upAuf dem Mac gibt es kein SocketCAN, dort braucht ein PCAN-kompatibler Adapter die PCBUSB-Bibliothek, und motorbridge bildet can0 darauf ab. Der erste Test ist immer ein Bus-Scan: Alle sieben IDs müssen antworten. Antwortet keine, ist die Fehlerquelle in meiner Erfahrung häufiger der Adapter als der Arm – bei mir war es eine kalte Lötstelle im USB-CAN-Adapter, während die Verkabelung die ganze Zeit stimmte.
motorbridge-cli scan --vendor robstride --channel can0 --start-id 1 --end-id 7Verbinden, ohne etwas einzuschalten
Der erste Python-Code sollte nur lesen. Wichtig ist, dass Verbinden und Einschalten zwei getrennte Schritte sind:
from motorbridge import Controller
JOINTS = [("J1", 1, "rs-06"), ("J2", 2, "rs-06"), ("J3", 3, "rs-06"),
("J4", 4, "rs-00"), ("J5", 5, "rs-00"), ("J6", 6, "rs-00"),
("Greifer", 7, "rs-00")]
HOST_ID = 0xFD # ID, an die die Motoren antworten
ctrl = Controller("can0")
motors = {name: ctrl.add_robstride_motor(mid, HOST_ID, model)
for name, mid, model in JOINTS}
# noch kein enable(): die Motoren sind registriert, aber kraftlosEine Falle steckt in der bequemen with Controller(...)-Form aus vielen Beispielen: Beim Verlassen des Blocks werden alle Motoren deaktiviert. Für ein Skript, das den Arm danach weiter halten soll, ist das genau falsch. Gib die Handles stattdessen einzeln frei.
Grundregeln, bevor sich irgendetwas bewegt
Die gefährlichste Zeile in jeder Robotersteuerung ist nicht der Bewegungsbefehl, sondern disable_all(). Ein abgeschalteter Motor hält nichts mehr. Steht der Arm ausgestreckt in der Luft, fällt er. Das klingt banal und passiert trotzdem – bei mir genau einmal, aus der Annahme heraus, der Arm liege schon auf. Er lag nicht.
Daraus ergeben sich die Regeln, die ich vor jede Zeile Steuercode stellen würde:
- Abschalten nur in der Ruhepose. Außerhalb wird nicht abgeschaltet, sondern erst kontrolliert abgelegt (fahren, dann aus). Für Notfälle gibt es zwei verschiedene Knöpfe, dazu gleich mehr.
- Radiant im Code, Grad nur an den Rändern. Das SDK rechnet in rad und rad/s. Grad gehören in Anzeigen und in gespeicherte Posen, weil Menschen sie lesen.
- Nullpunkt nur in einem eigenen Kalibrierskript setzen.
set_zero_position()hat in normalem Steuercode nichts zu suchen. Ein falsch gesetzter Nullpunkt verschiebt jede gespeicherte Pose. - Software-Gelenkgrenzen pflegen. Die Datenblattwerte sind ein Anfang, die echten Anschläge misst man besser selbst von Hand nach und zieht ein paar Grad Sicherheitsabstand ab. Bei mir wich ein Handgelenk deutlich vom Datenblatt ab.
- Neue Bewegungen langsam testen, ohne Last, mit der Hand in der Nähe.
- Freeze und Not-Aus unterscheiden. Freeze heißt: sofort an der aktuellen Stelle starr halten. Not-Aus heißt: Strom weg, Arm fällt. Freeze ist fast immer der richtige Knopf.
| Aktion | Was passiert | Wann |
|---|---|---|
| Freeze | Bewegung abbrechen, aktuelle Stellung starr halten | Standard bei jedem Problem |
| Ablegen | langsam in die Ruhepose fahren, dann Motoren aus | Feierabend |
| Motoren aus | sofort kraftlos | nur in der Ruhepose |
| Not-Aus | sofort kraftlos, egal wo | nur wenn jemand den Arm hält oder Gefahr für Menschen besteht |
Die Regelmodi: Wer regelt eigentlich?
Moderne Servomotoren bieten mehrere Betriebsarten, und die Wahl entscheidet darüber, ob du einzelne Ziele schickst oder einen kontinuierlichen Sollwertstrom. Bei den RobStride-Motoren sind drei relevant:
| Modus | Prinzip | Stärke | Schwäche |
|---|---|---|---|
| Profile Position (PP) | Ziel + Geschwindigkeitslimit schicken, Motor plant selbst | einfach, wenig Busverkehr | Gelenke laufen nicht synchron, Ziele werden ohne Quittung übernommen |
| Cyclic Sync Position (CSP) | Positions-Sollwerte im festen Takt, Motor regelt intern | sehr kleiner Endfehler | Moduswechsel braucht Abschalten, eigene Reglerparameter |
| MIT | pro Takt Position, Geschwindigkeit, kp, kd und Vorsteuermoment | volle Kontrolle, Schwerkraft-Vorsteuerung, weiches Halten | Rechner muss den Takt zuverlässig liefern |
Der MIT-Modus (benannt nach dem Mini-Cheetah-Regler des MIT) rechnet im Motor nach einer einfachen Formel:
τ = kp · (q_soll − q_ist) + kd · (v_soll − v_ist) + τ_vorsteuerDas ist ein PD-Regler mit Zusatzmoment. Der Charme: kp und kd kannst du pro Gelenk und sogar pro Takt ändern, und über τ_vorsteuer gibst du das Moment mit, das die Schwerkraft gerade braucht. Der PD-Teil muss dann nur noch kleine Abweichungen korrigieren statt den ganzen Arm zu tragen. Seeed selbst nutzt für den B601-RS einen MIT-Sollwertstrom mit 200 Hz, und dabei bin ich geblieben.
Ein Detail, das mich einen Tag gekostet hat: Bei PP-Befehlen aus motorbridge wurde ein Ziel einmal still nicht ausgeführt – das Gelenk hielt einfach weiter seine alte Position. Wer PP nutzt, sollte das Zielregister nach dem Schreiben zurücklesen und vergleichen. Im MIT-Modus stellt sich das Problem nicht, weil jeder Takt den Sollwert neu sendet und jede Antwort die Ist-Position mitliefert.
Eine einzelne Pose anfahren
Eine Pose ist nichts weiter als sechs Gelenkwinkel – und das Anfahren besteht darin, aus Start und Ziel eine glatte Kurve zu machen und diese im festen Takt an die Motoren zu streamen. Der Greifer gehört bewusst nicht zur Pose. Sonst öffnet „fahre zu Pose X" nebenbei den Greifer, und das Teil, das gerade drin war, fällt herunter.
Gespeichert wird eine Pose lesbar in YAML, in Grad:
bereit: [0, 50, 50, 0, 0, 0]
vorne: [0, 80, 80, 0, 0, 0]
links_oben: [-40, 60, 90, -10, 20, 45]Min-Jerk statt Trapez
Die naheliegende Bahn ist eine Gerade mit Trapezprofil: beschleunigen, konstant fahren, bremsen. Das Problem sind die Knicke im Profil – an jedem springt die Beschleunigung, und ein Arm mit Getriebespiel antwortet darauf mit einem hörbaren Klacken. Besser ist ein Minimum-Jerk-Profil, ein Polynom fünften Grades, bei dem Geschwindigkeit und Beschleunigung am Anfang und Ende null sind:
import math
import numpy as np
def min_jerk(q0, q1, max_vel, max_acc, rate=200.0):
"""Liefert (q, qd) für eine synchrone Min-Jerk-Fahrt aller Gelenke."""
d = np.asarray(q1) - np.asarray(q0)
dmax = float(np.max(np.abs(d)))
if dmax < 1e-4:
return
# Spitzengeschwindigkeit = 1.875·d/T, Spitzenbeschleunigung ≈ 5.77·d/T²
T = max(1.875 * dmax / max_vel, math.sqrt(5.77 * dmax / max_acc), 0.3)
n = max(2, int(T * rate))
for i in range(1, n + 1):
s = i / n
p = 10 * s**3 - 15 * s**4 + 6 * s**5
pd = (30 * s**2 - 60 * s**3 + 30 * s**4) / T
yield q0 + d * p, d * pdZwei Dinge machen diese Funktion praxistauglich. Erstens bestimmt das Gelenk mit dem größten Weg die Dauer, alle anderen werden synchron mitgezogen – alle Gelenke starten und kommen gleichzeitig an. Zweitens folgt die Dauer aus den Limits: Wer max_vel halbiert, bekommt eine doppelt so lange Fahrt, ohne dass sich die Form ändert.
Schwerkraft vorsteuern und Folgefehler prüfen
Die Schleife, die die Kurve abfährt, ist kurz. Wesentlich sind zwei Zusätze: das Schwerkraftmoment für jede Sollstellung und die Prüfung, ob der Arm überhaupt folgt.
import time
TRACK_ABORT = math.radians(8.0)
def move_to(arm, q_target, max_vel=0.5, max_acc=1.0, rate=200.0):
q_start = arm.positions()
dt = 1.0 / rate
for i, (q, qd) in enumerate(min_jerk(q_start, q_target, max_vel, max_acc, rate)):
t0 = time.perf_counter()
arm.send_mit(q, vel=qd, kp=KP, kd=KD, tau=gravity_torque(q))
if i % 40 == 0: # 5× pro Sekunde
err = np.abs(arm.positions() - q)
if np.any(err > TRACK_ABORT):
arm.send_mit(arm.positions(), vel=0, kp=KP, kd=KD,
tau=gravity_torque(arm.positions())) # dort halten, wo er ist
raise RuntimeError("Gelenk folgt nicht")
time.sleep(max(0.0, dt - (time.perf_counter() - t0)))gravity_torque(q) liefert das Moment, das jedes Gelenk in Stellung q gegen die Schwerkraft aufbringen muss. Das berechnet eine Dynamikbibliothek wie Pinocchio aus dem URDF-Modell des Arms, das Seeed mitliefert:
import pinocchio as pin
model = pin.buildModelFromUrdf("rebot_arm.urdf")
data = model.createData()
def gravity_torque(q):
return pin.computeGeneralizedGravity(model, data, q)Die Folgefehlerprüfung ist keine Kür. Fällt ein Motor mitten in der Fahrt aus (bei mir meldete ein Schultermotor mehrfach einen Encoderfehler), schickt die Schleife sonst munter weiter Sollwerte, die anderen Gelenke folgen, und der Arm verdreht sich. Mit acht Grad Toleranz und Halten an der gemessenen Stellung wird aus einem Absturz ein sauberer Abbruch.
Einen Takt von 200 Hz schafft Python übrigens problemlos: Gemessen lag die Schleife bei 5,00 ms im Mittel und maximal 6,4 ms, ohne Aussetzer. Ein Echtzeitbetriebssystem braucht es dafür nicht, solange der Thread sonst nichts tut.
Halten: der unterschätzte Zustand
Den Großteil der Zeit bewegt sich ein Roboterarm nicht – er hält. Und gerade das Halten hat eine Falle, in die fast jeder einmal tappt. Im MIT-Modus führt der Motor seinen letzten Befehl weiter aus. Es ist aber guter Stil, den Haltebefehl regelmäßig zu erneuern (bei mir alle 0,2 s), damit Schwerkraftmoment und Parameter aktuell bleiben.
Die Falle: Mit welcher Position frischst du auf?
# FALSCH: der Arm kriecht nach unten
arm.send_mit(arm.positions(), kp=KP, kd=KD, tau=gravity_torque(arm.positions()))
# RICHTIG: immer das kommandierte Ziel
arm.send_mit(q_cmd, kp=KP, kd=KD, tau=gravity_torque(q_cmd))Ein PD-Regler hält nie exakt, er sackt unter Last um ein paar Zehntelgrad ab – genau so viel, dass kp · Fehler das fehlende Moment liefert. Wer die gemessene Position als neues Ziel schickt, akzeptiert diesen Durchhang, und beim nächsten Auffrischen sackt der Arm wieder ein Stück. Nach einer Minute steht er sichtbar tiefer. Deshalb merkt sich die Steuerung das kommandierte Ziel q_cmd und hält immer genau das.
Dasselbe Prinzip gilt bei Last im Greifer. Ein gegriffenes Teil von 0,5 kg vorne am Arm drückte die Schulter bei kp 150 um rund 30 mm nach unten. Abhilfe ist nicht ein härterer Regler, sondern ein ehrliches Modell: Das Gewicht der Last kommt mit in die Schwerkraft-Vorsteuerung, sobald der Greifer zu ist.
Bewegungsabläufe: Posen zu Sequenzen verketten
Ein Bewegungsablauf ist eine Liste von Schritten, die nacheinander abgearbeitet werden – und diese Liste gehört in eine Datei, nicht in den Code. Damit kann man Abläufe ändern, ohne neu zu deployen, und dieselbe Datei läuft in der Simulation und am echten Arm.
# sequences/ablegen.yaml
- pose: bereit
vel: 0.5
- gripper: open
- path: zum_regal # aufgezeichnete Bewegung, siehe Teaching
speed: 1.0
- gripper: grip # schließen bis Kontakt
- wait: 0.5
- pose: bereit
- home: trueDie Schrittarten, die sich bewährt haben:
| Schritt | Bedeutung |
|---|---|
pose: <name> |
Min-Jerk-Fahrt zur gespeicherten Pose |
path: <name> |
aufgezeichnete oder geplante Bahn abspielen |
gripper: open | closed | <grad> |
Greifer auf Preset oder Winkel |
gripper: grip |
schließen bis Kontakt, ohne Kontakt bricht die Sequenz ab |
wait: <s> |
warten und dabei weiter halten |
home: true |
in die Ruhepose |
Der Interpreter dazu ist eine Schleife mit einem match über die Schrittart. Interessanter sind die Randfälle:
- Wiederholen: Ein Feld für Durchläufe (0 = endlos) und ein Knopf „nach diesem Durchlauf stoppen", der den aktuellen Durchlauf sauber zu Ende fährt. Freeze stoppt dagegen sofort.
- Ab Schritt n starten: praktisch zum Testen, gilt aber nur für den ersten Durchlauf.
- Vor dem Start einschalten: Startet eine Sequenz bei abgeschalteten Motoren, muss der Interpreter zuerst ins Halten wechseln. Bei mir warf genau dieser Fall anfangs eine Exception, weil ein Warte-Schritt eine Halteposition auffrischen wollte, die es noch nicht gab.
- Fehlende Posen vorher prüfen und markieren, nicht erst bei Schritt 7 merken.
Teaching Mode: den Arm von Hand führen
Im Teaching Mode bleibt der Arm eingeschaltet, kompensiert aber nur sein eigenes Gewicht – er schwebt und lässt sich mit einem Finger führen. Das ist die schnellste Art, einem Arm Posen und Bewegungen beizubringen, und technisch der anspruchsvollste Teil der Steuerung.
Das Prinzip ist der MIT-Regler mit sehr kleinem kp (oder null) und dem Schwerkraftmoment als einzigem echten Anteil:
def teach_step(arm, q_hold):
q = arm.positions()
tau = gravity_torque(q) * TAU_SCALE + walls(q)
arm.send_mit(q_hold, vel=0, kp=KP_TEACH, kd=KD_TEACH, tau=tau)Was in der Theorie elegant ist, scheitert in der Praxis an drei Stellen.
Das Modell stimmt nicht genau. Massen und Schwerpunkte im URDF sind Konstruktionswerte. Ist das Modell zu stark, hebt sich der Arm von selbst; ist es zu schwach, sinkt er. Ich habe die Schulter in Messfahrten vermessen und einen Skalierungsfaktor von 0,934 plus einen kleinen Restversatz bestimmt. Mit dem Herstellerfaktor ohne Korrektur hob sich die Schulter in der Ruhepose um rund 20 Grad.
Das Modell gilt nur dort, wo es vermessen wurde. Bei mir wurde die Schulter einmal von Hand über die Senkrechte geführt. Dahinter war das Modell nie geprüft, der Arm kippte nach hinten an den Anschlag und drückte dort aktiv weiter. Die Konsequenz waren virtuelle Wände: Jenseits einer Gelenkgrenze kommt ein Federmoment dazu, das den Arm sanft zurückschiebt.
WALL_KP = np.array([40.0, 120.0, 100.0, 20.0, 20.0, 10.0]) # Nm/rad
def walls(q):
lo, hi = LIMITS[:, 0], LIMITS[:, 1]
return WALL_KP * (np.maximum(lo - q, 0) - np.maximum(q - hi, 0))Das Handgelenk zittert. Mit einheitlicher Dämpfung für alle Gelenke vibrierten die kleinen RS00-Motoren am Handgelenk. Gelenkspezifische Werte (kd 2,5 an Schulter und Ellbogen, 0,3–0,4 am Handgelenk) lösten das.
Für das Gefühl beim Führen gibt es verschiedene Profile. Am angenehmsten war ein „Velocity Lock" aus Seeeds Beispielen: Der Arm folgt, solange man schiebt, und friert die Position ein, sobald der Endeffektor stillsteht. Man kann loslassen, ohne dass etwas nachsackt.
Posen und Bewegungen aufzeichnen
Aus dem Teaching Mode entstehen zwei Arten von Daten:
- Posen: Arm hinführen, Namen eingeben, speichern. Gespeichert werden die sechs aktuellen Winkel.
- Bewegungen: Aufnahme starten, Arm führen, Aufnahme stoppen. Ein Rekorder-Thread liest die Gelenkwinkel mit 50 Hz mit.
def record(arm, stop_event, rate=50.0):
samples = []
while not stop_event.is_set():
samples.append(np.degrees(arm.cached_positions()))
time.sleep(1.0 / rate)
return trim_still(np.array(samples)) # Stillstand an Anfang und Ende abschneidenDer Rekorder liest die zwischengespeicherten Positionen der Teach-Schleife (cached_positions), statt selbst auf den Bus zu gehen. Das hält die Regel ein, dass nur ein Thread den CAN-Kanal besitzt. Als Kriterium für „Stillstand" an den Enden hat sich eine Gelenkgeschwindigkeit unter 0,03 rad/s bewährt.
Aufgezeichnete Bewegungen glätten
Eine von Hand geführte Bewegung ist nie glatt: Sie enthält Zittern, kleine Korrekturen und Zögern – und der Arm spielt all das beim Abspielen getreu nach. Die Lösung ist eine Glättung, die beim Abspielen angewendet wird, nicht beim Speichern. So bleiben die Rohdaten erhalten, und die Stärke lässt sich nachträglich ändern.
Bewährt haben sich zwei Verfahren:
| Stufe | Verfahren | Wirkung |
|---|---|---|
| leicht / mittel / stark | nullphasiger Gauß-Filter, σ = 60 / 150 / 350 ms | entfernt Zittern, erhält die Form; „stark" schneidet Kurven |
| Ideallinie | Ramer-Douglas-Peucker auf wenige Stützpunkte, Catmull-Rom-Spline hindurch | macht aus einer zittrigen Aufnahme eine saubere Linie |
Der Gauß-Filter ist mit numpy allein schnell geschrieben. Entscheidend ist, dass Start- und Endpunkt danach exakt wieder stimmen – sonst fährt der Arm am Ende nicht dorthin, wo er aufgenommen wurde:
def gauss(x, sigma):
r = int(3 * sigma)
k = np.exp(-0.5 * (np.arange(-r, r + 1) / sigma) ** 2); k /= k.sum()
xp = np.pad(x, ((r, r), (0, 0)), mode="edge")
return np.stack([np.convolve(xp[:, j], k, mode="valid") for j in range(x.shape[1])], 1)
def pin_ends(raw, sm):
w = np.linspace(0, 1, len(sm))[:, None]
return sm + (raw[0] - sm[0]) * (1 - w) + (raw[-1] - sm[-1]) * w
smooth = pin_ends(q, gauss(q, sigma=0.15 * 50)) # Stufe „mittel" bei 50 HzDer Effekt ist größer, als man denkt: An einer verrauschten Testbahn sank die Spitzenbeschleunigung mit der mittleren Stufe von 450 auf 1,7 rad/s² – bei weniger als einem Grad mittlerer Abweichung von der Aufnahme.
Diagramm einer aufgezeichneten Bewegung: farbige Linien zeigen den Verlauf der Gelenkwinkel J1 bis J6 über 15,5 Sekunden; dünne Linien sind die Rohaufnahme, kräftige die geglättete Version, die Spitzenbeschleunigung sinkt laut Kopfzeile von 3,5 auf 1,3 rad/s²
Vorschau in der Bedienoberfläche: dünn die Rohaufnahme, kräftig die geglättete Kurve. Die Kopfzeile zeigt, wie stark die Glättung die Spitzenbeschleunigung senkt.
Vor dem Abspielen gehört noch eine Prüfung dazu: Würde ein Gelenk schneller als ein festgelegtes Maximum (bei mir 3 rad/s), wird die Bahn bei der gewählten Geschwindigkeit abgelehnt. Und der Arm fährt zuerst per Min-Jerk zum Startpunkt der Bahn, bevor er sie abspielt – sonst springt er.
Kartesisch denken: Millimeter statt Gelenkwinkel
Gelenkwinkel sind für den Motor praktisch, für den Menschen aber unanschaulich – „10 cm nach vorn und dabei waagerecht bleiben" lässt sich in Gelenkwinkeln nicht ausdrücken. Dafür braucht es zwei Rechenwege:
Vorwärtskinematik (FK): Gelenkwinkel ──► Position + Orientierung des Greifers
Inverse Kinematik (IK): Position + Orientierung ──► Gelenkwinkel (falls erreichbar)Die FK ist eindeutig und schnell, Pinocchio rechnet sie aus dem URDF. Die IK ist das eigentliche Problem: Es gibt oft mehrere Lösungen (Ellbogen oben oder unten), manchmal keine, und ein lokaler Löser bleibt gern hängen, wenn er aus einer ungünstigen Stellung startet. Bei mir scheiterte der mitgelieferte Löser aus der eingeklappten Ruhepose regelmäßig.
Was zuverlässig funktioniert, ist ein mehrstufiges Vorgehen:
- Zuerst von der aktuellen Stellung aus lösen.
- Dann von den geteachten Posen aus – sie liegen meist in sinnvollen Bereichen.
- Dann von den nächsten Einträgen einer vorab berechneten Tabelle aus 30.000 Zufallsstellungen.
- Unter allen gültigen Lösungen die nehmen, die der aktuellen Stellung am nächsten ist. So wirft der Arm nicht plötzlich den Ellbogen um.
Daraus ergeben sich zwei Bewegungsarten:
| Art | Was passiert | Wann |
|---|---|---|
| PTP (Point-to-Point) | IK nur für das Ziel, Fahrt im Gelenkraum per Min-Jerk; der Weg des Greifers ist gekrümmt | freie Fahrten im Raum |
| Linear | Strecke in 2-mm-Stücke zerlegt, IK für jedes Stück, Orientierung per Slerp interpoliert | Einführen, Herausziehen, alles mit enger Toleranz |
Jede geplante Bahn wird vor dem Fahren geprüft: Gelenkgrenzen, und – oft vergessen – die Tischebene. Bei mir müssen alle Gelenkachsen ab dem Ellbogen mindestens 35 mm und das Werkzeug mindestens 15 mm über der Tischplatte bleiben. Das prüft die FK für jeden Zwischenpunkt.
Eine überraschende Erkenntnis aus der Planung: Die Erreichbarkeit hängt stark von der Orientierung ab. Senkrecht von oben greifen konnte der Arm wegen der Handgelenkgeometrie nur in einem schmalen Höhenband von 80 bis 210 mm; waagerecht nach vorn erreichte er dagegen etwa 50 bis 500 mm Höhe. Das sollte man wissen, bevor man die Arbeitsumgebung aufbaut.
Koordinaten-Editor im Browser: links eine Tabelle mit vier linearen Wegpunkten in Millimetern samt Ausrichtung „waagerecht nach vorn" und Tempo, darunter Draufsicht und Seitenansicht der Bahn mit nummerierten Punkten; rechts die 3D-Ansicht des Arms und eine Bedienkarte mit Jog-Knöpfen für X, Y, Z und Drehung
Kartesischer Editor: Wegpunkte in Millimetern im Basis-Koordinatensystem, jeweils als Punkt-zu-Punkt- oder Linearbewegung. Draufsicht und Seitenansicht zeigen die geplante Bahn, rechts lässt sich der Arm in Millimeterschritten joggen.
Aufnahme plus Planung: das Beste aus beiden
Die stärkste Kombination war am Ende diese: von Hand vormachen, dann idealisieren. Die Aufnahme wird per FK in eine Greiferbahn umgerechnet, mit Ramer-Douglas-Peucker auf wenige markante Punkte reduziert (Position und Orientierung, ein Grad zählt wie zwei Millimeter) und durch gerade Linearstrecken ersetzt. Aus einer 26,6 Sekunden langen, zittrigen Handaufnahme wurden so vier saubere Strecken mit maximal 18 mm Abweichung.
Dialog „Aufnahme in Idealbewegung umwandeln": links Einstellungen für Toleranz (20 mm), Ausrichtung des Greifers, Bewegungsart „Linear" und automatisches Tempo; rechts Draufsicht und Seitenansicht, in denen die graue zittrige Aufnahme von einer orangen Linie aus vier nummerierten Punkten nachgezeichnet wird
Grau die von Hand geführte Aufnahme, orange die daraus abgeleitete Idealbahn mit vier Wegpunkten. Das Ergebnis landet als normale, editierbare Wegpunktliste im Editor.
Damit die Idealbahn ungefähr so lange dauert wie die Aufnahme, rechnet man das Tempo zurück: Bei einem Min-Jerk-Profil ist die Spitzengeschwindigkeit das 1,875-Fache der Durchschnittsgeschwindigkeit, also v = 1,875 · L / T.
Der Greifer: ein eigenes Kapitel
Ein Greifer ist kein siebtes Gelenk wie die anderen – er hat einen festen Anschlag, gegen den er drücken soll, und genau das macht ihn gefährlich für sich selbst. Ein Positionsregler, der ein Ziel jenseits des Anschlags bekommt, drückt mit voller Kraft dagegen. Bei mir ist auf diese Weise ein Gehäuseteil am Greifer gebrochen.
Drei Maßnahmen machen den Greifer beherrschbar.
Weiche Regelung statt harter Position. Mit kleinem kp (bei mir 5) wird die Greifkraft ungefähr kp · Fehler. Ein Ziel einige Grad „hinter" dem Objekt erzeugt eine definierte Haltekraft, statt den Motor zu blockieren. Zusätzlich wird der Sollwert nie weiter als ein Maximum vom Istwert entfernt – damit ist das Moment unabhängig vom angeforderten Ziel gedeckelt:
GRIP_KP = 5.0
GRIP_ERR_MAX = math.radians(28.6) # → höchstens ≈ 2,5 Nm
def grip_target_limited(target, measured):
return measured + np.clip(target - measured, -GRIP_ERR_MAX, GRIP_ERR_MAX)Greifen bis Kontakt. Statt eines festen Schließwinkels schließt der Greifer langsam, bis sich die Backen trotz Zielvorlauf nicht mehr bewegen (bei mir: weniger als 1° in 150 ms bei mehr als 8° Vorlauf). Dann wird das Ziel so gesetzt, dass die gewünschte Haltekraft entsteht. Findet der Greifer keinen Kontakt, bricht die Sequenz ab – das ist die billigste Prüfung, ob das Teil überhaupt da ist. Nebenbei lässt sich das Teil in diesem Moment auch wiegen: Die Haltemomente von Schulter und Ellbogen vor und nach dem Greifen unterscheiden sich um genau das Lastmoment.
Der Positionswert nach dem Einschalten kann falsch sein. Das war der Grund für den Bruch. Der Motor kennt nach dem Einschalten nur den Rotorwinkel, nicht wie viele Umdrehungen der Rotor vor dem Getriebe gemacht hat. Beim RS00 mit 10:1-Getriebe ist der gemeldete Abtriebswinkel dann um ein Vielfaches von 36° falsch. Bei den Armgelenken fällt das nicht auf, weil sie beim Einschalten nahe null stehen. Der Greifer steht aber irgendwo zwischen offen und zu.
Die Lösung: Der Greifer ist nach jedem Start gesperrt, bis seine Referenz geklärt ist. Entweder durch eine Referenzfahrt (langsam und mit kleiner Kraft an den Zu-Anschlag, dort 0° setzen) oder durch Vergleich mit der zuletzt gespeicherten Referenz: Weicht der Rohwert um ein ganzes Vielfaches von 36° ab, wird der Versatz korrigiert, sonst bleibt der Greifer gesperrt. Werte außerhalb des physischen Bereichs sperren ebenfalls.
Ruckeln und Feintuning: Haftreibung ist der Gegner
Wenn ein Arm bei langsamer Fahrt ruckelt, ist die Ursache fast immer Haft-Gleit-Reibung (Stick-Slip): Das Gelenk bleibt hängen, bis der Regler genug Moment aufgebaut hat, reißt los und schießt über. Das sieht man nicht im Code, nur in Messdaten. Deshalb ist der wichtigste Ratschlag dieses Abschnitts: Zeichne bei jedem Abspielen Soll, Ist und Moment je Gelenk auf, bei mir mit 50 Hz als JSON-Datei. Ohne diese Telemetrie rät man nur.
Was ich in mehreren Versuchsreihen gelernt habe:
- Höheres kp hilft, aber begrenzt. Doppelte Steifigkeit senkte die Rucke nur um etwa ein Viertel, nicht um die Hälfte.
- Mehr Dämpfung (kd) hilft, verstärkt aber Rauschen. kd wirkt auf die gemessene Geschwindigkeit, und die ist verrauscht. Mit kd 20 an der Schulter erzeugte das Drehmomentspitzen von bis zu ±28 Nm, das Netzteil brach ein, alle Motoren meldeten Unterspannung. Abhilfe war der motorinterne Geschwindigkeitsfilter (RobStride-Parameter
0x7021): Mit 0,02 statt der Werkseinstellung 0,1 sank das Drehmomentrauschen auf etwa ein Drittel – erst damit war kd 20 an Schulter und Ellbogen sicher. - Geschwindigkeits-Vorsteuerung mitschicken. Mit
v_soll = 0bremst der kd-Anteil jede Bewegung. Wer die geplante Geschwindigkeit mitschickt, halbiert den Nachlauf. - Reibungs-Vorsteuerung ist heikel. Ein konstantes Zusatzmoment in Bewegungsrichtung half beim Losbrechen, schob aber danach weiter – das Überschwingen wurde größer. Haftreibung ist eben viel größer als Gleitreibung.
- Schneller fahren ist oft die beste Lösung. Stick-Slip ist bei kleinen Gelenkgeschwindigkeiten am stärksten. Eine Verdopplung der Standardgeschwindigkeit ließ ein periodisches Rucken an der Basis von 25 auf 8 Prozent schrumpfen.
- Der interne Positionsmodus (CSP) ist kein Wundermittel. Er beseitigt den Endfehler fast vollständig, aber nicht das Ruckeln unter Last.
Ein Ansatz, der in der Theorie perfekt passt, ist Iterative Learning Control (ILC): Weil sich das Ruckeln bei gleicher Bahn und gleichen Parametern fast identisch wiederholt (Korrelation zwischen Läufen 0,96 bis 0,99), zieht man den Fehler eines Laufs von der Sollbahn des nächsten ab. Am echten Arm sank der Gelenkfehler damit von 0,43° auf 0,045° rms. Das Wippen vorne am Greifer wurde allerdings nicht kleiner, sondern verschob sich – es war selbst erregt, keine feste Störung. ILC lohnt sich also für Genauigkeit, nicht gegen Schwingungen.
Die ehrliche Bilanz: Am Ende blieb ein Wippen von wenigen Millimetern vorn am Greifer. Für viele Aufgaben ist das egal. Wer Präzision im Zehntelmillimeterbereich während der Fahrt braucht, kommt mit Motoren am CAN-Bus und einem PD-Regler auf dem Rechner an Grenzen.
Überwachung: Watchdog und Fail-safe
Jede Antwort eines RobStride-Motors trägt in ihrer CAN-ID den Betriebsmodus und sechs Fehlerbits – wer sie nicht auswertet, merkt einen Motorausfall erst, wenn der Arm sich seltsam verhält. Im MIT-Modus kommt nach jedem Befehl eine solche Antwort zurück, die Überwachung kostet also keinen zusätzlichen Busverkehr:
FAULTS = {16: "Unterspannung", 17: "Überstrom", 18: "Übertemperatur",
19: "Magnetencoder", 20: "Hall-Encoder", 21: "unkalibriert"}
def decode_status(arbitration_id):
mode = (arbitration_id >> 22) & 0x3 # 0 Reset, 1 Kalibrierung, 2 läuft
errors = [txt for bit, txt in FAULTS.items() if arbitration_id & (1 << bit)]
return mode, errorsDaraus baut man einen Watchdog, der in der Regelschleife mitläuft. Die Regeln, die sich bewährt haben:
- Vor jedem Einschalten Status lesen. Meldet ein Motor einen Fehler: nicht einschalten.
- Bei unerklärlichem Verhalten zuerst die Statusbits lesen, dann Modell und Parameter anfassen. Ich habe einmal stundenlang an einer Schwerkraftkorrektur gefeilt, die gar nicht das Problem war – der Schultermotor hatte einen Encoderfehler und war schlicht aus.
- Fällt ein Motor während einer Bewegung aus, endet die Bewegung. Der Watchdog darf den Motor wieder einschalten, aber nicht, während die Bahn weiter Sollwerte schickt. Genau diese Kombination führte bei mir zu Sprüngen von über 70 Grad in einer Sekunde.
- Momentenwächter: Liegt das Moment eines Hauptgelenks über einer Schwelle (bei mir 25 Nm), wird die Fahrt abgebrochen und an der Ist-Stellung gehalten. Das fängt Kollisionen und Rauschspitzen.
- Jede Exception führt zum starren Halten, nie zum Abschalten. Ein Programmfehler darf den Arm nicht fallen lassen.
- Ausfälle protokollieren mit Zeit, Pose und Temperatur. Wenn der Hersteller-Support fragt, ob ein Fehler pose- oder lastabhängig ist, hast du Daten statt Vermutungen.
Wie man so etwas allgemein in Messwerte, Logs und Alarme übersetzt, beschreibe ich in Monitoring und Observability: womit man anfängt.
Die Web-Oberfläche: bedienbar machen, ohne die Kontrolle abzugeben
Eine Web-Oberfläche für einen Roboterarm ist eine ganz normale Web-Anwendung mit einer einzigen Besonderheit: Sie darf nie im Echtzeitpfad liegen. Der Browser bekommt den Zustand, schickt Wünsche und zeigt an. Was der Arm in den nächsten fünf Millisekunden tut, entscheidet ausschließlich der Robot-Service.
Bedienoberfläche im Browser: oben eine Kopfzeile mit Statusanzeige und den Knöpfen Teach, Halten, Ablegen, Motoren aus, FREEZE und NOT-AUS; links Reiter für Posen, Bewegungen und Sequenzen; in der Mitte eine 3D-Ansicht des Arms auf einem Gitterboden, darunter ein Sequenz-Editor mit Schritt-Knöpfen; rechts Gelenkwinkel, Jog-Knöpfe und eine Greifer-Steuerung
Die Bedienoberfläche im Simulationsmodus: Sicherheitsknöpfe immer sichtbar oben rechts, links die Bibliothek mit Posen, Bewegungen und Sequenzen, in der Mitte 3D-Ansicht und Sequenz-Editor, rechts Gelenke, Jog und Greifer.
Ein Thread besitzt den Arm
Der Kern ist ein RobotService, der als eigener Thread läuft und Kommandos aus einer Warteschlange abarbeitet. Die HTTP-Endpunkte stellen nur ein:
import queue, threading
from fastapi import FastAPI, WebSocket
class RobotService(threading.Thread):
def __init__(self, backend):
super().__init__(daemon=True)
self.b = backend
self.cmds = queue.Queue()
self.mode = "off" # off | teach | hold | moving | playing
self.abort = False
def submit(self, cmd, **kw):
self.cmds.put((cmd, kw))
def run(self):
while True:
try:
try:
name, kw = self.cmds.get(timeout=0.1)
getattr(self, "cmd_" + name)(**kw)
except queue.Empty:
pass
self.refresh_hold_and_watchdog()
except Exception as e:
self.fail_safe_hold(str(e)) # nie abschalten
app = FastAPI()
svc = RobotService(backend)
svc.start()
@app.post("/api/pose/goto")
async def goto(body: dict):
svc.submit("goto", name=body["name"], vel=body.get("vel", 0.5))
return {"ok": True}
@app.post("/api/freeze")
async def freeze():
svc.abort = True # sofort sichtbar für laufende Bewegungen
svc.submit("freeze")
return {"ok": True}Ein Detail steckt im Freeze-Endpunkt: Das Flag abort wird direkt gesetzt, nicht erst über die Warteschlange. Eine laufende Bewegung blockiert den Service-Thread; sie prüft das Flag in jedem Takt und bricht innerhalb von fünf Millisekunden ab. Ein Freeze, der hinter der laufenden Bewegung in der Queue wartet, wäre nutzlos.
Zustand per WebSocket
Den Zustand (Modus, Gelenkwinkel, Greifer, Ströme, Meldungen, Fortschritt einer Sequenz) schickt der Server zehnmal pro Sekunde an jeden verbundenen Browser:
import asyncio
@app.websocket("/ws")
async def ws(sock: WebSocket):
await sock.accept()
while True:
await sock.send_json(svc.snapshot())
await asyncio.sleep(0.1)10 Hz reichen für eine flüssige Anzeige und belasten nichts. Wichtig ist, dass snapshot() nur zwischengespeicherte Werte liest und nie auf den Bus geht.
Was die Oberfläche können sollte
Aus der täglichen Arbeit mit dem Arm hat sich diese Liste ergeben:
- Sicherheitsknöpfe immer sichtbar. Teach, Halten, Ablegen, Motoren aus, Freeze, Not-Aus in einer festen Kopfzeile, nie in einem Menü oder unterhalb der Scrollkante. Not-Aus mit Rückfrage, weil der Arm danach fällt; Freeze ohne.
- Bibliothek: Posen, Bewegungen, Sequenzen in Reitern, jeweils mit Anfahren, Umbenennen (passt alle Sequenzen mit an), Löschen.
- Sequenz-Editor: Schritte per Knopf hinzufügen, per Ziehen umsortieren, Felder direkt editieren, „ab hier abspielen", Fortschrittsbalken, Warnung bei ungespeicherten Änderungen.
- 3D-Ansicht: Seeed liefert einen MeshCat-Visualizer mit. Ich bette nicht dessen eigene Seite ein, sondern eine kleine eigene Seite auf demselben Ursprung, die das MeshCat-Bundle lädt und den WebSocket über den eigenen Server bezieht. MeshCats Server lehnt Cross-Origin-WebSockets ab.
- Jog: Knöpfe für ±X, ±Y, ±Z und Drehung in einstellbaren Millimeterschritten, wahlweise im Welt- oder Werkzeugkoordinatensystem.
- Simulation: ein zweiter Service mit einem simulierten Backend, der denselben Code durchläuft. „Simulieren" spielt eine Sequenz am virtuellen Arm ab, beginnend an der Ist-Stellung des echten Arms, während dieser unberührt bleibt. Ein farbiger Rahmen um die 3D-Ansicht macht unmissverständlich klar, welcher Arm gerade zu sehen ist.
Die Simulation ist der wichtigste Baustein für die Entwicklung. Weil alles oberhalb des Backends ohne Hardware läuft, lassen sich Oberfläche, Sequenzlogik, Glättung und Bahnplanung am Laptop testen. Am Arm prüft man dann nur noch die Physik.
Zwei Stolpersteine aus dem Frontend
- Safari blockiert
ws://127.0.0.1aus einer HTTPS-Seite still. Die Weboberfläche von Seeeds MotorBridge Studio hängt dann ewig in „connecting". Mit Chrome funktioniert es. - Das
hidden-Attribut verliert gegen eigenedisplay-Regeln. Ein Dialog mit.modal { display: flex }blieb trotzhiddenals unsichtbare Schicht über der Seite liegen und schluckte alle Klicks. Für Nutzer sah das aus wie „die Software hängt". Eine globale Regel[hidden] { display: none !important; }behebt das.
Häufige Fragen
Welche Programmiersprache eignet sich zur Steuerung eines Roboterarms? Python reicht für die meisten Hobby- und Laboranwendungen völlig aus. Ein 200-Hz-Regeltakt lief bei mir stabil mit 5 ms im Mittel. Die eigentliche Regelung passiert ohnehin im Motor. C++ oder ROS 2 lohnen sich, wenn mehrere Arme, Kameras und Sensoren koordiniert werden müssen.
Brauche ich ROS, um einen Roboterarm zu steuern? Nein. Für einen einzelnen Arm mit Posen, Sequenzen und Web-Oberfläche ist ROS eher Ballast. Seeed bietet für den B601-RS auch ROS 2 und MoveIt 2 an, sinnvoll vor allem bei Kollisionsplanung in komplexen Umgebungen. Für Datenaufnahme und Imitation Learning gibt es zusätzlich eine LeRobot-Integration.
Was ist der Unterschied zwischen Gelenkraum und kartesischem Raum? Im Gelenkraum beschreibst du eine Stellung durch die Winkel aller Gelenke, im kartesischen Raum durch Position und Ausrichtung des Greifers in Millimetern. Für den Motor zählt der Gelenkraum, für die Aufgabe meist der kartesische. Die inverse Kinematik übersetzt zwischen beiden.
Warum fällt mein Roboterarm beim Abschalten herunter? Weil die Motoren das Gewicht des Arms aktiv halten. Ohne Strom gibt es bei den meisten Servoarmen keine Bremse. Fahre vor dem Abschalten immer in eine Ruhepose, in der der Arm aufliegt, oder halte ihn fest.
Wie bringe ich einem Roboterarm eine Bewegung bei, ohne zu programmieren? Über einen Teaching Mode: Der Arm kompensiert nur sein Eigengewicht, du führst ihn von Hand, und die Software zeichnet die Gelenkwinkel auf. Anschließend wird die Aufnahme geglättet oder auf wenige Wegpunkte reduziert und kann beliebig oft abgespielt werden.
Warum ruckelt mein Roboterarm bei langsamen Bewegungen? Meist ist es Haft-Gleit-Reibung in Getriebe und Lagern. Höhere Reglersteifigkeit, mehr Dämpfung mit gefilterter Geschwindigkeit, Geschwindigkeits-Vorsteuerung und etwas höheres Tempo helfen. Zeichne Soll- und Istwerte auf, bevor du an Parametern drehst.
Kann ich einen Roboterarm sicher über den Browser steuern? Ja, wenn der Browser nur Kommandos schickt und nie im Regelkreis hängt. Ein Server-Thread besitzt die Hardware, prüft jede Anfrage und hält den Arm bei jedem Fehler starr. Fällt der Browser oder das Netzwerk aus, ändert sich am Verhalten des Arms nichts.
Zusammenfassung und praktische Tipps
- Ein Thread besitzt den Arm. Web-Server, Rekorder und Anzeige lesen Zwischenstände, nur der Robot-Service spricht mit dem Bus.
- MIT-Modus mit 200 Hz gibt die meiste Kontrolle: Min-Jerk-Sollwerte, Schwerkraft-Vorsteuerung, kp und kd pro Gelenk.
- Halten mit dem kommandierten Ziel, nie mit der gemessenen Position – sonst kriecht der Arm nach unten.
- Posen ohne Greifer speichern. Der Greifer ist eine eigene Aktion mit Presets.
- Sequenzen als Daten in YAML, damit sie ohne Codeänderung angepasst und in der Simulation getestet werden können.
- Teaching braucht ein vermessenes Schwerkraftmodell und virtuelle Wände dort, wo das Modell nicht gilt.
- Aufnahmen nicht-destruktiv glätten, Start und Ende exakt erhalten, und vor dem Abspielen Geschwindigkeiten prüfen.
- Den Greifer kraftbegrenzen und nach jedem Einschalten referenzieren, bevor er fahren darf.
- Telemetrie bei jedem Lauf aufzeichnen: Soll, Ist und Moment je Gelenk. Ohne Daten ist jedes Tuning geraten.
- Statusbits zuerst lesen, wenn sich ein Gelenk seltsam verhält, und bei einem Motorausfall die Bewegung beenden.
Mein Rat: Bau die Sicherheitslogik vor den Funktionen. Freeze, Ablegen, Folgefehlerprüfung und ein Fail-safe, der bei jeder Exception hält statt abzuschalten, kosten zusammen vielleicht einen Tag. Jede einzelne dieser Funktionen hat bei mir mindestens einmal einen Absturz des Arms verhindert oder hätte ihn verhindert, wenn sie schon da gewesen wäre. Danach ist die Reihenfolge einfach: erst eine Pose, dann Halten, dann Sequenzen, dann Teaching – und die Web-Oberfläche erst, wenn das alles auf der Kommandozeile stabil läuft. Eine schöne Oberfläche über einer wackligen Steuerung lädt nur dazu ein, öfter auf Knöpfe zu drücken, deren Folgen man nicht kennt.
Ressourcen
- reBot Arm B601-RS – Getting Started (Seeed Studio Wiki) – Aufbau, Verkabelung, CAN-Setup auf Linux und macOS
- MIT Position Control auf dem reBot Arm B601-RS (Seeed Studio Wiki) – Seeeds eigener Ansatz mit MIT-Sollwertstrom
- Pinocchio und MeshCat für den reBot Arm B601-RS (Seeed Studio Wiki) – Kinematik, Schwerkraftkompensation, 3D-Visualisierung
- LeRobot mit dem reBot Arm B601-RS (Seeed Studio Wiki) – Teleoperation, Datenaufnahme, Imitation Learning
- reBot-DevArm (GitHub, Seeed-Projects) – Hardware, URDF-Modell, ROS 2 / MoveIt 2
- motorbridge-Dokumentation (Seeed Studio) – Python-SDK und CLI für die Motoren
- Welche Motoren stecken im reBot Arm B601-RS? (OpenELAB) – Einordnung der RobStride RS00 und RS06
- Pinocchio (GitHub, stack-of-tasks) – Dynamik- und Kinematikbibliothek für FK, IK und Schwerkraftmomente
- FastAPI – WebSockets – Grundlage für den Zustandsstrom an den Browser
- API-Design: Schnittstellen entwerfen, die man nicht bereut – wie man die REST-Schnittstelle der Steuerung schneidet
- Clean Architecture – warum Treiber, Ablauflogik und Oberfläche getrennte Schichten sein sollten
- Monitoring und Observability: womit man anfängt – Telemetrie und Alarme systematisch aufbauen