Python programmieren: vollständige API-Referenz mit allen
30 Methoden und 4 Properties, Einheiten und Backend-Grenzen.
Beispiel-Abdeckung: was direkt geht und was das SDK benötigt.
Neu: spot.look() für benannte Umgebungsrichtungen, state.x/y/heading/speed,
spot.lights(), spot.beep(), spot.pose() und spot.supports().
Den Boston Dynamics Spot programmieren — für den Unterricht an der Kantonsschule.
spotlab nimmt die Betriebsmechanik des Spot-SDK ab (Anmeldung, Zeitsynchronisierung,
Not-Aus, Lease, Leistungsverwaltung), damit ein Schüler mit fünf Zeilen Python anfangen
kann. Es verstellt dabei nicht den Weg zum echten SDK: spot.robot und spot.send()
führen jederzeit zum vollen bosdyn-Client.
Jeder Lauf wird vollständig aufgezeichnet — Kommandos, Roboterzustand mit 10 Hz, Bilder.
Diese Aufzeichnungen sind zugleich die Datengrundlage, um später die MuJoCo-Simulation
aus matura-spot gegen das echte Verhalten zu kalibrieren.
Schüler: Das Schüler-Release-ZIP herunterladen, entpacken und einrichten.cmd
doppelklicken. Danach Spotlab über die Desktop-Verknüpfung öffnen und in der GUI
programmieren. Das Paket enthält GUI und Simulation samt Robotermodell; das
Matura-Repository und Entwicklerwerkzeuge werden nicht benötigt.
Schulanleitung · Releases bauen
Python 3.11–3.14 und einmalig Internetzugang werden vorausgesetzt. Die Pakete
werden in einer eigenen .venv installiert; der Installer prüft Abhängigkeiten
und den Modellaufbau. GitHubs „Source code“-Archiv ist kein Schüler-Release.
Entwickler: Im geklonten Repository:
.\einrichten.cmd -Entwickler
# Optional mit den lokalen Sim-Quellen:
.\einrichten.cmd -Entwickler -MitSim -SimPfad ..\matura-spotDieser ausdrückliche Modus installiert weiterhin dev, gui und mcp editierbar.
Die Schülerausgabe installiert nur die GUI-/Sim-Abhängigkeiten. Die CLI bleibt
für Fortgeschrittene verfügbar; für die Schüler ist kein Terminal im Alltag nötig.
spotlab login # IP, Benutzer, Passwort (Passwort in den Windows-Tresor)
spotlab doctor # Netz, Anmeldung, Zeitsync, Not-Aus, Lease, Akku,
# Lizenz, Nutzlasten, Dienste, Zertifikatsablauf
spotlab new mein-projekt
spotlab open mein-projekt
spotlab run hallo_spot.py
Ohne Roboter üben — baut und prüft alle Kommandos, bewegt nichts:
spotlab run hallo_spot.py --dryrun
pip install -e .[gui]
spotlab gui
Elf Ansichten in einer Seitenleiste: Projekte (anlegen, in VS Code öffnen, starten — mit Häkchen für Trockenlauf), Code (der eingebaute Editor), Live-Lauf (Ereignisse, Telemetrie, Kamerabild, Ausgabe), Läufe (vergangene Läufe mit der Kurve kommandiertes gegen gemessenes Tempo), Karten (GraphNav aufzeichnen und ansehen), Umwelt (was Spot gerade sieht), Gehzeit (den Schulversuch starten und nachtragen), Fahren (den echten Spot über die Tastatur fahren), Raumeditor (Räume bauen und ohne Roboter fahren), Anbindungen (fremde Projekte) und Spot (Zugangsdaten und Prüfung). Hell und dunkel folgen der Windows-Einstellung.
Die Ansicht Code ist eine Ergänzung, kein Ersatz — spotlab open und „In VS Code
öffnen" bleiben. Sie kann: mehrere Reiter, Syntaxhervorhebung, Vervollständigung im
ganzen Code, Syntaxfehler nach einer kurzen Ruhepause,
Suchen und Ersetzen, Starten und Stoppen ohne die Ansicht zu wechseln, und anklickbare
Dateinamen in Tracebacks — ein Klick springt an die Zeile.
Die Vervollständigung gilt überall, nicht nur nach spot.: nach jedem Punkt
(math., eine Liste, eine eigene Variable) und sobald ein angefangenes Wort zwei Zeichen
hat. Nach spot. und spotlab. steht zuerst die eigene Liste mit deutscher Erklärung und
Signatur, darunter ergänzt jedi den Rest. In Kommentaren und Zeichenketten kommt nichts —
dort schlägt jedi sonst Verzeichnisse deines Laptops vor. Strg+Leertaste fragt trotzdem,
an jeder Stelle.
Zwei Dinge, die im Alltag zählen:
- Vor dem Start wird alles gespeichert, nicht nur der sichtbare Reiter. Ein Projekt aus mehreren Dateien läuft sonst mit der alten Fassung seiner Importe.
- Beide Editoren dürfen dieselbe Datei offen haben. Jeder Reiter merkt sich Änderungszeit und Grösse und fragt, bevor er fremde Änderungen überschreibt.
Nicht eingebaut: Debugger mit Haltepunkten, git-Integration, projektweite Suche, Erweiterungen. Dafür ist VS Code da.
Die Oberfläche zeigt jeden Lauf im Arbeitsordner — auch die, die du in VS Code mit F5 startest.
Zwei Arten anzuhalten, und der Unterschied ist wichtig:
| Knopf | Wirkung |
|---|---|
| Stopp | Freundlich: das Programm bricht ab, der Abbau läuft, der Spot setzt sich hin. Reagiert es nach 3 s nicht, wird hartes Beenden angeboten. |
| NOT-AUS | Hart, ein Klick, keine Rückfrage: der Prozess wird getötet, die Motoren gehen aus und der Spot sackt zusammen. |
Der NOT-AUS wirkt nur auf das laufende spotlab-Programm. Das primäre Sicherheitsmittel bleibt der physische Not-Aus am Tablet.
Die GUI hält nie ein Lease — sie liest alles aus dem Lauf-Verzeichnis mit. Deshalb gibt es kein Live-Kamerabild ohne laufendes Skript: Bilder erscheinen, sobald ein Programm welche aufnimmt.
Der Spot kann einen Raum als GraphNav-Karte aufzeichnen und darauf danach autonom fahren.
Aufzeichnen — in der Ansicht „Karten" oder im Terminal:
spotlab record-map turnhalle
Zwei Voraussetzungen, die nicht verhandelbar sind: der Spot muss beim Start ein Fiducial sehen (die AprilTag-Markierung), und gefahren wird mit dem Tablet. spotlab zeichnet nur mit und übernimmt die Steuerung nicht — es braucht dafür kein Lease.
Fahre Runden. Beim Speichern sucht spotlab die Schleifen in der Aufnahme und schliesst sie, dann optimiert es die Anker; die Statuszeile sagt, was dabei herauskam. Ohne diesen Schritt ist eine Aufnahme eine Kette: der Dienst weiss nicht, dass der Gang, durch den Spot zum zweiten Mal fährt, derselbe ist, und legt einen zweiten Strang daneben. Spot fährt dann wie auf Schienen die aufgezeichnete Strecke ab, statt den kurzen Weg zu nehmen, und die Draufsicht schliesst sich sichtbar nicht. Beides passiert auf dem Roboter, ohne Lease, und scheitert es, wird die Karte trotzdem gespeichert — mit dem Grund in der Meldung.
Ansehen und auswählen — spotlab maps listet auf, die Ansicht „Karten" zeigt eine
Draufsicht. Als aktive Karte setzen merkt die Wahl, danach reicht im Skript load_map()
ohne Argument.
Darauf fahren:
with spotlab.connect() as spot:
spot.power_on()
spot.stand()
karte = spot.load_map() # die in der GUI gewählte Karte
print(karte.waypoints) # ['start', 'kueche', 'fenster']
spot.localize() # über das Fiducial verorten
spot.navigate_to("kueche")
navigate_tobewegt den Roboter autonom. Er fährt selbstständig eine Route ab. Sorge für freie Fläche und Aufsicht. Der Geschwindigkeitsdeckel ausconfig.tomlgilt auch hier — er wird alsvelocity_limitan GraphNav durchgereicht.
Ohne Skript, aus dem Tab. In der Ansicht „Karten" eine Karte wählen, „🧭 Zu Wegpunkten
fahren" drücken und in der Zeichnung einen Wegpunkt anklicken — Spot fährt hin. Die
Zeichnung zeigt das Ziel als Ring, den Wegpunkt, an dem Spot sich verortet hat, gefüllt, und
Spot selbst als Pfeil; die Zeile darunter sagt, was der Lauf tut. Ein Klick während der Fahrt
wechselt das Ziel, derselbe Wegpunkt noch einmal geklickt heisst noch einmal fahren, „■ Stopp"
hält an und beendet. Wegpunkte lassen sich hier auch nachträglich benennen: anklicken und
„✎ Wegpunkt benennen…", oder Doppelklick — der Name landet im Graphen der Karte, wo ihn auch
spot.navigate_to("kueche") liest; Namen müssen eindeutig sein, leer entfernt den Namen.
Dahinter steckt kein zweiter Weg zum Roboter: der Knopf startet das
Programm navigieren.py aus dem Projekt Beispiele am echten Spot, mit Lease, Not-Aus-Endpunkt,
Aufzeichnung und dem Tempodeckel wie jeder Lauf; der Tab schreibt nur das Ziel in eine Datei
des Lauf-Verzeichnisses und liest den Stand aus einer anderen. Zum Verorten muss ein AprilTag
der Karte im Kamerabild sein — der Lauf versucht es alle zwei Sekunden neu und sagt, woran es
hängt. Ein Ziel, das Spot nicht erreicht, beendet den Lauf nicht: der nächste Klick geht wieder.
Eine alte Karte nachträglich verbessern. Karten, die vor dieser Fassung aufgezeichnet
wurden, sind Ketten. „✨ Karte verbessern…" im Tab „Karten" holt es nach: die Karte geht auf
den Spot, dort werden Schleifen geschlossen und die Anker optimiert, und die verbesserte
Karte kommt zurück. Spot bewegt sich dabei nicht, braucht aber das Lease — Hochladen
gehört ihm. Deshalb ist es ein Programm (Beispiele/karte_verbessern.py) und keine Arbeit
der Oberfläche; der Fortschritt steht in der Live-Ansicht. Geschrieben wird erst am Schluss
und nur, wenn wirklich etwas gerechnet wurde: die gespeicherte Karte ist die einzige Kopie
deiner Aufnahme.
Karten liegen im Format des SDK unter <arbeitsordner>/karten/<name>/. Eine mit spotlab
aufgezeichnete Karte lässt sich deshalb unverändert an graph_nav_command_line.py und
view_map.py aus dem Spot-SDK verfüttern — und umgekehrt.
Der Sim fährt nach den am 12.08.2026 gemessenen Gangarten. Ein Meter dauert dort so lange wie am echten Spot — ein Programm, das im Übungsraum ankommt, kommt auch am Gerät an.
In der Ansicht Raumeditor wählst du ein Zimmer — eine Vorlage oder einen eigenen Raum —, setzt den Start und baust um, was du brauchst:
| Vorlage | Inhalt |
|---|---|
leer |
vier Wände, ein Tag — für die ersten Schritte |
moebliert |
Tisch, zwei Stuhlstapel, zwei Tags — Ausweichen üben |
durchgang |
zwei Zimmer, eine Tür, der Tag liegt drüben |
treppe |
ein Podest, eine Treppe mit sieben Stufen hinauf, der Tag hängt oben |
Bauen. Links die Werkzeuge: Auswählen (klicken, ziehen, Rahmen aufziehen,
Shift ergänzt), Wand (Klick-Klick zeichnet eine Wandkette, Esc beendet;
Wandenden fangen sich), Block (Rechteck aufziehen), Boden (Rechteck
aufziehen: ein Podest; mit anstieg wird es eine Rampe, mit anstieg und
stufen eine Treppe — der Anstieg läuft entlang der langen Seite, ein Pfeil
zeigt bergauf), Tag (Klick), Start (Klick setzt, Ziehen dreht den Blick).
Am gewählten Element erscheinen Griffe: Wandenden, Block- und Bodenecken, ein
Drehring. Rechts stehen die Elementliste, die Zahlenfelder (Mitte, Grösse,
Drehung, Höhe, z …) und Hinweise. Blöcke lassen sich drehen und haben eine
Höhe; in der Datei heissen sie [[block]], Böden [[boden]], die alte
Schreibweise hindernisse wird weiter gelesen. Eigene Räume liegen unter
<arbeitsordner>/raeume/<name>.toml; die Vorlagen bleiben unverändert, ein
geänderter Raum fragt beim Speichern nach einem Namen.
Ebenen. Jedes Element hat ein z, seine Unterkante über dem Grundboden.
Über der Zeichnung wählst du die Ebene (die Bodenhöhen des Raums, „alle
Ebenen" zeigt alles): was auf einer anderen Ebene liegt, erscheint blass, neue
Elemente landen auf der gewählten Ebene, und G gefolgt von Z hebt die
Auswahl. Absturzkanten (mehr als eine Stufe, 0.25 m) zeichnet der Editor als
Kammlinie; Spot bleibt dort stehen wie an einer Wand. Der Grundboden liegt
überall bei 0, Böden liegen darauf. Auf einer Treppe zeigt Spots Nase immer
bergauf: vorwärts hoch, rückwärts runter — nimmt ein Programm sie falsch
herum, bleibt Spot an der Kante stehen und der Lauf sagt es
(treppe_verweigert). In config.toml steht unter [limits] der Treppenmodus
treppen = "auto"; mit "aus" sind Rampen und Treppen für Spot tabu, im Sim
wie am Roboter.
2D | 3D. Der Knopf rechts über der Zeichnung (oder Tab) schaltet auf eine
3D-Sicht: linke Maustaste wählt, rechte dreht die Kamera, mittlere (oder
Shift+rechts) schwenkt, das Rad zoomt, Home rahmt den Raum; die Tasten sind
dieselben wie in 2D. Ohne OpenGL 3.3 bleibt der Knopf grau und sagt, warum
(QT_OPENGL=software hilft oft); der Editor arbeitet dann in 2D weiter.
Aus einer Karte. „Rekonstruieren…" macht aus einer GraphNav-Karte
(aus „Karten" oder ein beliebiger Ordner mit graph und waypoint_snapshots/)
einen Raum: Wände aus der Punktwolke (Höhenband über dem Boden, 5-cm-Gitter,
Sichtprüfung gegen Tiefen-Artefakte, Linien, Türen als Lücken, begradigt auf
rechte Winkel), AprilTags aus den Ankern mit Blickrichtung und Höhe, Start am
ersten Wegpunkt. Die Punktwolke bleibt als graues Pauspapier unter
der Zeichnung — dort ziehst du nach, was die Automatik falsch hat, und speicherst
den Raum wie jeden anderen (das Pauspapier liegt daneben als .pauspapier).
Ohne Punktwolken wird der Pfad ein Schlauch. Der Katakomben-Gang der Schule
(107 Wegpunkte, 31 Tags) braucht dafür rund sechs Sekunden.
Korrigieren. Eine rekonstruierte Karte hat Wände mit Lücken und einen Boden
nur dort, wo Spot gelaufen ist. „Korrigieren…" listet jede Lücke (Lücke, Ecke,
Anschluss, und Wände quer über den Weg) mit einem Vorschlag und dem Grund: lief
der Roboter hindurch, ist es ein Durchgang; liegen Pauspapier-Punkte in der
Lücke, ist es Wand; sonst „unklar" — du entscheidest. Die gewählte Zeile
leuchtet in der 2D-Sicht auf. „Anwenden" schliesst die Wände und baut das
Gelände: ein Höhenraster, das jeden Gang bis an die Wände füllt, mit dem
Gefälle des gelaufenen Wegs und weichen Übergängen an Abzweigungen (Rampen und
Podeste der Rekonstruktion gehen darin auf, Treppen bleiben und stehen darauf).
Wo weder Wand noch Punkte aufhalten, endet der Boden 2 m neben dem Weg, und die
offenen Ränder leuchten orange, bis du die Wand nachziehst und den Korrigierer
nochmals laufen lässt. Das Gelände steht als eine Zeile in der Elementliste, ist
nur gerechnet (nicht von Hand zu ziehen), erscheint in 2D als Relief mit
Höhenlinien, in 3D als Fläche und in MuJoCo als Boden; spot.state.z folgt
ihm. Alles zusammen ist ein Schritt für Ctrl+Z. Gespeichert wird es als
.gelaende neben der Raumdatei; das Pauspapier trägt seit Fassung PAUS2 auch
den gelaufenen Weg, damit der Korrigierer jederzeit neu rechnen kann.
Tasten wie in Blender, in beiden Sichten:
| Taste | Wirkung |
|---|---|
G / R / S |
Bewegen / Drehen / Skalieren der Auswahl |
X / Y / Z |
Achse sperren (Z: beim Bewegen heben, beim Skalieren die Höhe) |
Ziffern, ., - |
Zahl eingeben; Enter bestätigt, Esc bricht ab |
Shift+D / Entf |
duplizieren / löschen |
Ctrl+Z / Ctrl+Y |
zurück / vor |
A / Alt+A |
alles / nichts auswählen |
Home |
alles zeigen — das Rad zoomt, die mittlere Taste schwenkt |
Ctrl+S |
speichern |
with spotlab.connect(backend="sim") as spot: # oder raum="durchgang"
spot.power_on()
spot.stand()
spot.move(forward=1.5)
for tag in spot.tags():
print(f"Tag {tag.id}: {tag.distance:.1f} m")An Wänden bleibt Spot stehen — kein Fehler, kein Abbruch, so wie am echten Gerät. Die Stelle wird in der Zeichnung markiert und im Protokoll vermerkt.
Über dem Editor steht „Wo läuft es?" mit bis zu vier Möglichkeiten:
| Wahl | Was passiert |
|---|---|
Echter Spot |
der Roboter fährt |
Trockenlauf (nur Text) |
nichts bewegt sich, das Programm läuft durch — keine Position |
Übungsraum (virtuell) |
Spot fährt durch das gewählte Zimmer, als Zeichnung |
Übungsraum 3D (MuJoCo) |
dasselbe im 3D-Zimmer mit Kameras, Tiefengitter und Kollision — nur, wenn spotsim installiert ist (unten) |
Bei Übungsraum (virtuell) geht ein eigenes Fenster auf und zeichnet die
Fahrt mit, während sie läuft — Weg, Blickrichtung, Anstösse, die letzte
Ausgabezeile. Leg es neben den Editor, dann siehst du Code und Fahrt zugleich.
Der Knopf „▶ Offene Datei starten" im Raumeditor stellt diese Wahl selbst
ein, speichert vorher, wenn nötig, und kann den echten Spot nicht erreichen.
Auch ein Start aus „Code" fährt im Raum, der im Raumeditor offen ist — ist der
noch nicht gespeichert (neu, rekonstruiert oder geändert), fragt spotlab nach
einem Namen; wer abbricht, startet nichts. Das Übungsfenster zeigt danach den
Raum, den der Lauf wirklich bekommen hat: fährt ein Programm ohne Raum, bleibt
die Zeichnung leer und sagt es.
spot.tags() und spot.obstacles() arbeiten dabei aus der Raumgeometrie, du
kannst also die ganze Bibliothek ohne Roboter üben.
Eigene Räume: eine TOML-Datei unter <arbeitsordner>/raeume/<name>.toml — der
Raumeditor schreibt sie; von Hand geht es auch, gebaut wie die mitgelieferten
unter welt/vorlagen/.
Beispiele. Im Arbeitsordner liegt immer das Projekt Beispiele — die GUI
legt es beim Start an und ergänzt, was fehlt; deine Änderungen bleiben.
durchgang_finden.py zeigt, wie Spot laufend — ohne anzuhalten, mit
spot.walk(…, stop=False) — aus spot.obstacles().free_distance(…) die
offenste Richtung wählt, durch die Tür in der Mitte in den Nachbarraum geht,
den Tag dort als Beweis nimmt und sich setzt — im 2D-Raum, in 3D und am echten
Spot dasselbe Programm.
backend="mujoco" ist derselbe Sim mit einem Körper: der Menagerie-Spot aus
matura-spot, kinematisch gesetzt (Weg A — Wiedergabe statt Regelung, Spec
docs/superpowers/specs/2026-09-06-uebungsraum-3d-design.md). Was dazukommt:
- Kollision an der echten Geometrie statt an einem Kreis (
welt/kollision.py). spot.obstacles()aus fünf gerenderten Tiefenbildern, durch denselben Entpacker wie am Roboter — mit Verdeckung und unbekannten Zellen: was hinter dem Tisch liegt, ist nicht frei, weil ungesehen.spot.tags()mit Kamerablickfeld, Ausrichtung und Sichtstrahl — ein Tag hinter dem Tisch bleibt unsichtbar.spot.camera("frontleft_fisheye_image")und die fünf Tiefenkameras.- Das Übungsfenster zeigt das gerenderte Zimmer über der Zeichnung. Verfolgen
schaltet auf eine Kamera, die schräg hinter Spot mitfährt und in seine Richtung
schaut; das Mausrad über dem Bild zoomt in beiden Ansichten. Der Wunsch geht
als
kamera.jsonins Lauf-Verzeichnis und bleibt über Läufe hinweg erhalten.
Selbst fahren. „🎮 Fahren" im Raumeditor startet fahren.py aus dem Projekt
Beispiele im gewählten Raum (3D, wenn MuJoCo da ist, sonst 2D) und öffnet das
Übungsfenster mit der Verfolgerkamera; solange gefahren wird, gehören alle Tasten
diesem Fenster. Dort fährst du mit W/S vor und zurück,
A/D seitwärts, Q/E drehen; die Leertaste hält, „Stopp" beendet. Die
Tasten gehen als fahrt.json ins Lauf-Verzeichnis, und ein Befehl, der älter ist
als eine halbe Sekunde, heisst Stopp — losgelassene Taste, eingeschlafene GUI.
Es ist ein Lauf wie jeder andere: aufgezeichnet, mit den Tempogrenzen aus der
Konfiguration, und danach als Video zu haben.
Gemessen sind Gangarten (12.08.2026) und die Antwort auf move() (02.09.2026:
Anfahren, Reisetempo, Bremsen, Totzeiten); die Standhöhe folgt aus den
gemessenen Winkeln über die Kinematik des Modells und liegt 2–8 mm neben der
gemessenen. Annahmen stehen in bericht() und in lauf.json.
Video. Nach einem Lauf steht im Übungsfenster „🎬 Video speichern", in
der Ansicht „Läufe" derselbe Knopf für jeden alten Lauf — auch für 2D- und für
echte Läufe: das Video entsteht nachträglich aus zustand.jsonl (Pose und
zwölf Gelenke, 10 Hz, auf 30 fps interpoliert), nicht aus dem Bildschirm. Auf
der Kommandozeile: spotlab film <lauf> → <lauf>/film.mp4.
Installation (einmalig; matura-spot liegt neben spotlab):
pip install -e .[sim]
pip install -e ../matura-spot
python ../matura-spot/scripts/fetch_menagerie.py
Glas löst kein Sensor des Spot: die Stereokameras schauen hindurch, das Hindernisgitter meldet freie Fläche. Dasselbe gilt für eine Treppe hinter der Ecke oder einen Bereich mit teurem Gerät. Wer es weiss, ist der Mensch.
Im Raumeditor gibt es dafür das Werkzeug Sperrzone: ein Rechteck aufziehen, Namen und
Grund eintragen („Glasfront"). Der Roboter fährt nicht hinein — im Übungsraum bleibt er
an der Grenze stehen wie an einer Wand, und am echten Gerät nimmt das Explorationsskript
aus matura-spot die Vorwärtsfahrt aus dem Kommando (--raum <name>).
Eine Zone ist eine Regel, kein Hindernis: sie steht in keinem Hindernisgitter und taucht in keiner Rekonstruktion auf. Genau deshalb hält sie dort, wo der Sensor „frei" sagt.
Am echten Roboter braucht sie die Verortung: der Raum trägt seit Fassung 5 seinen
Kartenbezug ([karte]), das Skript spielt die Karte ein und verortet sich; ohne das fährt
es nicht vorwärts. Eine Zone an der falschen Stelle wäre schlimmer als keine.
In der Ansicht Fahren startet „🎮 Fahrt beginnen" das Programm fahren.py aus dem
Projekt Beispiele am echten Spot — über denselben Startweg wie „Starten" im Editor, also
mit Lease, Not-Aus-Endpunkt, den Tempogrenzen aus der Konfiguration und der Aufzeichnung
wie bei jedem Lauf. Sobald der Lauf steht, gehören die Tasten dem Tab: W/S vor und
zurück, A/D seitwärts, Q/E drehen, Leertaste oder Esc hält. Losgelassen
heisst Stopp — ein Fahrbefehl, der älter ist als eine halbe Sekunde, gilt nicht mehr, und
jedes Kommando trägt eine Endzeit von rund einer Sekunde: stirbt die GUI oder der Lauf,
steht der Roboter. Wechselst du den Reiter oder verliert das Fenster den Fokus (Alt-Tab mit
gehaltenem W), lässt der Tab alle Tasten los und Spot steht.
Der Blick nach vorn. Während der Fahrt zeigt der Tab, was die beiden Frontkameras sehen — zu einem Bild zusammengesetzt wie auf dem Tablet, in Farbe, wo der Spot Farbe kann, und so schnell, wie er Bilder liefert (die erreichte Rate steht unter dem Bild). Im Übungsraum steht dort stattdessen das gerenderte Zimmer von aussen. Beides kommt als Datei aus dem Lauf-Verzeichnis; die Oberfläche spricht nie selbst mit dem Roboter.
Drei Tempostufen, als Auswahl im Tab oder mit den Tasten 1, 2, 3 während
der Fahrt: Langsam 0.2 m/s (vorausgewählt), Normal 0.4 m/s, Schnell 0.8 m/s; seitwärts und
drehen skalieren mit. Der Deckel aus config.toml gilt zusätzlich, „Schnell" ist genau die
Voreinstellung max_speed, und die Drehrate wird dort auf max_turn_rate geklemmt. Ein
Wechsel wirkt sofort, auch bei gehaltener Taste. Dieselben Stufen gelten im Übungsfenster
(dort ist Normal vorausgewählt). „■ Stopp" ist derselbe freundliche Stopp wie in der
Live-Ansicht, der NOT-AUS steht im Kopf, in jedem Reiter. Freifläche, Aufsicht, Tablet mit
Not-Aus in Reichweite — und vor dem ersten Mal Abnahmepunkt A1 (docs/ABNAHME.md), sonst
ist nicht belegt, dass der Not-Aus am Tablet während des Laufs wirkt.
Halte ein AprilTag in der Hand oder häng es an den Rucksack, starte
Beispiele/folgen.py und geh los. Spot dreht sich zu dir und hält Abstand.
Das Programm besteht aus zwei Teilen, die getrennt austauschbar sind. Ein Ziel-Finder sagt nur, wo das Ziel ist: Peilung in Grad, Abstand in Metern. Der Regler hält daraus Abstand und Kurs. Damit lassen sich Strategien vergleichen, statt sie zu behaupten:
folgen.folge(spot, folgen.tag_finder(), lauf_dir=spot.recorder.dir) # AprilTag
folgen.folge(spot, folgen.personen_finder(), lauf_dir=spot.recorder.dir) # Spots TrackerGesichter, und wo sie aufhören. gesicht_finder sucht mit YuNet aus OpenCV Gesichter
im zusammengesetzten Kamerabild. Das braucht das Extra pip install "spotlab[gesicht]" und
die Modelldatei face_detection_yunet_2023mar.onnx unter ~/.spotlab/modelle/ (oder
SPOTLAB_GESICHTSMODELL). Sie liegt im OpenCV-Zoo
— über git-lfs, der gewöhnliche raw-Link liefert also einen 131 Byte grossen Zeiger
statt der 232 589 Bytes des Modells. Fehlt sie, fällt der Finder beim ersten Aufruf aus, das
Tag trägt allein weiter, und der Grund steht genau einmal in runs/<lauf>/diagnose.log.
Wer meint, der Erkenner erkenne ihn nicht, schaut zuerst dort nach.
Zwei Dinge sind daran gemessen und nicht behauptet:
| Abstand | höchster sichtbarer Punkt |
|---|---|
| 1.5 m | 1.20 m |
| 2.0 m | 1.45 m |
| 3.0 m | 1.94 m |
Das Bild, das zum Erkenner geht, wird vorher aufgehellt — die Frontbilder sind im Gebäude dunkel (gemessen: mittlere Helligkeit 36 von 255), und YuNet blieb darauf mit 0.37 unter der Schwelle. Nach dem Histogrammausgleich findet es dasselbe Gesicht mit 0.71. Die Fahransicht bleibt unberührt. Ehrlich dazu: auch danach fand der Erkenner das Gesicht nur in 2 von 60 Takten — die Grenze ist die Geometrie, nicht die Helligkeit.
Spots Frontkameras schauen nach unten. Ein stehender Mensch hat deshalb erst ab gut zweieinhalb Metern ein Gesicht im Bild; näher sieht Spot Beine. Und ein Kasten mit hoher Punktzahl ist noch kein Gesicht: über eine Aufzeichnung vom 12.08.2026 fand der Erkenner vier Kästen, davon war der beste eine Stuhllehne und der zweitbeste ein Schienbein. Deshalb prüft spotlab jeden Kasten gegen die Geometrie nach — aus Höhenwinkel und gemessener Entfernung aus der Tiefenkamera folgt die Höhe über dem Boden, und was nicht auf Kopfhöhe liegt, fällt heraus. Die Entfernung wird nie aus der Kastengrösse geschätzt: an ihr hängt der Mindestabstand.
Weil die Gesichtssuche unterhalb von zweieinhalb Metern nichts findet, staffelt man sie:
folgen.folge(spot, folgen.zuerst(folgen.gesicht_finder(), folgen.tag_finder()), lauf_dir=…)zuerst nimmt den ersten Finder, der etwas findet. Fällt einer aus, weil OpenCV oder das
Modell fehlt, tragen die übrigen weiter, und der Grund steht im Protokoll.
personen_finder nutzt Spots eigenen Personen-Tracker über spot.people(). Kein eigenes
Modell: der Tracker steckt in der Firmware und liefert Nummer, Position, Geschwindigkeit und
eine Sicherheit. Ob dein Roboter ihn hat, zeigt erst das Gerät — findet er niemanden, bleibt
Spot stehen.
Am Schul-Spot gemessen (11.09.2026): dieser Roboter verfolgt keine Menschen. 560 Abfragen über drei Läufe, kein einziges verfolgtes Objekt — und das ohne jeden Filter, während derselbe Dienst gleichzeitig AprilTags meldete. Der Finder ist trotzdem richtig und bleibt; er findet hier nur nichts. Die Zahlen stehen bei Abnahmepunkt A34 Teil 1.
Die Schranken gelten alle gleichzeitig, und jede, die ihre Daten nicht lesen kann, verbietet die Fahrt statt sie zu erlauben:
| Schranke | Wirkung |
|---|---|
| Mindestabstand | Näher kommt Spot nie, und rückwärts fährt er gar nicht — nach hinten sieht er nichts. |
| Ziel verloren | Er hält sofort, im selben Takt, nicht nach einer Frist. |
| Hindernisgitter | Vorwärts nur, wenn voraus Platz gemeldet ist. |
| Kopfraum | Vorwärts nur, wenn nichts über dem Weg hängt — das Gitter ist eine Bodenkarte und sieht keine Tischplatte. |
| Sperrzonen | Nur mit Raum und Verortung; ohne beides sagt er es, statt Sicherheit vorzutäuschen. |
Höher schauen während der Fahrt. Spots Frontkameras schauen nach unten, und deshalb
kommt ein stehendes Gesicht erst spät ins Bild. Mit blick_grad hebt Spot beim Gehen die
Nase, und die Kameras schauen mit:
| Nickwinkel | Gesicht 1.60 m sichtbar ab | Boden erst ab |
|---|---|---|
| 0° | 2.31 m | 0.32 m |
| 10° | 1.46 m | 0.52 m |
| 15° | 1.18 m | 0.66 m |
folgen.folge(spot, finder, lauf_dir=…, blick_grad=12)Zehn bis fünfzehn Grad reichen, damit ein Gesicht schon auf Folgeabstand im Bild ist. Der Preis steht in der rechten Spalte: der Boden dicht vor den Füssen fällt aus dem Blick, und das ist der Bereich, in dem die Hindernisschranke prüft. Deshalb ist die Vorgabe null. Die Neigung bleibt auch im Stehen, sonst legt Spot die Nase ab, sobald er im Wunschabstand ist, verliert das Gesicht und pendelt. Wie viel Neigung der Roboter im Laufen wirklich zulässt, misst Abnahmepunkt A34.
Der Tempodeckel aus config.toml gilt zusätzlich. Spot bewegt sich dabei autonom:
Freifläche, Aufsicht, Tablet mit Not-Aus in Reichweite, und vor dem ersten Mal A1 und A34.
Ein Versuch aus der Verhaltensbiologie: wie schnell gehen Leute, und hängt das von der Gruppengrösse ab? Mit der Stoppuhr in der Hand misst man immer nur eine Gruppe. Spot stellt sich seitlich in den Gang und stoppt für jeden gleichzeitig.
Aufbau: zwei AprilTags an die Enden der Strecke, Spot so hinstellen, dass er beide sieht,
jede Person trägt ein Tag am Rucksack. Dann im Reiter Gehzeit auf „▶ Versuch
starten" drücken — oder Beispiele/gehzeit.py aus dem Editor starten, wenn du am
Programm etwas ändern willst.
Strecke 8.42 m zwischen Tag 1 und Tag 2 (Streuung 0.03 m, 12 Abtastungen).
tag7: 6.10 s für 8.42 m = 1.38 m/s
tag8: 6.35 s für 8.42 m = 1.33 m/s
tag9: verworfen (gestoppt)
Die Strecke wird gemessen, nicht eingetippt. Ein eingetippter Wert stimmt beim ersten Aufbau und bleibt stehen, wenn jemand die Tags am nächsten Tag zwei Meter weiter hängt — die Zeiten sähen weiter richtig aus, und jedes Tempo wäre um denselben Faktor falsch. Gerechnet wird über den Median mehrerer Blicke, und wie stark sie streuten, steht daneben.
Spot bewegt sich dabei nicht und hält kein Lease (connect(nur_lesen=True)): die
Aufsichtsperson behält das Tablet in der Hand.
Wer stehen bleibt, umkehrt oder verschwindet, wird verworfen — mit Grund in der Tabelle, nicht stillschweigend.
Die Gruppengrösse trägt ein Mensch nach. Ob drei Leute eine Gruppe waren oder drei Einzelne, die zufällig gleichzeitig losgingen, kann Spot nicht wissen. Er schlägt vor, wer gleichzeitig unterwegs war, und hält Bilder des Abschnitts bereit. Derselbe Reiter zeigt sie: links die Durchgänge, rechts die Bilder zum ausgewählten, darunter Klasse und Gruppengrösse. Wer lieber tippt:
python -m spotlab.experiment.nachtrag runs/20260910T094103Z_ab12cd34So oder so stehen dort die Durchgänge mit Uhrzeit, Zeit und Tempo, dazu die Bilder. Klasse
und Gruppengrösse kommen in dieselbe Tabelle — gehzeit/gehzeit.csv, Semikolon und
Dezimalkomma, öffnet sich mit einem Doppelklick. Am Schluss fragt das Programm, ob die
Bilder gelöscht werden sollen: es sind Aufnahmen von Mitschülern, gemacht für genau eine
Frage.
Kein Alter aus dem Gesicht, und das ist Absicht: über die Aufzeichnung vom 12.08.2026 fand der Gesichtserkenner in 4 von 107 Takten etwas, und die zwei nachgesehenen Treffer waren eine Stuhllehne und ein Schienbein. Eine Altersschätzung darauf stünde ununterscheidbar neben gemessenen Zeiten.
Der Spot führt selbst eine Liste der Objekte, die er erkennt: AprilTags, Dockingstationen, Türen. Dazu ein Hindernisgitter mit dem Abstand zum nächsten Hindernis je Zelle.
In der Ansicht Umwelt steht dafür ein Knopf: „Umgebung abfragen".
apriltag 1 2.06 m +14 Grad vor 3 s
dock 3 1.00 m +0 Grad vor 3 s
Die Abfrage hält kein Lease und sendet kein Kommando. Der Spot kann sich dadurch nicht bewegen, und sie funktioniert, während jemand anderes mit dem Tablet fährt. Genau dafür ist sie gedacht: nachsehen, ob Spot einen Tag überhaupt erkennt und aus welcher Entfernung — bevor ein Programm startet, das ihn bewegt.
Im eigenen Programm ist das connect(nur_lesen=True): die Sitzung meldet sich an und
synchronisiert die Uhr, holt aber kein Lease und registriert keinen Not-Aus-Endpunkt.
Sie kann den Spot nicht bewegen — nicht, weil das Programm es unterlässt, sondern weil
die Fähigkeit fehlt.
Im eigenen Programm sind es die Verben spot.tags(), spot.world_objects() und
spot.obstacles() (siehe unten).
import spotlab
with spotlab.connect() as spot:
spot.power_on()
spot.stand()
spot.move(forward=1.0) # 1 m vorwärts, wartet bis angekommen
spot.move(turn=90) # 90 Grad drehen
bild = spot.camera("frontleft")
bild.save("vorne.png")
print(spot.battery, "%")connect() meldet an, synchronisiert die Uhr, registriert den Not-Aus und holt das
Lease. Am Ende des with-Blocks setzt sich der Spot hin und schaltet die Motoren ab —
auch wenn das Programm mit einem Fehler abbricht.
Die Motoren gehen beim Verbinden nicht an. spot.power_on() ist eine eigene Zeile,
die jemand geschrieben haben muss: ein 30-kg-Roboter steht nicht auf, nur weil jemand
ein Programm gestartet hat.
| Bereich | Verben |
|---|---|
| Leistung | power_on(), power_off(), is_powered, battery |
| Haltung | stand(height=0.0), sit() |
| Bewegung | move(forward, left, turn), walk(vx, vy, wz, duration), stop() |
| Kameras | cameras(), camera(name), state |
| Umwelt | tags(id=None), world_objects(kinds=None), obstacles(), stairs() |
| Karten | load_map(name), localize(), navigate_to(ziel), waypoints() |
| Messen | messfenster(name, hz=50) |
| Roh | robot, send(command), close() |
move() ist eine relative Zieltrajektorie und wartet auf die echte Rückmeldung des
Roboters — turn in Grad. walk() ist das Geschwindigkeitskommando des SDK und
sendet intern automatisch nach, weil Geschwindigkeitskommandos beim echten Spot
ablaufen.
spot.cameras() meldet, was das aktive Backend wirklich hat. Fehlt eine Fähigkeit,
gibt es einen klaren Fehler statt einer Attrappe.
spot.stairs() nennt die Treppen in Sicht, nächste zuerst: direction („auf" — Spot
steht am Fuss, „ab" — am Kopf), steps, rise_m, distance, bearing zur nächsten
Kante und axis_bearing, die Richtung bergauf. spot.move(turn=treppe.axis_bearing)
stellt die Nase bergauf, und so nimmt Spot jede Treppe: vorwärts hoch, rückwärts
runter — ohne sich oben umzudrehen. Am Roboter kommen die Treppen aus der Firmware,
im Übungsraum aus dem Raum; das Beispiel treppe_steigen.py zeigt den ganzen Weg.
Die Umwelt-Verben sprechen Grad und Meter, dieselbe Einheit wie move():
for tag in spot.tags():
print(f"Tag {tag.id}: {tag.distance:.1f} m, {tag.bearing:+.0f} Grad")
spot.move(turn=spot.tags()[0].bearing) # dreh dich zum nächsten TagDie Liste ist nach Distanz sortiert, spot.tags()[0] ist also ohne Nachdenken das
nächste Ziel. Nichts gesehen ist eine Antwort, kein Fehler — dann kommt [] zurück.
spot.obstacles() liefert das Hindernisgitter mit distance_at(x, y) und
is_free(x, y); wo Spot nicht hingesehen hat, gilt „nicht frei", nicht „frei".
| Kommando | Zweck |
|---|---|
spotlab login |
IP, Benutzer und Passwort hinterlegen |
spotlab doctor |
stufenweise Diagnose, bricht beim ersten Fehler ab; sagt auch, was dieser Roboter kann (Lizenz, Nutzlasten, Dienste) und wann sein Zertifikat abläuft |
spotlab new <name> |
Projekt mit lauffähiger Vorlage anlegen |
spotlab open [projekt] |
in VS Code öffnen |
spotlab run <datei> [--dryrun] |
starten und aufzeichnen |
spotlab runs · runs <id> |
Läufe auflisten und ansehen |
spotlab lease [--take] |
wer steuert den Spot; bewusste Übernahme |
spotlab gui |
das Fenster öffnen (braucht pip install -e .[gui]) |
spotlab maps |
aufgezeichnete Karten auflisten |
spotlab record-map <name> |
eine GraphNav-Karte aufzeichnen |
spotlab mcp |
MCP-Server für Agenten, über stdin/stdout |
Die Aufzeichnung hängt an connect(), nicht an spotlab run — wer in VS Code F5
drückt, bekommt sie genauso.
Pro Lauf ein Verzeichnis unter runs/<zeitstempel>_<skript-hash>/:
| Datei | Inhalt |
|---|---|
lauf.json |
Metadaten, Roboterkennung, Ergebnis, Dauer |
ereignisse.jsonl |
Kommandos, Rückmeldungen, Fehler, mit Zeitstempel |
zustand.jsonl |
10 Hz: Pose, Geschwindigkeit, 12 Gelenke, Fusskontakte, Akku |
bilder/ |
einzelne Kamerabilder aus spot.camera(...), mit Intrinsics |
kamera/ |
nur im Beobachter-Modus: laufender Bildmitschnitt aller Kameras |
kamera/ ist bewusst von bilder/ getrennt. Dort liegen die Schnappschüsse, die ein
Schülerskript anfordert — eine Handvoll je Lauf, und die Live-Ansicht sucht darin bei
jedem Takt das neueste Bild. Der Mitschnitt einer Messfahrt sind zehntausend Dateien;
er bekommt deshalb ein eigenes Verzeichnis mit anhängendem Index kamera.jsonl.
Bilder liegen dort so, wie der Roboter sie geschickt hat: JPEG als .jpg, Tiefe roh
als .raw — umkodieren hiesse bei Tiefenbildern, die Millimeterwerte wegzuwerfen.
Zwei getrennte Wege, beide korrekt für ihre Situation:
- Geordnetes Ende (durchgelaufen, Ausnahme, Ctrl-C): stoppen → sicher hinsetzen → Motoren aus → Lease frei → Not-Aus-Endpunkt abmelden.
- Prozess hart getötet: die Keepalives sterben, der Roboter geht von selbst in den sicheren Zustand. Dieser Weg lässt sich nicht kaputtprogrammieren.
spotlab registriert seinen Not-Aus-Endpunkt zusätzlich zur bestehenden
Konfiguration, damit der physische Not-Aus am Tablet wirksam bleibt. Der bequeme
SDK-Weg force_simple_setup() würde ihn verdrängen und wird deshalb nicht benutzt.
Vor dem ersten Schülerbetrieb ist die Abnahmeliste in
docs/ABNAHME.mdabzuarbeiten, insbesondere Punkt A1 (Not-Aus-Koexistenz am echten Gerät).
In der Ansicht Spot lassen sich max_speed und max_turn_rate setzen; sie landen in
~/.spotlab/config.toml. Für eine Anfängerstunde lohnt sich 0.2 m/s statt der
Voreinstellung 0.6. Der Deckel wirkt auf allen drei Wegen — walk(), move() und die
autonome Fahrt — und ein Schülerskript kann ihn nicht überschreiben.
Werte, die keine Sicherheitsgrenze mehr wären, werden abgewiesen statt stillschweigend
übernommen: inf, 0 und negative Zahlen führen zu einer Fehlermeldung beim Start, nicht
zu einem wirkungslosen Deckel.
Für die Benutzung
- Abnahme am Gerät:
docs/ABNAHME.md— 22 Punkte, A1 ist der Sperrpunkt - Fremde Projekte anbinden:
docs/ANBINDUNG.md
Für die Entwicklung — jede Stufe hat ihren eigenen Entwurf mit Begründungen:
| Stufe | Entwurf |
|---|---|
| Fundament | 2026-08-06-spotlab-fundament-design.md |
| Oberfläche | 2026-08-07-spotlab-gui-design.md |
| GraphNav | 2026-08-07-spotlab-graphnav-design.md |
| Editor | 2026-08-07-spotlab-ide-design.md |
| Anbindung + MCP | 2026-08-07-spotlab-anbindung-design.md |
| Kalibrierung | 2026-08-08-spotlab-kalibrierung-design.md |
| Beobachter-Modus | 2026-08-09-spotlab-beobachtung-design.md |
| Umwelt | 2026-09-02-spotlab-umwelt-design.md |
| Übungsraum | 2026-09-02-spotlab-uebungsraum-design.md |
- Umsetzungspläne:
docs/superpowers/plans/ - Härtung, Befunde und Fahrplan:
docs/HAERTUNG.md - Nicht verhandelbare Regeln und ihre Begründungen:
CLAUDE.md
pytest
Alle Tests laufen ohne Roboter und ohne Netz. Das dryrun-Backend ist das
Standard-Testdouble; die Kommando-Protobufs werden dabei echt gebaut und gegen die
SDK-Schemata geprüft.
with connect() as spot:
if spot.supports("depth"):
depth = spot.depth("frontleft")
print(depth.distance_at(212, 120)) # Meter oder None
cloud = depth.point_cloud(frame="body", stride=2)
cloud.save("punkte.ply")
if spot.supports("local_grid"):
print(spot.grid_types())Tiefenbilder, Punktwolken und LocalGrids: Rohdatenexport, Gültigkeitsmasken und Koordinatenrahmen. Keine Motorfreigabe nötig; connect() verwendet weiterhin den normalen Sitzungskern.
Neu in der Backend-Auswahl: Physik 3D (experimentell, ebener Boden). Der Koerper wird von Kontaktkraeften getragen. Start im Stand; walk/stop, Sensorik und Livebild sind angebunden. Treppen, sit und move sind noch nicht unterstuetzt. Anleitung und Grenzen.
Experimenteller Einzelstufenversuch: physik_einzelstufe.py mit Raum
physik_einzelstufe und Start (0, 0, 0) im Modus Physik 3D.
6-cm-Podest vorwärts hinauf, rückwärts herunter; mehrere Minuten Laufzeit.
Grenzen und Messwerte: docs/PHYSICS.md.
Neu: physik_treppe_3stufen.py mit Raum physik_treppe_3stufen,
Physik 3D und Start (0,0,0): drei niedrige 4-cm-Stufen vorwärts hinauf und
rückwärts herunter. Etwa 7–12 Minuten; Grenzen und Messwerte in docs/PHYSICS.md.
Spotlab steht unter der MIT-Lizenz. Ausgenommen sind die unverändert
übernommenen Dateien aus dem OpenCV Model Zoo in src/spotlab/backends/real/zoo/
(Apache 2.0, Lizenz im selben Ordner). Die Erkennermodelle im Schüler-ZIP stehen
unter MIT beziehungsweise Apache 2.0; siehe tools/modelle_lizenzen.txt.