English | 简体中文
Headless, agent-first MAVLink ground-control CLI for ArduPilot vehicles.
Quick start — paste this into any coding agent:
uv tool install mavctl
npx skills add LeaderOnePro/mavctl -y -gConnected to a flight controller and you can tell your Agent what to do in plain language — take off, hover, land, RTL, one instruction at a time.
mavctl is ArduPilot-first and built to be driven by both humans on a terminal and AI coding agents (Claude Code, Codex, OpenClaw, …). A resident daemon keeps the MAVLink link alive and caches vehicle state; every CLI call is one short, structured request to that daemon.
Status: developed and verified against ArduPilot SITL. It has not been proven across the breadth of real MAVLink vehicles and is not presented as ready for production flight on a real aircraft.
GUI ground stations such as Mission Planner or QGroundControl are excellent for a human at the controls — and a poor interface for a shell script or an LLM agent: clickable UIs, no stable exit codes, no machine-readable output.
mavctl takes the other side of that trade:
- the daemon owns the MAVLink connection and continuously caches telemetry, so each command is quick and stateless;
- every command prints human-readable output by default and structured JSON
with
--json; - failures carry explicit exit codes (3 daemon down, 4 link lost, 5 guard rejection, 6 vehicle NACK / timeout) instead of stack traces;
- dangerous operations pass safety guards before anything reaches the vehicle;
- mavctl embeds no LLM — it is designed to be called by agents such as Claude Code, Codex or OpenClaw, or by plain bash.
Implemented commands — this is the complete list:
mavctl daemon start|stop|status
mavctl status
mavctl telemetry
mavctl arm
mavctl disarm
mavctl mode <MODE>
mavctl takeoff --alt <metres>
mavctl land
mavctl rtlCross-cutting behaviour:
| Flag / behaviour | Meaning |
|---|---|
--json |
structured output on stdout; errors as {"error": {...}} on stderr |
--confirm |
required on every state-changing command; without it exit code 5 |
--dry-run |
run the exact same guards, never reach the vehicle |
--wait --timeout <s> |
block until the target state is reached (default 60 s) |
| idempotent repeats | re-applying an achieved change succeeds ("already armed") |
| transaction safety | ACK/NACK handling, serialized commands, link-loss abort |
Not implemented — current scope only, not a roadmap promise:
Mission upload/download/start
Parameters
Geofence
Log download / analysis
Firmware flashing
Multi-vehicle orchestration
Notable in 0.2.1:
mavctl --versionprints the installed version — no daemon or vehicle needed.status --jsoncarries per-stream freshness ages:telemetry_age_s,gps_age_s,battery_age_s,home_position_age_s,landed_state_age_s(monotonic-clock based; they keep counting after heartbeat loss so cache staleness stays visible).- Ordinary non-force
disarmrequires fresh positive ground evidence; stale apparent ground evidence is rejected asground_state_stale(exit 5).
Requires Python >= 3.10 and uv. Always bring up SITL first; do not point an agent-driven workflow at a real vehicle.
Terminal 1 — start ArduPilot SITL:
sim_vehicle.py -v ArduCopter --out udp:127.0.0.1:14550Terminal 2 — install from source and connect:
uv sync
uv run mavctl daemon start --connect udp:127.0.0.1:14550
uv run mavctl status --jsonSafe takeoff to 10 m and return to launch:
uv run mavctl mode GUIDED --confirm --wait
uv run mavctl arm --confirm
# Poll status --json until armed=true (the arm ACK can beat the heartbeat)
uv run mavctl takeoff --alt 10 --confirm --wait --timeout 45
uv run mavctl rtl --confirm --wait --timeout 120
uv run mavctl daemon stopSafety notes — read before pointing mavctl at anything that flies:
- Validate every workflow in SITL first; treat real-aircraft use as its own review process.
- After
arm, pollstatus --jsonuntilarmed=truebefore takeoff: the COMMAND_ACK can arrive about one heartbeat before reported state catches up. - End flights with
rtl/land, notdisarm. Ordinarydisarmrequires provable ground contact (ground_state_unknownotherwise). disarm --forceis an emergency motor stop only — in flight it can cause a crash.- There is no
arm --forceanywhere in mavctl; pre-arm checks cannot be bypassed.
mavctl 0.2.0 is published on production PyPI. Install with:
uv tool install mavctl
# or run once without a persistent install:
uvx mavctl --help
# or:
pipx install mavctlVerify the install — mavctl --version needs no daemon or vehicle:
mavctl --versionBefore pointing mavctl at anything that flies, walk through the SITL-first quickstart above. Release history and the publishing runbook live in docs/PUBLISHING.md.
For development from source, use uv sync and uv run mavctl …:
git clone https://github.com/LeaderOnePro/mavctl.git
cd mavctl
uv sync
uv run mavctl --helpmavctl bundles a portable Agent Skill under skills/mavctl-flight/
(entrypoint plus workflows / safety / troubleshooting references). It is a
source asset of this repo: installing the Skill is independent of installing
the mavctl CLI from PyPI, e.g. uv tool install mavctl.
Install the bundled Agent Skill globally:
npx skills add LeaderOnePro/mavctl -y -gThe community skills CLI manages installation for the agent runtimes it supports, according to its current environment and configuration. See docs/SKILLS_CLI_ACCEPTANCE.md for tested behaviour and compatibility notes.
Short version; full details in skills/mavctl-flight/references/safety.md:
- the daemon owns the vehicle link; CLI calls are short transactions;
- state-changing commands require
--confirm;--dry-runpreviews decisions; - exit 4 = no live vehicle state (link lost / heartbeat expired), including
mid-
--waitloss (link_lost_during_wait); - exit 5 = guard rejection with structured
reason+hint; - exit 6 = vehicle NACK / ACK timeout / wait timeout;
- force-arm does not exist at any layer (CLI option, RPC field, adapter verb);
- ordinary
disarmneeds positive ground evidence, elseground_state_unknown(exit 5); - after link loss,
statusreflects stale cache:armedrenders unknown/n/a, never silently disarmed.
uv sync
uv run ruff check .
uv run mypy .
uv run pytest -m "not sitl"
uv run pytest -m sitl # requires a running ArduPilot SITLFurther reading:
- docs/SITL_ACCEPTANCE.md
- docs/SITL_ACCEPTANCE_PHASE2.md
- AGENTS.md — architecture rules and contribution constraints
- skills/mavctl-flight/SKILL.md — agent-facing flight guidance
MIT — Copyright (c) 2026 LeaderOnePro.