SkyCore is the software that runs on the vehicle. It lives in the skyhub_core repository and turns an NVIDIA Jetson plus an ArduPilot/Pixhawk flight controller and a SIYI gimbal camera into an internet-connected, AI-capable drone or rover. Everything is packaged as a set of docker-compose services, all running with network_mode: host and restart: always, glued together by three interconnect fabrics: MAVLink (routed by a single mavp2p hub), ROS 2 DDS, and a Redis message bus. From the cloud’s perspective, the drone is reached at exactly one control endpoint — rosbridge on port 9090 — which the Gateway Service connects to over a per-drone WireGuard VPN. This page is the map of what runs behind that port. Deep-dives on each area live in the sibling pages linked throughout.
Two CLIs with similar names do different jobs:
  • skycore (installer/sc.sh) is the vehicle-side install and lifecycle tool — skycore install, activate, up, status, version, update.
  • skycore_cli.py at the repo root is not the OS entrypoint or service orchestrator. It is a host-run ArduPilot maintenance CLI (nav source, params, log/terrain download, camera SD).
The real “boot” is docker-compose profiles plus supervisord inside the skyhub container. See the SkyCore CLI Reference.

The three services

The production topology is defined in docker-compose.yml. Each service is gated by a Compose profile, and COMPOSE_PROFILES selects which ones start on boot.
Service (compose name)ProfileWhat it doesDetail page
skyhubskyhubOne image, both halves, selected by SKYHUB_ROLE (vehicle | core | agent). The ROS half: MAVROS + rosbridge (:9090) + drone_node, which loads the video_stream, aruco_landing, and battery modules. The agent half: a WebSocket (:5001 /gamepad) for manual control, safe GUIDED velocity, gimbal/zoom, and arm/disarm asset upload (HLS video, photos, logs) to S3.Services · Module System · Safe Control
mavproxymavproxyThe mavp2p MAVLink router. Bridges the FC serial link to a fixed UDP port map for every other consumer.MAVLink Routing
rtk-ntriprtkNTRIP client that injects RTCM corrections into the FC for RTK-fixed GPS and sends GGA back to the caster.RTK NTRIP
The default in .env.example is COMPOSE_PROFILES=skyhub,mavproxy,rtk; skycore activate defaults to skyhub,mavproxy.

System topology

The three interconnect fabrics

Every service talks to the others over one of exactly three buses. Understanding which one carries what is the fastest way to trace a bug.

MAVLink via mavp2p

A single router owns the FC serial port and fans MAVLink out over UDP. The port map is load-bearing.

ROS 2 DDS

Shared ROS_DOMAIN_ID over host networking. Carries /mavros/* topics and the video-room string topics.

Redis pub/sub

Per-drone {ip}:channel command and status bus, keyed by the drone’s WireGuard IP.
mavproxy runs the mavp2p router. It is the single owner of the flight-controller serial link and fans MAVLink out over a fixed UDP port map that every other consumer must agree on (docker-compose.yml:184):
docker-compose.yml (mavproxy command)
serial:/dev/ttyACM0:115200  udps:0.0.0.0:14900  udpc:127.0.0.1:14550 \
  udps:0.0.0.0:14777  udps:0.0.0.0:14560  udps:0.0.0.0:14561
UDP portConsumer
14550skyhub MAVROS (FCUURL=udp://127.0.0.1:14550@)
14777skyhub agent MAVLink handler (MAVLINK=udpout:127.0.0.1:14777)
14560rtk-ntrip RTCM injection
14561skyhub ArUco-landing direct velocity control
14900External GCS (e.g. QGroundControl over the VPN)
This port map is hardcoded in the mavproxy command line and mirrored in each consumer’s env vars. Change one side without the other and the MAVLink link silently breaks. See MAVLink Routing.

2. ROS 2 DDS

MAVROS publishes the /mavros/* topic tree and drone_node consumes /mavros/state — over DDS on a shared ROS_DOMAIN_ID (default 1, per-vehicle configurable) across the host network. drone_node (docker/core/main.py:67) also carries the Janus video-room config as std_msgs/String topics (video_room_details, video_room_state, video_room_source), which the Gateway publishes over rosbridge.

3. Redis message bus

The manual-control and status plane is Redis pub/sub. Channels are namespaced by the drone’s WireGuard wg0 IP{ip}:gamepad_input, {ip}:output, {ip}:chat, {ip}:aruco_tracking, and more — so one Redis can serve many drones without cross-talk. Two channels are deliberately global (unprefixed): video_stream_state and video_stream_status_request. The channel catalog and dual command path (Redis relay vs. direct WebSocket) are documented in the Message Bus page.
If the wg0 interface can’t be resolved (e.g. local dev with no VPN), you must set IP_OVERRIDE or the agent half raises RuntimeError at boot. The legacy misspelling IP_OVRIDE is still honored for backwards compatibility.

How the cloud attaches

The Gateway never speaks MAVLink or Redis directly to a drone in the command path — it attaches at rosbridge :9090 and pub/subs ROS topics and calls ROS services. From there:
  • Commands (arm, takeoff, set-mode, missions) become rosbridge call_service/publish frames → MAVROS → mavp2p → the FC. See the Drone Control Service.
  • Telemetry flows the reverse way: MAVROS topics → rosbridge → Gateway → Socket.IO rooms → the Dashboard. Note set_stream_rates.sh must raise the FC stream rate to 10 Hz first, or sensor topics stay empty.
  • Manual gamepad control takes a separate low-latency path: Dashboard → WS Proxy → Redis {ip}:gamepad_input → the drone’s agent half on GAMEPAD_WS_PORT (default 5001), not through rosbridge. See Vehicle Commands.
  • Video is fully out-of-band: the on-drone GStreamer pipeline whipsinks H264 to the WHIP server, which registers a Janus VideoRoom publisher the Dashboard subscribes to over WebRTC. See Video Streaming.

Service boot and dependencies

docker-compose up reads COMPOSE_PROFILES and starts the selected services. All are network_mode: host and restart: always. Startup order is delay-based, not health-gated (except in the SITL overlay): each service sleeps <SERVICE>_STARTDELAY seconds before starting (DRONE_STARTDELAY=5, RTK_STARTDELAY=10, etc.), reinforced by depends_onskyhub and rtk-ntrip both depend on mavproxy, and nothing else has an edge. Inside the skyhub container there is a second init layer: supervisord (docker/skyhub/supervisord.conf) is PID-1-like and launches six processes in priority order. Which ones start is decided by SKYHUB_ROLE. drone_node starts last (priority 999) so MAVROS, rosbridge, and stream rates are ready before it subscribes.

Gotchas a future editor must preserve

  • network_mode: host everywhere. There is no Docker port isolation — ports 9090, 5001, 322, and the 145xx MAVLink ports are on the host. All three application ports are env-configurable (ROSBRIDGE_PORT, GAMEPAD_WS_PORT, SKYHUB_SSH_PORT) because containers can share a host network namespace. Moving to bridged networking would break both DDS discovery and the 127.0.0.1 MAVLink loopback assumptions.
  • mavp2p is a single point of failure and un-configurable at runtime (endpoints are CLI args). The port map above is load-bearing.
  • Two independent module systems share the names ModuleLoader/ModuleBase: the ROS 2 env-registry loader in the ROS half vs. the explicitly-registered loader in the agent half. They now live in one image — ROS half at /, agent at /app — kept apart by sys.path, not by a container boundary. They are different code; do not merge them. See Module System.
  • Movement is GUIDED-mode SET_POSITION_TARGET_LOCAL_NED, not RC_CHANNELS_OVERRIDE. Zero velocity means hold, and control only activates when user-enabled and the vehicle is in GUIDED, with a dead-man command-timeout watchdog. All three guards are safety-critical — see Safe Control.
  • The video overlay is intentionally not burned into the stream (the “canvas approach”): ArUco tracking goes to Redis {ip}:aruco_tracking for a frontend canvas overlay, to save Jetson GPU and preserve video quality. setup_video_overlay() is a deliberate no-op.

Local development

There is a hardware-free path: docker-compose.local.yml overlays an ArduPilot SITL container and a local Redis (10.223.0.2) on a bridge network that mimics the VPN (10.223.0.0/16), parks the hardware services (mavproxy, rtk-ntrip) on a disabled profile, switches to the TEST camera and a software encoder, and injects IP_OVERRIDE so Redis channels resolve without wg0.
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d
See Local Development with SITL for the full overlay and how sitl.sh selects the vehicle type and home location.

Where to go next

Microservices & Container Profiles

Per-service reference: env vars, network mode, and startup ordering.

Redis Message Bus & WebSocket Interfaces

The full channel catalog and the dual command path.

MAVLink Routing (mavp2p)

The port map and flight-control link in detail.

Guided Velocity Control & Safety Model

The safe manual-flight control that replaced RC override.

e-ArUco Precision Landing

Marker-guided descent over the SIYI down-camera.

SkyCore CLI Reference

The host-run ArduPilot maintenance tool and the skycore installer CLI.