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.py mit 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 1 ist 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:

PortZweck
tcp:5760SITL SERIAL0 — nur MAVProxy hängt hier dran
udp:14551GCS (QGroundControl / Mission Planner)
udp:14552dein Test-Skript / Automatisierung

Falle tcp:5760: SITL beendet sich, sobald sein SERIAL0-TCP-Client die Verbindung schließt („Closed connection on SERIAL0" → „Exitting"). Klemme deine Tests deshalb nie direkt auf 5760, 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):

EEPROMParams + SkripteErgebnis
frischvanillaOK, 2,56 m / 0,3°
frischProjektOK, 2,55 m / 1,5°
altProjektkippt, 0,14 m / 50,7°
frisch + SIM_ENGINE_FAIL=2vanillakippt, 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.bin ist ein unsichtbarer Parameter-Speicher. Er gewinnt gegen --defaults und ist per grep nicht 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:5760 ist 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 -w gesetzt
  • ArduCopter-Binary + MAVProxy als getrennte Prozesse gestartet
  • GCS auf 14551, Test auf 14552 — nicht auf 5760
  • Erster Test: Heartbeat, Arm, Mode, Takeoff über pymavlink
  • Nach dem Lauf: Prozesse sauber killen, Logs sichern