Bevor an einer Drohne echte Propeller drehen, will man das Flugverhalten unter Laborbedingungen sehen: Sensor-Ausfälle, Wind, schiefe Dächer, kaputte Parameter. Genau dafür gibt es SITL (Software In The Loop) — ArduPilot läuft dann als normales Linux-Programm mit einem simulierten Fahrzeug, und du redest über dieselben MAVLink-Ports mit ihm wie mit einem echten Pixhawk.
Der Clou: Das braucht keinen Simulator mit 3‑D-Grafik und keine Grafikkarte. Ein kleiner Linux-Server oder eine VM im Homelab reicht. In diesem Artikel richte ich SITL headless ein — ohne GUI, nur mit MAVLink-Ports —, damit sich Flüge automatisiert testen lassen.
Warum headless
SITL gibt es in zwei Geschmäckern:
- Interaktiv über
sim_vehicle.pymit MAVProxy-Konsole (und optional Kartenfenster). Gut zum Ausprobieren von Hand. - Headless: der ArduCopter-Binary startet direkt, MAVProxy hängt als Daemon dahinter, und ein Test-Skript verbindet sich auf einen UDP-Port. Das läuft ohne Terminal, ohne X11 — und damit auf jedem Server.
Für automatisierte Tests ist der zweite Weg der richtige. Er ist auch der leichtere, sobald die Eigenheiten klar sind.
Wo das läuft
SITL ist ein normales Linux-Programm (ArduPilot baut es als sitl-Target). Es
läuft auf jeder Debian-/Ubuntu-Maschine — bei mir in einer kleinen
Linux-VM im Homelab. Wichtig sind nur:
- CPU: SITL mit
--speedup 1ist Echtzeit; eine VM mit zwei Kernen genügt. - Netzwerk: ein Haufen UDP-Ports, alle an
127.0.0.1— nach außen braucht nichts geöffnet zu werden. - User-Land: Python + MAVProxy (
pip3 install MAVProxy). Kein Kernel-Modul, kein Passthrough. Ein LXC/Container funktioniert genauso wie eine VM, solange UDP lokal erlaubt ist.
Vorbereitung
Das ArduPilot-Repo wird einmal geklont; sim_vehicle.py baut das SITL-Target
beim ersten Aufruf selbst:
git clone --recurse-submodules https://github.com/ArduPilot/ardupilot.git
cd ardupilot
# einmalig: Toolchain + Abhängigkeiten
./Tools/environment_install/install-prereqs-ubuntu.sh -y
# MAVProxy
pip3 install MAVProxy
Der headless-Start
Das Herzstück sind zwei Prozesse: der ArduCopter-SITL-Binary und MAVProxy
dahinter. Der Binary spricht auf SERIAL0 TCP; MAVProxy ist der Brückenkopf,
der die Daten auf UDP-Ports verteilt, auf die sich Tests/GCS verbinden können.
# Arbeitsverzeichnis — hier landet auch das eeprom.bin (dazu unten)
mkdir -p ~/sitl/run && cd ~/sitl/run
# ArduPilot-Binary starten: Modell X (Quad), EEPROM wischen (-w),
# Home = Beispielkoordinate, Instanz 0
~/ardupilot/build/sitl/bin/arducopter \
--model x \
-w \
--speedup 1 \
--slave 0 \
--defaults ~/ardupilot/Tools/autotest/default_params/copter.parm,./sitl_params.parm \
--sim-address=127.0.0.1 \
-I0 \
--home=<LAT>,<LON>,584,353 &
COPTER_PID=$!
sleep 3
# MAVProxy als Brücke: SERIAL0 rein, GCS + Test-Ports raus
mavproxy.py \
--master tcp:127.0.0.1:5760 \
--sitl 127.0.0.1:5501 \
--out "udp:127.0.0.1:14551" \
--out "udp:127.0.0.1:14552" \
--daemon &
MAVP_PID=$!
Die Ports:
| Port | Zweck |
|---|---|
tcp:5760 | SITL SERIAL0 — nur MAVProxy hängt hier dran |
udp:14551 | GCS (QGroundControl / Mission Planner) |
udp:14552 | dein Test-Skript / Automatisierung |
Falle
tcp:5760: SITL beendet sich, sobald seinSERIAL0-TCP-Client die Verbindung schließt („Closed connection on SERIAL0" → „Exitting"). Klemme deine Tests deshalb nie direkt auf5760, sondern immer auf die UDP-Ports — sonst stirbt dir die Simulation mitten im Lauf.
Der EEPROM-Fallstrick
Jetzt zum teuren Teil. SITL persistiert alle Parameter in einer eeprom.bin
im Arbeitsverzeichnis. Und dieser gespeicherte Wert gewinnt gegen
--defaults. Ein einmal gesetzter Parameter überlebt damit beliebig viele
Neustarts, obwohl er in keiner .parm-Datei steht und per grep nicht
auffindbar ist.
Bei mir stand aus einer früheren Sitzung noch ein simulierter Motorausfall in der
eeprom.bin (SIM_ENGINE_FAIL auf Motor 2). Folge: Der Quad kippte bei jedem
Takeoff über eine Arm-Achse, kam nie über 14 cm Höhe, und der Schub sättigte auf
100 %. Keine Zeile im Setup erklärte das — der Grund lag in der Altlast im
EEPROM.
Die Beweis-Matrix (Takeoff auf 3 m, erreichte Höhe / maximale Neigung):
| EEPROM | Params + Skripte | Ergebnis |
|---|---|---|
| frisch | vanilla | OK, 2,56 m / 0,3° |
| frisch | Projekt | OK, 2,55 m / 1,5° |
| alt | Projekt | kippt, 0,14 m / 50,7° |
frisch + SIM_ENGINE_FAIL=2 | vanilla | kippt, 0,14 m / 98,7° |
Erst die letzte Zeile machte klar: nicht das Setup war schuld, sondern der
Alt-Parameter. Die Lösung ist das Wisch-Flag -w (siehe oben) — bei jedem
Start das EEPROM leeren, damit jeder Lauf von derselben Basis startet. Nur wenn
Parameter bewusst über einen Neustart erhalten bleiben sollen, lässt man -w
weg — und lebt dann mit der Regel „EEPROM schlägt --defaults".
Verbinden und testen
Die Simulation steht, sobald MAVProxy Daten auf 14551 wirft. Ein minimaler
Check mit pymavlink:
from pymavlink import mavutil
# an den Test-Port hängen
m = mavutil.mavlink_connection("udpin:127.0.0.1:14552")
m.wait_heartbeat(timeout=10)
print("Heartbeat von System", m.target_system, "Komponente", m.target_component)
Armen, Modus setzen und einen Takeoff kann man in einer echten Session über die MAVProxy-Konsole oder ein Skript fahren — der Punkt hier ist: der Test spricht MAVLink, genau wie gegen Hardware.
Lessons Learned
- Headless ist der Test-Pfad. GUI braucht man beim Entwickeln von Abläufen nicht — und ohne GUI läuft es auf jedem Server.
eeprom.binist ein unsichtbarer Parameter-Speicher. Er gewinnt gegen--defaultsund ist pergrepnicht zu finden. Bei Tests:-w.- Verifiziere die Testbasis, nicht nur das Ergebnis. Der Quad kippte nicht, weil das Projekt kaputt war, sondern weil die Umgebung (EEPROM) nicht die dokumentierte war.
tcp:5760ist MAVProxy. Der Test gehört auf UDP — sonst killst du die Simulation beim Verbinden.
Checkliste
- ArduPilot geklont, SITL gebaut, MAVProxy installiert
- Arbeitsverzeichnis mit
sitl_params.parm, Wisch-Flag-wgesetzt - ArduCopter-Binary + MAVProxy als getrennte Prozesse gestartet
- GCS auf
14551, Test auf14552— nicht auf5760 - Erster Test: Heartbeat, Arm, Mode, Takeoff über pymavlink
- Nach dem Lauf: Prozesse sauber killen, Logs sichern