Order a parcel in a web page → a drone flies it across a park, lowers it onto a landing pad, camera-lands itself back home, and you get a "delivered" email — all tracked live on a map.
🎥 Demo video: https://youtu.be/zj5VMQE8P9Q · Built by SSU NC-Lab
Imagine a courier company, but the couriers are autonomous drones and the depots are small landing stations scattered around a site. AirPost is the whole system that makes that work:
- a website where you (or an operator) register a parcel and pick where it goes,
- a brain (backend servers) that chooses which drone flies, plans the route, and tracks everything,
- the drones and stations themselves — each is a little internet-connected device (an IoT device) that reports its GPS, temperature, humidity and light, and lights up a lamp at night so the drone can still see the landing marker,
- and a physics simulator so you can watch the entire delivery happen on your laptop — no real drone required.
A single delivery looks like this:
Register parcel in the UI → backend assigns the nearest free drone and a route → drone takes off, flies to the pickup, lowers the parcel by a winch onto a red drop-pad → flies to the destination station → uses its downward camera to land exactly on a marker → you get a "delivered" email and watched the whole flight live on a map.
Note
New to drones / robotics? Jump to the Glossary at the bottom — every acronym (PX4, SITL, MAVSDK, MQTT, Kafka, AprilTag, EKF…) is explained in one line.
A real delivery network has several very different jobs, and each needs different technology — that's why the project is split into the parts below. The split is deliberate, not accidental complexity:
| The job | What it really requires | Which part does it |
|---|---|---|
| Let people order & operators manage | a friendly web app | AirPost_UI (React) |
| Decide, route, remember, notify | reliable servers + a database + rules | AirPost_Backend (Go services + MySQL) |
| Move parcels through the air, safely | flight control, vision, collision-avoidance | the drone autopilot + flight service (PX4 + simulation/) |
| Sense the world & stay online | tiny always-on devices with sensors | AirPost_Drone / AirPost_Station / AirPost_Sink (IoT) |
| Stream, store & visualise telemetry | a high-throughput data pipe | Kafka → Elasticsearch → Kibana |
| Prove it works without risking hardware | a physics world | simulation/ (Gazebo) |
If all of this lived in one program it would be impossible to test, scale, or run partly on a drone and partly in the cloud. Splitting it lets each piece be developed, deployed and verified on its own.
flowchart LR
subgraph People
UI["AirPost_UI<br/>React web app"]
end
subgraph Brain["Backend (Go)"]
APP["application :8081<br/>REST API + routing + dispatch"]
LC["logic-core :8084<br/>rules + email + telemetry sink"]
HC["health-check :8083/8085<br/>live tracking (WebSocket)"]
DB[("MySQL<br/>orders, nodes, routes")]
end
subgraph Edge["Drones & Stations (IoT)"]
DRO["Drone<br/>autopilot + camera + winch"]
STA["Station<br/>sensors + pad lamp"]
SINK["Sink<br/>MQTT to Kafka bridge"]
end
subgraph Data["Telemetry pipeline"]
KAFKA[("Kafka")]
ES[("Elasticsearch")]
KB["Kibana :5601"]
end
MQTT(["MQTT broker<br/>mosquitto :1883"])
UI -->|"register parcel (REST)"| APP
UI -->|"live map (WebSocket)"| HC
APP --> DB
APP -->|"flight order"| MQTT
MQTT --> DRO
DRO -->|"GPS / status"| HC
DRO -->|"delivered"| LC
STA -->|"GPS, temp, humidity, light"| SINK
DRO -->|"telemetry"| SINK
SINK --> KAFKA --> LC --> ES --> KB
LC -->|"'delivered' email"| MAIL["MailHog :8025"]
Read it as three flows:
- The order flow (left → flight): UI →
application→ MQTT → drone. One order = one sortie. - The tracking flow (flight → UI): the drone streams its position to
health-check, which pushes it over a WebSocket to the live map. - The data flow (everything → dashboards): every device streams sensor readings through the
Sink into Kafka;
logic-coreconsumes them, runs rules (e.g. "on delivered, send an email"), and stores everything in Elasticsearch for Kibana dashboards.
This is exactly what the autopilot does on every sortie (and what you see in the simulator):
1. TAKEOFF lift off vertically from the home station helipad
2. CRUISE fly to the parcel's drop site at an assigned altitude band
(each drone gets its own band so two drones never share an altitude → no collision)
3. DESCEND come straight down to ~10 m above the red drop-pad
4. WINCH lower the parcel on a cable and set it down ON the red box, then release
5. CLIMB & CRUISE rise back to the band, fly to the destination landing station
6. APPROACH arrive over the station; if another drone is landing there, HOLD to the side
7. PRECISION LAND from a few metres up, the downward camera finds the AprilTag marker and the
drone steers itself onto the tag centre — not just "near the pad", on the mark
8. DELIVERED publish status → "delivered" email fires, tracking map updates
Two safety ideas run through the whole fleet:
- No two drones ever collide. Cruise altitudes are separated into bands; when two drones need the same drop pad or landing pad, one waits to the side until the other clears.
- Vision, not just GPS, for the final metre. GPS / the drone's own position estimate can be off by tens of centimetres. The camera + AprilTag closes that gap so the parcel lands on the pad and the drone lands on the marker. (See the honest accuracy notes in §8.)
This umbrella repo pulls every part together as submodules. Clone with --recurse-submodules.
| Path | What it is | Stack |
|---|---|---|
simulation/ |
The physics simulation — flies the whole mission (takeoff → winch → camera precision-land) for 1–N drones, no hardware needed. Start here to see it work. | PX4 SITL, Gazebo Harmonic, Python, MAVSDK |
AirPost_Backend/ |
The brain. Three Go services: application (REST API, routing, drone dispatch, MySQL), logic-core (Kafka consumer, rule engine, "delivered" email, Elasticsearch sink), health-check (WebSocket live-tracking). |
Go, Gin, GORM, MySQL |
AirPost_UI/ |
The web app. Operator dashboard (manage drones/stations/tags, see health), parcel registration, and the live tracking map. | React, Vite, TypeScript |
AirPost_Drone/ |
On-drone control (ROS 2). The airpost_drone ROS 2 node that flies PX4 v1.17 over the native uXRCE-DDS bridge (telemetry, OFFBOARD delivery, winch) + a realsense-swappable camera node. (noetic branch keeps the legacy ROS 1/MAVROS controller.) |
Jetson, ROS 2 Humble, px4_msgs, uXRCE-DDS |
AirPost_Station/ |
Station IoT. A landing station's sensors and its pad lamp (turns on in the dark so the camera can still read the tag). | Python, Raspberry Pi |
AirPost_Sink/ |
The data bridge. Forwards device telemetry from MQTT into Kafka. | Go/Python |
docker-elasticsearch-kibana/ |
Standalone Elasticsearch + Kibana compose (telemetry storage & dashboards). | Docker |
Ports at a glance: application 8081, logic-core 8084, health-check 8083 (+WS 8085), UI 4173, MySQL 3306, Kafka 9092, Elasticsearch 9200, Kibana 5601, MailHog SMTP 1025 / web 8025, MQTT 1883.
git clone --recurse-submodules https://github.com/jsoone24/NC_AirPost.git
cd NC_AirPost/AirPost_Backend
docker compose up --build -d
docker compose ps # wait until application / logic-core / health-check / ui-next are healthy| Open | What you get |
|---|---|
| http://localhost:4173 | the web app (login admin@airpost.local / admin) |
| http://localhost:8081/swagger/index.html | the backend REST API (Swagger) |
| http://localhost:5601 | Kibana telemetry dashboards |
| http://localhost:8025 | MailHog — every "delivered" email lands here |
The simulation needs a real GPU/GL context, so it runs natively (not in Docker). One command builds the world, spawns the drones, starts the per-drone camera detectors and the winch manager, and waits for orders from the same MQTT broker the backend uses:
cd ../simulation
SERVICE=1 ./run_airpost_fleet.sh 4 # 4 drones, Gazebo window opens (~90 s to boot)Then register a parcel in the UI (or via the API) and watch in the Gazebo window: drones take off,
carry the parcel, winch it onto the red pad, and camera-land on the tag — while the UI map tracks
them live and a "delivered" email shows up in MailHog. Full step-by-step: docs/RUNBOOK.md.
The deep simulator guide (how multi-drone, collision-avoidance, the winch, and AprilTag precision landing actually work, plus the ground-truth verifier) is in
simulation/README.md.
run_airpost_fleet.sh flies the drones from a host-side MAVSDK service — great for the
precision-landing demo. To instead exercise the actual code that ships on the aircraft — the
airpost_drone ROS 2 node talking to PX4 over the native uXRCE-DDS bridge, exactly as on the
Jetson+Pixhawk — use:
cd ../simulation
./run_ros2_fleet.sh 1 # Micro-XRCE-DDS Agent + PX4/gz + drone_node + dummy_camera + GCS heartbeatThis brings up the real ROS 2 graph; an MQTT delivery order then makes the drone arm, take off, fly to
the drop, winch, and land — while it streams PX4's own telemetry on data/DRO51. See
AirPost_Drone for the architecture and the realsense-camera swap.
Tear down: docker compose -f AirPost_Backend/docker-compose.yml down (add -v to wipe data).
AirPost is designed so the exact same backend and UI drive either a real drone or a simulated one — only the bottom layer changes. That's why the simulator is genuinely useful: a green sim run means the whole stack above the autopilot is correct.
+------------ UI + Backend + Kafka/ES (identical) ------------+
| |
REAL ----->| MQTT order -> AirPost_Drone ROS2 node --uXRCE-DDS--> PX4 ->| real flight
SIM ----->| MQTT order -> the SAME ROS2 node --uXRCE-DDS--> PX4+Gazebo>| simulated flight
| (run_ros2_fleet.sh) |
+-------------------------------------------------------------+
The on-drone software (AirPost_Drone, branch main) is ROS 2 Humble talking to PX4 v1.17
over uXRCE-DDS — and the simulator runs that same node, so a green sim run means the real flight code
is correct, not just a stand-in. (simulation/fleet_service.py is a separate host-side MAVSDK driver
kept for the multi-drone precision-landing demo.)
The end-to-end loop is verified in simulation, measured against the simulator's ground truth (its exact physics positions), not the drone's own estimate:
- ✅ UI/API order → nearest free drone assigned → mission flown → live tracking + "delivered" email.
- ✅ Real on-drone ROS 2 stack flies over uXRCE-DDS: the actual
airpost_droneROS 2 node drives PX4 v1.17 over the native DDS bridge (arm → takeoff → waypoint → winch-drop → land) from an MQTT order, streaming PX4's own telemetry todata/<id>→ Sink → Kafka — the same code that runs on the Jetson+Pixhawk, verified end-to-end in SITL (simulation/run_ros2_fleet.sh). - ✅ Parcel placement: rests on the red drop-pad (centre, ~0–5 cm) every run, including 4 drones at once.
- ✅ Multi-drone, no collisions: altitude bands + hold-to-the-side; verified with 4 concurrent sorties.
- ✅ Camera precision landing works: a 4-drone batch landed all four on the tag centre within 1–2 cm (ground truth).
⚠️ Consistency caveat (honest): precision landing is real and usually centimetre-accurate, but there is still run-to-run variance — an occasional sortie has landed ~0.7 m off-centre (on the pad, near the edge). Tightening this to always centimetre-level is ongoing. The drone's own report can read "0.03 m" while ground truth is larger, which is exactly why accuracy is judged by ground truth (seesimulation/tests/verify_truth.py).
| Choice | Why |
|---|---|
| PX4 v1.17 + Gazebo Harmonic | industry-standard open autopilot + physics; the same PX4 the real Pixhawk runs, so sim ≈ reality |
| ROS 2 Humble + uXRCE-DDS (px4_msgs) | PX4 v1.17's native ROS 2 interface — the on-drone code subscribes the autopilot's own topics and commands OFFBOARD over DDS; identical on SITL and the Jetson+Pixhawk (no MAVROS shim) |
| MAVSDK | clean async API for the host-side multi-drone precision-landing demo driver |
| Go (Gin + GORM) | small, fast, statically-typed services that are easy to containerise |
| MQTT (mosquitto) | lightweight pub/sub — the natural fit for "send one flight order to a drone" |
| Kafka → Elasticsearch → Kibana | durable high-throughput telemetry stream + searchable storage + dashboards |
| AprilTag / ArUco vision | passive, cheap, robust fiducial markers for the final precision-landing metre |
| MySQL | relational store for orders, nodes (drones/stations/tags), routes |
| Docker Compose | one command brings the whole back end up reproducibly |
| Read this | For |
|---|---|
| this file | the whole-system overview (you are here) |
simulation/README.md |
how the flight, multi-drone, winch and precision landing work; how to run/verify the sim |
docs/RUNBOOK.md |
step-by-step operations: bring it up, fly an order, tear down, troubleshoot |
SECURITY.md |
security posture, the dev credentials, what to harden for production |
each submodule's README.md |
that component's internals (backend services, UI, drone/station IoT) |
| Term | Plain meaning |
|---|---|
| Autopilot / PX4 | the flight-control software on a drone; it stabilises and flies it given high-level commands |
| SITL | Software In The Loop — running the real autopilot software on a PC instead of on a flight board |
| Gazebo | a 3D physics simulator; here it provides the world, the drone's body, camera and sensors |
| MAVLink / MAVSDK | the messaging protocol drones speak / a friendly library to talk it |
| MQTT | a lightweight publish/subscribe messaging system; the backend "publishes" a flight order, the drone "subscribes" |
| Kafka | a high-throughput, durable event stream; carries the firehose of sensor telemetry |
| Elasticsearch / Kibana | a search database for telemetry / a dashboard tool to chart it |
| IoT device | a small internet-connected gadget with sensors; here, each drone and station |
| AprilTag / ArUco | a printed black-and-white square marker a camera can detect and measure precisely |
| Precision landing | using the camera + marker (not just GPS) to land exactly on a target |
| EKF | the drone's internal estimate of where it is (fused from GPS + sensors); can drift a little |
| Ground truth | the simulator's exact, true position of an object — used to honestly grade accuracy |
| Winch | the motorised cable the drone uses to lower the parcel without landing |
| Helipad / drop-pad | the marked landing surface (helipad, with an AprilTag) / the red box parcels are set on |

