← Work

Skymule 2026

Mullet: ground control station for drone swarms

Operators plan, launch and monitor drone missions from one browser tab: a live 3D map of the fleet, a mission builder, and a drone that refuses anything unsafe with a reason.

Problem

Off-the-shelf ground stations like QGroundControl are built for one drone at a time. Running coordinated missions with several drones in Offboard mode needed a tool built for swarms.

Approach

  • Browser → FastAPI gateway → ROS 2 → mullet_core → PX4. Each drone runs its own supervisor (mullet_core) that owns arming, Offboard, takeoff, land and hold.
  • Drones announce themselves with a 10 Hz heartbeat snapshot; the gateway discovers the fleet from it, so nothing is configured per drone.
  • A mission framework on the drone (the ergodic MPPI planner, for now) receives the mission, checks it and streams setpoints; the ground station only draws and supervises.

What I built

  • Backend in FastAPI that bridges the operator to the fleet: telemetry over WebSocket, commands and mission upload through ROS 2 actions and services.
  • React/TypeScript frontend with a live 3D map of every drone: heading arrow, id, a 2-minute trail, and per-drone control (takeoff, start mission, hold, return home, land).
  • Mission builder on the map: draw the area, obstacles and target density in 2D or 3D. Real buildings become obstacles at their mapped height.
  • Offline field mode: download the basemap and terrain for a site ahead of time, then build and fly without connectivity.
  • Tested against a PX4 SITL fleet through the same gateway a real drone uses, with no mock mode.

Safety

  • The browser never talks to PX4. It only calls semantic endpoints, and the gateway refuses Start mission until the drone’s planner reports READY.
  • Every mission is preflighted on the drone before it can start: spec validation, a timing check against a 16 ms control deadline, and a 20 s dry run that must stay inside the area and the safety margins. A refusal comes back to the operator with its reason.
  • The area polygon is the geofence, obstacles are inflated by the drone’s stopping distance, and crossing the ceiling holds the drone.
  • A stale command stream is ignored and the drone holds position; after 2 s it drops to HOLD. Velocity and acceleration are clamped on the drone, not in the UI.
  • The pilot always wins: switching out of Offboard on the RC, or the kill switch, is honoured and the supervisor never fights it. Land works from any state.

Results

  • Used in field operations at Skymule.
  • Drone state at 10 Hz per drone: FSM state, PX4 mode, position, and a capability flag per command, so the UI only offers what the drone will accept.
  • Missions flown from the builder track the plan within 0.18 m (p95) in simulation-in-hardware at 1.5 m/s.
End to end: browser → FastAPI gateway → ROS 2 → mullet_core on each drone → PX4.
End to end: browser → FastAPI gateway → ROS 2 → mullet_core on each drone → PX4.

Stack

Backend
FastAPI, rclpy action and service clients, WebSocket
Frontend
React, TypeScript, Vite, MapLibre GL (3D terrain and buildings)
Vehicle side
ROS 2 Jazzy, C++ supervisor, PX4 1.17 Offboard over uXRCE-DDS
Maps
OpenFreeMap vector tiles and AWS terrain tiles, cached by the gateway
Dev loop
Docker, PX4 SITL fleet, pytest

Code is proprietary; happy to walk through the architecture in an interview.

mmantovani.work@pm.me