In my previous post, I assembled a local drone simulation using ArduPilot SITL and Gazebo Harmonic. Getting the simulated quadcopter into the air was satisfying, but a single successful takeoff does not tell us very much about the quality of an autonomous flight program.
The next step was to turn the simulator into a test bench. A useful test should be repeatable, observable, and able to fail without damaging hardware. That means starting from a known location, sending the same mission commands, recording telemetry, and checking the result against explicit expectations.
A manual flight in MAVProxy is useful for confirming that the installation works. It is a poor regression test, however. The result depends on where the mouse is, which mode is active, and whether the previous simulation left a vehicle armed.
I split the process into four stages:
The mission is intentionally boring. A rectangle at 10 metres above ground is enough to exercise arming, takeoff, waypoint navigation, position reporting, and landing. More complicated paths can be added after this basic scenario is reliable.
pymavlink exposes the same MAVLink messages that a companion computer would receive from a real flight controller. The following fragment waits for position updates and prints a compact progress line:
import time
from pymavlink import mavutil
master = mavutil.mavlink_connection("udpin:127.0.0.1:14550")
master.wait_heartbeat(timeout=30)
started = time.monotonic()
timeout_seconds = 120
while time.monotonic() - started < timeout_seconds:
message = master.recv_match(
type=["GLOBAL_POSITION_INT", "HEARTBEAT", "STATUSTEXT"],
blocking=True,
timeout=2,
)
if message is None:
print("No telemetry received")
continue
if message.get_type() == "GLOBAL_POSITION_INT":
altitude = message.relative_alt / 1000.0
latitude = message.lat / 10_000_000.0
longitude = message.lon / 10_000_000.0
print(f"alt={altitude:5.1f}m lat={latitude:.6f} lon={longitude:.6f}")
elif message.get_type() == "STATUSTEXT":
print(message.text)
A production script should also inspect the message severity, record timestamps, and stop waiting when the vehicle reports a critical error. The important part is the shape of the loop: commands are not considered successful merely because they were sent. The vehicle’s reported state is the source of truth.
MAVLink missions are uploaded as a sequence of mission items. Each item describes a command, frame, latitude, longitude, altitude, and any command-specific parameters. For a first test, I used a takeoff command, several MAV_CMD_NAV_WAYPOINT items, and a final landing command.
The coordinates should be generated around the simulated home position rather than copied from a real location. This keeps the test deterministic and prevents an accidental connection to a real vehicle from turning a development command into a flight command.
A useful mission runner performs these checks before upload:
After uploading, the runner waits for mission acknowledgements and watches MISSION_CURRENT and position messages. It should distinguish between the vehicle reaching the final waypoint and the process simply timing out.
The first versions of my script mostly printed whatever arrived on the MAVLink connection. That made it difficult to tell whether a test had passed. I changed the output into a small event log:
2026-07-18T14:03:11Z CONNECTED system=1 component=1
2026-07-18T14:03:14Z MODE mode=GUIDED
2026-07-18T14:03:19Z ARMED
2026-07-18T14:03:32Z ALTITUDE value=10.1
2026-07-18T14:04:08Z WAYPOINT index=2
2026-07-18T14:04:51Z RESULT status=PASS
This is a small change, but it makes comparison between runs much easier. It also exposes a category of problems that are easy to miss in a graphical simulator: delayed heartbeats, unexpected mode changes, stale position data, or a vehicle that has stopped progressing through the mission.
For longer experiments, I would write the events and raw telemetry to a timestamped file and retain the simulator configuration alongside it. A test result without the exact parameters used to produce it is difficult to reproduce.
A passing SITL mission is evidence that the software behaved correctly in the simulated conditions. It is not evidence that the same mission is safe in the real world.
The simulation does not automatically capture every motor, battery, radio, compass, weather, or airframe problem. Its sensor models can also be more predictable than reality. Before any physical test, the mission needs a separate review of the aircraft, firmware parameters, operating area, regulatory requirements, and human supervision plan.
Simulation is still extremely valuable. It lets me exercise the control flow repeatedly, test the unhappy paths, and make mistakes while the vehicle is made of pixels. That is exactly the right place to discover that a timeout handler is missing or that a mode transition was assumed rather than verified.
The useful unit of progress is no longer “the drone took off.” It is “the same mission can be run repeatedly, its state can be observed, and failure produces a controlled outcome.” That gives the autonomy work a foundation that can be measured instead of guessed.
Next I want to move the decision-making out of the mission script and into a small companion-computer controller. The controller will have to decide what to do from telemetry, while still allowing the autopilot to enforce the fundamental flight and safety constraints.