Your first hover hop
In this tutorial you fly ArduPilot’s stock quadplane through mcardupilot’s tools: take off vertically, climb to 5 m, hover for fifteen seconds, land, and then read the flight back out of its log. It takes about ten minutes, most of it reading.
Every result shown here comes from a real run on the stock quadplane SITL, trimmed to the fields that matter. Your numbers will differ in the last digits.
Before you start
Section titled “Before you start”You need an ArduPilot checkout with the plane SITL built, uv, and an MCP client. The examples use Claude Code.
# in your ArduPilot checkout (mcardupilot looks in ~/ardupilot unless told otherwise)./waf configure --board sitl./waf plane # builds build/sitl/bin/arduplaneIf your tree lives somewhere else, set MCARDUPILOT_ARDUPILOT to its path. Install and connect covers the options.
Fly the hop
Section titled “Fly the hop”-
Add the server to Claude Code.
Terminal window # from PyPIclaude mcp add mcardupilot -- uvx mcardupilot# or from a local checkoutclaude mcp add mcardupilot -- uv run --directory /path/to/mcardupilot mcardupilotStart a new Claude Code session and ask for
server_info. It should reportbinary_exists: trueand the instance pool,40-59by default. -
Open a SITL.
open_sitl(model="quadplane", label="first-hop", idle_speedup=0.05){"session_id": "7ae354","instance": 40,"mavlink_port": 6160,"defaults": [".../Tools/autotest/default_params/quadplane.parm"],"provenance": { "ardupilot_sha": "4c98c92", "ardupilot_patch_sha256": null },"snapshot": {"state": "open", "seq": 1184, "t_sim_s": 10.224, "speedup": 10.0,"armed": false, "mode": "FBWA", "height_m": 0.17, "landed": true,"rc": [1500, 1500, 1000, 1500, 1800, 1500, 1500, 1500]}}The call returned once home was set. You got the first free instance in the pool, and its MAVLink port follows from the number: 5760 + 10 × 40 = 6160. Because
paramswas empty, the server loaded the frame’s own defaults from the tree, the same filesim_vehicle.py -f quadplanewould load. From now on a thread inside the server owns the link and keeps the GCS heartbeat and RC overrides flowing between your calls.Keep the
session_id. Every other session tool takes it. -
Arm in QLOITER.
arm(session_id="7ae354", mode="QLOITER"){"armed": true,"refused": null,"snapshot": { "t_sim_s": 49.673, "mode": "QLOITER", "armed": true, "height_m": 0.0,"texts": [ { "text": "AHRS: EKF3 active" }, { "text": "Throttle armed" } ] }}armdrops the throttle, switches mode and retries every 3 sim seconds while the pre-arm checks settle. Here that took about 40 sim seconds, the time the EKF needs to start using GPS. Heights from now on are measured from where it armed. -
Push the throttle up.
rc_override(session_id="7ae354", channels={3: 2000}){ "rc": [1500, 1500, 2000, 1500, 1800, 1500, 1500, 1500], "enabled": true, "script_channels": [] }Channel 3 is the throttle stick. In QLOITER, stick above centre climbs and stick at centre holds height. The server now sends 2000 on channel 3 with every message it pumps, until something changes it.
Before you go on, think about what happens next. The vehicle is climbing, and it keeps climbing until a later call moves the stick back.
-
Stop the climb at 5 m, inside the server.
wait_until(session_id="7ae354", condition="altitude", value=5, then_rc={3: 1500}){"satisfied": true, "reason": "met", "snapshot_at": "settle","waited_sim_s": 3.3, "waited_wall_s": 0.38,"snapshot": {"seq": 6905, "t_sim_s": 53.022, "speedup": 0.05, "mode": "QLOITER","height_m": 5.0, "climb_mps": 2.80, "throttle_pct": 66.0,"rc": [1500, 1500, 1500, 1500, 1800, 1500, 1500, 1500]}}The stick went back to centre in the same tick the vehicle crossed 5 m, and the snapshot was taken in that tick too, so
height_m: 5.0is a measurement at that moment rather than a look a call later.speedupin the snapshot reads 0.05: that isidle_speedupat work, which the next section explains. -
Hover for 15 sim seconds.
wait_until(session_id="7ae354", condition="sim_time", value=15, relative=True){"satisfied": true, "reason": "met", "waited_sim_s": 15.05, "waited_wall_s": 1.56,"snapshot": { "t_sim_s": 68.071, "mode": "QLOITER", "height_m": 7.3, "climb_mps": -0.006 }}relative=Truecounts from the tick the previous wait settled, so the hover is fifteen seconds of autopilot time from the moment the stick centred, however long you took to send this call. Notice the height: 7.3 m, not 5. The vehicle was climbing at 2.8 m/s when the stick centred, and QLOITER braked it to a stop above the target. The figure below shows it. -
Land.
set_mode(session_id="7ae354", mode="QLAND")wait_until(session_id="7ae354", condition="disarmed", timeout_s=120){ "satisfied": true, "reason": "met", "waited_sim_s": 20.8,"snapshot": { "t_sim_s": 88.922, "mode": "QLAND", "armed": false, "height_m": 0.0, "landed": true } }set_modereturns only once the autopilot’s own heartbeat reports QLAND. The wait ends when ArduPilot disarms itself after detecting the landing. -
Close the session.
close_sitl(session_id="7ae354"){ "session_id": "7ae354", "instance": 40, "kept": [".../logs/20261003T202812Z-first-hop.BIN"], "pruned": [], "state": "closed" }SITL’s whole process group is killed (it ignores SIGTERM), the server waits for its ports to come free, the lease on instance 40 is released, and the dataflash log moves out of the instance’s run directory into the kept-log directory under the label you gave.
What the waits were for
Section titled “What the waits were for”Tool calls are not free in sim time. SITL runs at speedup times real time, 10 by default, and it cannot pause. Everything you do between two calls, reading a result, deciding, writing the next call, is flight time with the last RC still held:
That is why step 5 put the reaction inside the wait. then_rc, then_mode and then_speedup are applied by the server in the tick the condition is met, so your own round trip never separates the condition from the response. And it is why step 6 waited on sim_time instead of counting seconds from whenever the call arrived.
idle_speedup=0.05 is the other half. With it set, the session runs at full speed only while a call is in flight and at a twentieth of real time otherwise, so the moment a wait settles the sim nearly stops and waits for you. For a flight you pace by hand, it makes your thinking time almost irrelevant. For a manoeuvre with many steps, a step program takes the round trips out altogether.
log_fields (POS, RelHomeAlt). The stick centred in the tick the vehicle crossed 5 m, and QLOITER then braked a climb of about 2.9 m/s, so it settled near 7.3 m. Stock quadplane SITL at speedup 10.Read the flight back
Section titled “Read the flight back”The kept log is a few MB of ArduPilot dataflash. Ask for the summary by its name:
log_summary(log="20261003T202812Z-first-hop.BIN"){ "firmware": "ArduPlane V4.8.0-dev (4c98c922)", "duration_s": 78.149, "energy_wh": null, "lift_motor_channels": [5, 6, 7, 8], "phases": [ { "t0_s": 10.804, "t1_s": 49.527, "mode": "QLOITER", "armed": false, "lift_pct": 0.0, "alt_max_m": 0.31 }, { "t0_s": 49.527, "t1_s": 68.098, "mode": "QLOITER", "armed": true, "lift_pct": 72.9, "alt_max_m": 7.49, "roll_abs_max_deg": 0.36, "climb_mps": 0.398 }, { "t0_s": 68.098, "t1_s": 88.729, "mode": "QLAND", "armed": true, "lift_pct": 58.8, "alt_max_m": 7.41, "climb_mps": -0.351 }, { "t0_s": 88.729, "t1_s": 88.953, "mode": "QLAND", "armed": false, "lift_pct": 0.0 } ], "events": [ { "t_s": 49.527, "text": "EV armed" }, { "t_s": 82.601, "text": "SIM Hit ground at 0.502843 m/s" }, { "t_s": 88.729, "text": "Land complete" }, { "t_s": 88.729, "text": "EV disarmed" } ]}The flight splits at every mode change, arm and disarm. The lift motors were found from the log’s own SERVOn_FUNCTION parameters, so lift_pct is the average of channels 5 to 8 above idle. The touchdown at 82.6 s and the land-complete six seconds later are ArduPilot’s landing detector taking its time to be sure.
energy_wh is null because the stock quadplane parameters configure no battery monitor. Open the next flight with set={"BATT_MONITOR": 4} and the summary reports current, voltage and energy per phase.
The first phase, QLOITER and disarmed for 38.7 s, is the arming wait from step 3: arm switched the mode at once and then retried until the pre-arm checks passed.
Where next
Section titled “Where next”You flew the whole hop one call at a time. Fly a manoeuvre as one step program runs the same hop as a single call, Run a gusty A/B test adds wind and compares two flights, and Read a flight back goes further into the log tools.