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 SIYI/RealSense cameras 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.
Despite the name, 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 toggle, params, log/terrain download, camera SD). The real “boot” is docker-compose profiles plus supervisord inside the core container. See the SkyCore CLI Reference.

The six 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
corecoreROS 2 service: MAVROS + rosbridge (:9090) + drone_node, which loads the video_stream, aruco_landing, and battery modules. The drone’s cloud-facing brain.Services · Module System
ws_proxygamepadFastAPI WebSocket (:5001 /gamepad) for manual control, safe GUIDED velocity, gimbal/zoom, and arm/disarm asset upload (HLS video, photos, logs) to S3.Safe Control · Video
mavproxymavproxyThe mavp2p MAVLink router. Bridges the FC serial link to a fixed UDP port map for every other service.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
camera-proxycameraRe-serves the SIYI RTSP camera as a local endpoint (:8554 /stream HQ passthrough + /fast_stream HW re-encoded for YOLO).Video Streaming
isaac-slamslamGPU service: Isaac ROS Visual SLAM + nvblox + docking + TTS audio. Bridges VIO pose into the FC EKF for GPS-denied nav.SLAM Navigation
The Compose service name and its profile name are not always the same: the gamepad service is the ws_proxy service under profile gamepad. The default in .env.example is COMPOSE_PROFILES=mavproxy,core,rtk,gamepadslam and camera are opt-in.

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 service must agree on (docker-compose.yml:139):
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
14550core MAVROS (FCUURL=udp://127.0.0.1:14550@)
14777gamepad MAVLink handler (MAVLINK=udpout:127.0.0.1:14777)
14560rtk-ntrip RTCM injection
14561core 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 service’s env vars. Change one side without the other and the MAVLink link silently breaks. isaac-slam is the exception — it does not use mavp2p UDP; it reads /mavros/state over DDS and injects vision pose over its own serial path. See MAVLink Routing.

2. ROS 2 DDS

The core container’s MAVROS publishes the /mavros/* topic tree, drone_node consumes /mavros/state, and isaac-slam subscribes to the same topics — all over DDS on a shared ROS_DOMAIN_ID (default 1) 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 gamepad service 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 ws_proxy/:5001 service, 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_on (core and rtk-ntrip depend on mavproxy; isaac-slam depends on mavproxy, core, and ws_proxy). Inside the core container there is a second init layer: supervisord (docker/core/supervisord.conf) is PID-1-like and launches five processes in priority order. drone_node starts last (priority 999) so MAVROS, rosbridge, and stream rates are ready before it subscribes.
docker/core/entrypoint.sh hardcodes ROS_DOMAIN_ID=1 for the MAVROS launch, ignoring the env var. If you set ROS_DOMAIN_ID to anything other than 1, MAVROS and drone_node/isaac-slam land on different DDS domains and silently stop seeing each other.

Gotchas a future editor must preserve

  • network_mode: host everywhere. There is no Docker port isolation — ports 9090, 5001, 8554, and the 145xx MAVLink ports are on the host. 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 core vs. the explicitly-registered loader in gamepad. They are different code — do not merge them blindly. 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.
  • The isaac-slam directory is misspelled issac-slam on disk, while the Compose service and image use isaac. The bind mount hardcodes an absolute host path into /home.

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), disables the hardware services (mavproxy, camera-proxy, rtk-ntrip, isaac-slam), 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.

Isaac Visual SLAM & Pose Bridge

GPS-denied navigation via VIO pose injection.

SkyCore CLI Reference

The host-run ArduPilot maintenance tool.