⟵ Back to projects

Autonomous Drone Delivery Stack

December 202510 min read

Autonomous Drone Delivery Stack cover

A campus delivery drone that flies to a GPS pin with no pilot in the loop. I led software for the project at the Autonomous Robotics Club of Purdue, fall 2024 through fall 2025. I built the pipeline from a customer’s pin to a flyable Nav2 mission.

  • Five-layer stack: mapping, localization, planning, flight control, and ROS 2 keeping them talking.
  • Ground-robot planner, taught to fly: my C++ nodes hand Nav2 a flat ghost of the drone to steer while PX4 flies the real drone.
  • GPS translator: latitude and longitude become map coordinates, verified over 17 km.
  • Recovery playbook: a stuck drone wipes its map, waits, backs up, and tries again, six times before giving up.

The semester went into making lidarslam, Nav2, and PX4 agree on where the drone is.

The Autonomous Robotics Club of Purdue (ARC) was my main software project outside class. We began with a mission statement and an inherited code repo from previous semesters.

Context

The mission statement was a campus-wide package delivery system run by a fleet of autonomous drones. Order on a web app and a drone flies the package to your GPS pin, with no pilot anywhere in the loop. Starship’s sidewalk robots already haul bagels across campus at walking speed. We wanted the version that skips the sidewalk.

We broke that one sentence into semesters:

I led the software side. The team ran as three subteams. LIO (lidar-inertial odometry) kept the drone aware of its surroundings, PX4-ROS2 spoke to the flight controller, and Nav2 turned maps into missions. The Nav2 layer was mine.

The simulated hexacopter hovering over the Gazebo world The testbed hexacopter over Gazebo’s tree line, with the keyboard teleop window showing the movement targets it sends.

The Stack

Five layers, none of them optional:

LayerToolJob
MappinglidarslamTurn laser sweeps into a persistent 3D map of the surroundings
LocalizationPX4 EKF2 + our bridge nodeBlend GPS and motion sensors into one steady answer to “where am I?”
NavigationNav2Plan routes around obstacles and run the mission, decision by decision
Flight controlPX4Flight modes, motor commands, staying level
GlueROS 2, with a bridge to the autopilotOne shared language between the onboard computer and the autopilot

The simulated drone mirrors the real build. PX4’s stock Typhoon H480 hexacopter carries a Velodyne VLP-16-class lidar and a Zed 2i-class depth camera, the same pair the physical drone flies alongside its Pixhawk and Jetson.

Full system architecture diagram
The whole system on one board, from the customer's web app down to the six motor controllers. The Pixhawk flies. The Jetson thinks. My corner is everything between "Web app" and "Pixhawk". Click to open full size.

Inside Nav2, the choices that mattered most were:

My favorite design wrinkle sits below all of that. Nav2 was built for ground robots. It wants a machine sitting flat on a floor, and a drone never sits flat on anything. So Nav2 gets a flat ghost of the drone that always sits level on the ground, steers that, and lets PX4 fly the real drone above it. Three small C++ programs keep the two in step. The first listens to the IMU, the drone’s inner ear, and strips out the tilt:

// stabilized_tf_broadcaster.cpp: shed roll and pitch, keep yaw
tf2::Quaternion q_old(msg->orientation.x, msg->orientation.y,
                      msg->orientation.z, msg->orientation.w);
tf2::Matrix3x3(q_old).getRPY(roll, pitch, yaw);

tf2::Quaternion q_new;
q_new.setRPY(roll, pitch, 0.0);  // this transform absorbs roll and pitch

That transform keeps base_link_stabilized level above the tilting base_link. A second node drops it to the ground as base_footprint, hiding the GPS altitude where Nav2 never has to see it. A third converts PX4’s odometry from aerospace NED axes into ROS ENU. Three names for the same drone, at three heights, in one set of axes.

What I Built

A delivery address arrives as latitude and longitude. Nav2 does not speak GPS. It plans in meters on a costmap that has never heard of the equator. The translating fell to me. The handoff runs top to bottom like this:

A GPS pin arrives from the ordering web app
1. My converter turns latitude and longitude into map coordinates
RViz view of the global costmap where the GPS goal lands
The goal has to land on the global costmap, the planner's entire view of the world. White is free space, black is off-limits.
2. The converted pose goes out as an action request, a long-running request Nav2 reports progress on
The NavigateToPose action definition
Nav2's order form, an action called NavigateToPose: destination in, progress updates out, error codes if anything goes sideways.
3. Nav2 accepts the goal and wakes the behavior tree
The custom Nav2 behavior tree rendered in Groot
Our behavior tree is the drone's decision flowchart. The left half plans and flies, the right half recovers when something breaks. Click to open full size.
Plan, fly, recover, repeat. The drone arrives at the GPS pin.

Under the hood, the converter is three moves. First, turn the latitude and longitude into a position on an Earth-sized grid centered on the planet’s core, the same grid GPS satellites use. Then subtract where the map’s corner sits on that grid, which leaves a plain offset in meters. Finally, hand that offset to the behavior tree as a NavigateToPose goal. A test with two points 17 km apart confirmed the distance came out right. One refinement is still on the list. The raw offset needs rotating flat to the local ground, because that grid tilts relative to wherever you are standing.

The recovery branch is what makes the tree robust. When navigation fails outright, it refuses to sulk:

<RoundRobin name="RecoveryActions">
  <Sequence name="ClearingActions">
    <ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
    <ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
  </Sequence>
  <Wait wait_duration="3.0"/>
  <BackUp backup_dist="0.50" backup_speed="0.05"/>
</RoundRobin>

Wipe the costmaps clean in case outdated obstacles have piled up. Still stuck, wait three seconds. Still stuck, back up half a meter. One move per failed attempt, rotating through the list, six attempts before the tree admits defeat. That’s when a human operator has to step in to save the day.

Why Simulation First

A planner bug in Gazebo costs a laugh and a relaunch. The same bug at altitude costs a drone. On a student-org budget, the whole stack had to prove itself in simulation before touching real props.

Simulation still found ways to fight back. Because PX4, Gazebo, and ROS 2 each keep their own clock, position updates arrived with three mismatched timestamps that nothing could line up. The math was fine. The timing disagreed. Two switches fixed it, use_sim_time on the ROS side and one clock-sync setting on the autopilot.

Proof came in two screenshots. First, the mapping half. We flew a loop while lidarslam stitched the laser sweeps into a 3D map, then flattened it into the floor plan the planner runs on.

RViz view of the lidarslam point cloud map and traced flight path lidarslam at work. The environment accumulates in red, the flight path traced in yellow.

Second, the navigation half reading a world it has never seen. Gazebo’s cone course (left) shows up in the costmap (right) as no-go cells wrapped in a personal-space bubble that gets pricier the closer you fly. Nobody writes an “avoid the cone” rule. Theta* simply shops for the cheapest route through.

Gazebo cone course beside the RViz costmap with inflated obstacles
The global and local costmaps rendered live in RViz (ROS's 3D viewer). Click to open full size.

Results

Status by the spring review:

That last box is the point of the whole architecture. Every subteam’s piece plugs into the same behavior tree, which makes final integration a wiring job.

The Ultimate Takeaway

Autonomy is mostly integration. lidarslam, Nav2, and PX4 each work beautifully alone. The heart of the problem is making them agree on where the drone is, what time it is, and what to do next. My GPS pipeline was one small file of actual math. It made three large systems point at the same spot on the map.