- Python 81.7%
- HTML 15.3%
- Batchfile 2%
- Shell 1%
| docs/plans | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.bat | ||
| build.sh | ||
| LICENSE | ||
| pyproject.toml | ||
| QUICKSTART.md | ||
| README.md | ||
| Screenshot 2026-05-29 120124.png | ||
| simlauncher.bat | ||
| simlauncher.sh | ||
| tunerview.bat | ||
| tunerview.sh | ||
| uv.lock | ||
simlauncher
Launch and manage multiple ArduCopter SITL drones for testing QGroundControl (QGC) and Rift Ground Control (RGC). Web UI by default — runs headless on a Raspberry Pi 5 and is driven from any browser on the LAN. An optional desktop (Qt) UI is available for local use.
- Targets: Linux ARM64 (Raspberry Pi 5 / Debian / Ubuntu), Linux x86_64, macOS arm64.
- SITL binaries: auto-downloaded from
Unmanned-Software-Solutions/ardupilot-sitl-binaries
on first run (or point
SIMLAUNCHER_BINARYat your own). - Rift integration: can launch
rift-drone-servicesand MAVProxy per drone so RGC discovers the sim drones over mDNS exactly like real hardware.
Two UI modes
| Mode | How to launch | Needs | Use |
|---|---|---|---|
| Web (default) | uv run simlauncher → open http://<host>:8080 |
base deps only | Headless Pi, shared/remote control, multiple operators |
| Desktop (Qt) | uv run simlauncher --gui |
--extra gui |
Local workstation, single user |
The persisted default is stored in config (ui_mode); the web UI's ⚙ settings can flip it and
restart. --web / --gui on the command line override it for that launch.
Quick start (Raspberry Pi 5 / Linux)
# 1. install uv (manages Python 3.12 + deps)
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env
# 2. clone + install
git clone git@forge.riftdynamics.io:MasterPotato/SimLauncher.git ~/simlauncher
cd ~/simlauncher
uv sync # base (web) deps
# uv sync --extra mavproxy # add if you'll use the MAVProxy / RGC path
# uv sync --extra gui # add if you'll use the desktop UI or tunerview
# 3. run the web UI
uv run simlauncher --host 0.0.0.0 --port 8080
Then open http://<pi-ip>:8080 from any machine on the LAN.
The SITL binary auto-downloads on first launch and caches under
~/.simlauncher/bin/<platform>/bin/ArduCopter. Override with SIMLAUNCHER_BINARY=/path/to/ArduCopter.
Web UI
The header shows the global drone capacity (default 10, set with --max-drones). Work is
organised into sessions — each is an independent group of drones with its own config. Click
+ Session to add one (disabled when the pool is full); sessions are shared across every
browser and survive a page refresh.
Per session:
| Field | Meaning |
|---|---|
| Drones | How many SITL instances (capped to remaining pool slots) |
| Base Lat/Lng/Alt/Hdg | Home; drones auto-space in a ~3 m grid |
| QGC / GCS IP | Optional. Set a plain QGC computer's IP to also push MAVLink there. Blank = RGC discovery only (the custom QGC connects via Rift discovery) |
| Frame | Physics model (Stock quad, or Tulip 2 kg) |
| Rift Services | Launch rift-drone-services per drone for RGC discovery (needs the binary path + D-Bus) |
| MAVProxy | Route MAVLink through MAVProxy so RGC can connect to the advertised port (needs the [mavproxy] extra) |
| Prefix (RGC) | rift-drone-services device-id prefix — drones are <prefix>-01, <prefix>-02, … and RGC matches them by prefix. Blank = sim |
Hit Start on a session; live per-drone logs stream into tabs over WebSocket.
Ports
For global drone index N (0-based, across all sessions):
| Consumer | Port | Notes |
|---|---|---|
| QGC | 14550 + N |
SITL (or MAVProxy) pushes MAVLink here; QGC listens |
| RGC | 14650 + N |
MAVProxy udpin; rift-drone-services advertises this over mDNS |
| SITL→MAVProxy (internal) | 14540 + N |
Only when MAVProxy is enabled |
QGroundControl setup
- QGC / GCS IP in the session = your computer's IP (or blank for the same machine).
- In QGC → Application Settings → Comm Links, add a UDP link listening on each drone
port (
14550,14551, …). QGC's default only covers14550, so add one per drone. - Allow inbound UDP through your firewall. On Windows (PowerShell as Administrator):
New-NetFirewallRule -DisplayName "QGC MAVLink SITL" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 14550-14559 -Profile Any
Rift Ground Control (RGC) / rift-drone-services
To make sim drones discoverable by RGC exactly like real hardware:
- Build
rift-drone-servicesfor the host (from therift-dronerepo) and note its path. - Install MAVProxy:
uv sync --extra mavproxy(orsudo apt install python3-mavproxywhere available). - In the session: tick Rift Services (point it at the binary), tick MAVProxy, and
optionally set a Prefix so RGC can match this batch of drones (
<prefix>-01, …). Leave QGC / GCS IP blank — RGC connects via discovery, not a pushed stream.
RGC then discovers each drone over mDNS (_rift_drone._tcp), reads the advertised MAVLink port,
and connects through MAVProxy. rift-drone-services runs with --mock-modem --dry-run for sim.
D-Bus must be running (default on Raspberry Pi OS).
Running as a service (Pi)
# /etc/systemd/system/simlauncher.service
[Unit]
Description=simlauncher SITL web UI
After=network-online.target
Wants=network-online.target
[Service]
User=admin
WorkingDirectory=/home/admin/simlauncher
ExecStart=/home/admin/.local/bin/uv run --extra mavproxy simlauncher --host 0.0.0.0 --port 8080
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now simlauncher
journalctl -u simlauncher -f
Update: git pull && sudo systemctl restart simlauncher (a pull alone changes nothing until the
service restarts).
Desktop UI
For a local workstation with a display:
uv sync --extra gui --extra mavproxy
uv run simlauncher --gui
Same capabilities as the web UI (optional GCS IP, Rift Services, MAVProxy, device prefix) in a Qt window.
Scaling the ceiling
--max-drones bounds the total across all sessions (default 10, a comfortable Pi 5 limit). On a
faster dedicated machine, raise it:
uv run simlauncher --max-drones 40
Each SITL instance is roughly half a core + ~80–100 MB RAM at 1× speedup.
Frame profiles
- Stock quad — built-in 1.5 kg X-quad, generic tune.
- Tulip 2 kg (10-inch, 6S) — Rift's 10-inch airframe; mass, hover throttle, battery, and drag
calibrated from a real flight log so simulated cruise speed-vs-pitch-vs-throttle matches the real
drone. Ships a matching
--defaultsparam file.
Add more: drop src/simlauncher/resources/<airframe>.json (schema mirrors tulip_2kg.json) and add
a line to bundled_frame_profiles() in core/config.py.
Config & cache
- Config:
~/.simlauncher/config.json(SIMLAUNCHER_CACHE_DIRoverrides the base dir). - SITL binary cache:
~/.simlauncher/bin/<platform>/bin/. - Per-drone run dirs + logs:
~/.simlauncher/run/drone-<N>/.
Troubleshooting
| Symptom | Fix |
|---|---|
| Plain QGC sees nothing | Set GCS IP to your computer's IP (blank = RGC-only, no push); add a QGC UDP comm link per port; open the firewall (above). |
| RGC doesn't discover the drones | Make sure Rift Services + MAVProxy are ticked and rift-drone-services is running; check the drones' device-id Prefix matches what RGC expects. |
| MAVProxy checkbox does nothing / errors | Install it: uv sync --extra mavproxy. |
| First run: "GitHub API rate limit" | Set GITHUB_TOKEN (any read PAT) or SIMLAUNCHER_BINARY=/path/to/ArduCopter. |
| Drone → "crashed" | Open its log tab; usual cause is a UDP port already in use. |
systemctl restart hangs |
Fixed in current versions (bounded graceful shutdown); the first restart onto the fix may still be slow — be patient once. |
Tunerview (sibling tool)
A live MAVLink tuning viewer (target-vs-actual ground speed, attitude rates, vibration, motor PWM).
Needs the [gui] extra.
uv sync --extra gui
uv run tunerview --udp-host 0.0.0.0 --udp-port 15550
Front it with MAVProxy for a bidirectional link (tunerview bumps message-interval rates):
mavproxy.py --master=udp:127.0.0.1:14550 --out=udp:127.0.0.1:14551 --out=udp:127.0.0.1:15550
Then point QGC/MP at 14551 and tunerview at 15550. Offline replay:
tunerview --tlog-file <path> or tunerview --log-dir <dir> (newest closed .tlog).
Development
uv sync --extra gui --extra mavproxy # everything, so no tests skip
uv run pytest -q # full suite
Tests: per-module units in tests/test_<module>.py; asyncio integration (real subprocesses, mock
binary) in tests/test_launcher_integration.py; web API in tests/test_web_server.py; desktop GUI
in tests/test_gui.py (skips without the gui extra). Mock binaries under tests/fixtures/.
License
MIT (see pyproject.toml).