aerpawlib
Official vehicle-control library for AERPAW, NC State's NSF testbed for 5G/6G and autonomous aircraft.
Built
I maintain AERPAW/aerpawlib — the Python API experimenters use to fly LAM6/SAM4 UAVs and the rover. I moved it from DroneKit onto MAVSDK, kept the v1 Runner/StateMachine surface, and added a v2 async API. Current release is 1.4.11.
Hard parts
- Testbed MAVLink filter only allows takeoff, land, goto, and yaw (20–100 m takeoff, land within 5 m of home).
SET_MODEis blocked; GUIDED has to be held withMAV_CMD_DO_SET_MODE. LOITER drops the E-VM link.- Safety checker belongs on the C-VM, not localhost or the OEO console.
- Timed
set_velocitywas killing newer commands; bearings ignored latitude (~51° vs 45° at 35.7°N). - Multi-vehicle ZMQ dropped transitions until JSON + ACK +
wait_for_peers.
Learned
- The filter defines the API as much as the SDK does.
- SITL is necessary and not sufficient.
- A compat layer plus a second API beats a flag-day rewrite.
Also
- Primary maintainer of the official AERPAW.org library (v1 + v2 on MAVSDK 3.17)
- Safety checker with KML geofences, fail-closed command validation, success-only RTL
- ZMQ XSUB/XPUB proxy for multi-vehicle scripts (ports 5570/5571, JSON control plane)
- ArduPilot SITL + MAVSDK integration tests; CI on Python 3.10–3.14
- Docs at aerpaw.github.io/aerpawlib
The platform
AERPAW — Aerial Experimentation and Research Platform for Advanced Wireless — is NC State's NSF PAWR testbed. The public line is that it is the first platform built to study 5G/6G together with autonomous aircraft. The vehicles experimenters actually fly are the LAM6 and SAM4 multicopters and a rover, all on Cube Orange running ArduPilot, speaking MAVLink v2. Portable radio nodes (SDRs on the large vehicles, lighter radios on SAM4) are the payload. Experiment VMs start through startexperiment.sh → startVehicle.sh.
A MAVLink filter sits between experimenter code and the autopilot. Only takeoff, land, simple goto, and yaw go through. Takeoff is 20–100 m. Landing has to be within 5 m of the takeoff point. Gotos are checked against the field geofence; the rover also has no-go zones for creeks. Status messages flow the other way unrestricted. That filter is the real API.
What I maintain
The original aerpawlib was John Kesler's DroneKit / pymavlink library: experimenters wrote Runner and StateMachine scripts, asyncio in the background, QGroundControl .plan files for waypoints. I am a listed co-author and the primary maintainer of the official AERPAW/aerpawlib tree. Current release is 1.4.11.
I migrated the library onto MAVSDK so vehicle firmware can move without monkey-patching Python. v1 keeps the old surface — DroneKit-shaped telemetry wrappers, a dual asyncio/gRPC loop so existing sync-looking scripts still run. v2 is the API I wanted: VehicleTask, can_takeoff / can_goto / can_land, one event loop. I also own the SITL harness, the CLI, the docs, and the 1.4.x series that made this survive the live E-VM / C-VM filter.
What the filter taught the API
SET_MODE is not on the allow-list. MAV_CMD_DO_SET_MODE is. action.hold() / LOITER severs the E-VM link, so GUIDED has to be held with raw MAVLink. The safety checker has to talk to this node's C-VM, not localhost and not the OEO console — that bug shipped and got reverted in 1.4.7–1.4.8. Auto RTL runs only after a successful mission; abort used to fly home at home.alt ≈ 0 and get rejected by the 20 m minimum.
Smaller, uglier ones: timed set_velocity loops cancelled the next command; geodesic bearings ignored latitude (about 51° vs 45° at Lake Wheeler); ExternalProcess dropped stdin/stdout files. Multi-vehicle ZMQ started as fire-and-forget PUB/SUB. It drops transitions. The control plane is now JSON with ACK, HELLO, and wait_for_peers on ports 5570/5571.
How I know it works
Unit tests run on Python 3.10–3.14 in CI. Integration tests start ArduCopter and Rover SITL plus mavsdk_server and reset the vehicles between cases. MAVProxy will block if stdout is a pipe — the suite has to run with capture off. Docs live at aerpaw.github.io/aerpawlib. The useful lesson is boring: SITL is necessary and not sufficient. The filter wins arguments.