This is the low-level wiring reference for the two message fabrics a drone runs on top of ROS2: the Redis pub/sub bus (drone-side inter-service messaging, keyed off the drone’s WireGuard IP) and the MAVLink UDP mesh routed by mavp2p. It also enumerates the MAVROS topics and services the Gateway drives over rosbridge. Everything here is load-bearing — a port number or channel prefix that drifts silently severs two components with no error.
On the vehicle there are two local fabrics (MAVLink and ROS2 DDS), plus a shared Redis bus the drone dials out to and one WebSocket uplink. Redis and MAVLink are documented on this page; the other two are covered in Redis Message Bus & WebSocket Interfaces and MAVLink Routing (mavp2p).
  1. MAVLink / mavp2p — UDP mesh between the flight controller and every consumer (this page).
  2. ROS2 DDS — MAVROS publishes /mavros/* topics on a shared ROS_DOMAIN_ID (default 1); isaac-slam reads /mavros/state here.
  3. Redis pub/sub — JSON control/status messages, IP-namespaced; a shared bus the drone connects out to (REDIS_HOST, cloud-hosted in prod; drone-local only in docker-compose.local.yml/SITL) (this page).
  4. rosbridge :9090 — the ROS2↔WebSocket uplink the Gateway attaches to (see Rosbridge Connection & Reconnect).

Redis pub/sub channel catalog

Redis is the drone’s control/status bus. It carries manual gamepad input, ArUco tracking overlays, flight-mode/armed announcements, docking status, TTS audio, and video-stream state between the on-drone services (core, gamepad, isaac-slam) and the cloud (WS Proxy → Dashboard). It is separate from the Gateway’s rosbridge path — manual control and these status streams never traverse the Gateway.

Namespacing rule

Almost every channel is prefixed with the drone’s WireGuard wg0 IP, resolved at service startup:
  • gamepad resolves it via get_wg0_ip() (or the IP_OVERRIDE env, legacy typo IP_OVRIDE still honored) — skyhub_core/docker/gamepad/main.py:86,1145 and skyhub_core/docker/gamepad/src/shared/config.py:34. The IP-namespaced channel names are then built in skyhub_core/docker/gamepad/src/modules/redis/handler.py:44-54.
  • isaac-slam reads it from the MY_IP env (falls back to localhost) — skyhub_core/docker/issac-slam/.../services/navigation/mavros_state_monitor.py:335.
  • core modules format {self._ip_address}:...skyhub_core/docker/core/src/modules/aruco_landing/module.py:248-249.
{wg0_ip}:{topic}       e.g.  10.71.0.8:gamepad_input
The WS Proxy on the cloud side publishes/subscribes to the same prefix using the drone’s IP looked up from PostgreSQL, which is how per-drone messages stay isolated on a shared Redis.
Without a wg0 interface you MUST set IP_OVERRIDE (gamepad) / MY_IP (isaac-slam) or the drone’s channels won’t match what the cloud publishes — messages silently go nowhere. This is the single most common “my command isn’t received” cause in SITL/dev.

IP-namespaced channels

All of these are prefixed {wg0_ip}:. “Backend/UI” reaches them through the WS Proxy ({ip}:gamepad_input inbound, {ip}:output/{ip}:aruco_tracking outbound).
Channel ({ip}: prefix)Publisher(s)Subscriber(s)Purpose
gamepad_inputBackend/UI (via WS Proxy), core aruco_landing, isaac-slam dockinggamepad (RedisHandler)Inbound control bus — axis/button frames, camera/gimbal/mode commands, auto-dock movement
outputgamepadWS Proxy → UIGamepad telemetry/status output stream
aruco_trackingcore aruco_landinggamepad → UIe-ArUco marker pose for the frontend canvas overlay (not burned into video)
mode_changeisaac-slam MavrosStateMonitorBackend/UIFlight-mode transitions (drives audio + UI)
armed_stateisaac-slam MavrosStateMonitorBackend/UIArmed/disarmed transitions
docking_statusisaac-slam DockingServiceBackend/UIVision-docking progress/result
slam_controlgamepad (publish_slam_command)isaac-slamSLAM start/stop/mode commands
charging_statusgamepad (publish_charging_status)Backend/UIBattery-swap / charging pad status
camera_statusgamepad (publish_camera_status)Backend/UIRecording / camera state
chatgamepad (publish_chat)isaac-slam AudioListenerSystem chat → TTS announcement
audiogamepad (publish_audio_announcement)isaac-slam AudioListenerExplicit TTS audio triggers
restart_controlgamepad (publish_system_command)SystemService restart / system commands
gamepad’s RedisHandler actively subscribes to only three channels at startup — {ip}:gamepad_input, {ip}:aruco_tracking, and the global video_stream_state (handler.py:120-137). The other IP-namespaced channels above are ones it (or isaac-slam) publishes; their consumers are the cloud (via WS Proxy) or the isaac-slam audio/docking services.

Global (non-namespaced) channels

Two channels are not IP-prefixed because core’s video_stream module is IP-agnostic (skyhub_core/docker/core/src/modules/video_stream/module.py:34-35):
ChannelPublisherSubscriberPurpose
video_stream_statecore video_streamgamepad → UIStream START/STOP sync to the UI
video_stream_status_requestgamepad MessageRoutercore video_streamAsk core for the current stream status
Because these two channels have no IP prefix, multiple drones sharing one Redis instance can cross-talk on video-stream state. Preserve the IP-namespacing on every other channel and be deliberate before adding a new global channel.

Publisher/subscriber matrix

mavp2p (the mavproxy compose service) is the single MAVLink router on the drone. It bridges the ArduPilot flight controller’s serial link to every UDP consumer bidirectionally — each endpoint sees FC traffic and can talk to the FCU. It has no env config; the endpoint list is pure CLI args in skyhub_core/docker-compose.yml:139:
skyhub_core/docker-compose.yml
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"]
udps = mavp2p is the UDP server (the consumer dials in with udpout:127.0.0.1:<port>); udpc = mavp2p connects out to a listener (MAVROS binds 14550).
EndpointRoleConsumerConsumer env
serial:/dev/ttyACM0:115200Flight controllerArduPilot FCU (Cube/Pixhawk)
udpc 127.0.0.1:14550FC ↔ MAVROScore MAVROSFCUURL=udp://127.0.0.1:14550@
udps 0.0.0.0:14777Manual control ingamepad (MAV_TYPE_GCS)MAVLINK=udpout:127.0.0.1:14777
udps 0.0.0.0:14560RTCM / GGArtk-ntripRTK_MAVLINK_CONNECTION=udpout:127.0.0.1:14560
udps 0.0.0.0:14561Direct velocitycore aruco_landingudpout:127.0.0.1:14561
udps 0.0.0.0:14900External GCSe.g. QGroundControl over VPN
Gotchas a refactor must preserve:
  • mavp2p is a single point of failure and un-configurable at runtime — the port map lives only in the compose command. Changing a port breaks the matching consumer’s udpout env.
  • isaac-slam does NOT use a MAVLink UDP endpoint. It reads /mavros/state over ROS2 DDS and injects VIO pose separately. Its pose_bridge default config lists serial /dev/ttyACM0 @921600, but mavp2p already owns that serial at 115200 — the two cannot both hold the port, so verify the real vision-pose route before trusting the serial default.
  • The installer compose diverges: skyhub_core/docker/docker-compose.installer.yml defines only 14900/14550/14777 (no RTK/ArUco ports) and uses /dev/ttyUSB1@230400.
See MAVLink Routing (mavp2p) for the full routing rationale, Guided Velocity Control & Safety Model for the 14777 path, RTK NTRIP GPS Corrections for 14560, and YOLO Detection, ArUco Landing & Docking for 14561.

MAVROS topic & service inventory

FC telemetry flows FCU → mavp2p → MAVROS → /mavros/* ROS2 topics → rosbridge :9090 → Gateway. The Gateway addresses these by the (topic_name, ros_type) tuple constants in src/utils/mavros_topics.py. Subscriptions are throttled to TELEMETRY_THROTTLE_RATE (default 200 ms, src/application/settings.py:145) except /diagnostics, which is subscribed with throttle_rate=0.
set_stream_rates.sh on the drone must raise the FC MAVLink stream rate to 10 Hz first, or the sensor /mavros/* topics never populate. The rosbridge subscribe/emit path and the dashboard yaw computation live in Socket.IO Telemetry Streaming; this table is just the topic contract.

Telemetry topics (Gateway subscribes)

ConstantTopicROS type
GPS/mavros/global_position/globalsensor_msgs/msg/NavSatFix
GPS_RAW/mavros/gpsstatus/gps1/rawmavros_msgs/msg/GPSRAW
RELATIVE_ALT/mavros/global_position/rel_altstd_msgs/msg/Float64
COMPASS_HEADING/mavros/global_position/compass_hdgstd_msgs/msg/Float64
VFR_HUD/mavros/vfr_hudmavros_msgs/msg/VFR_HUD
IMU_ORIENTATION/mavros/imu/datasensor_msgs/msg/Imu
SYSTEM_STAT/mavros/batterysensor_msgs/msg/BatteryState
HOME_POSITION/mavros/home_position/homemavros_msgs/msg/HomePosition
DIAGNOSTICS/diagnosticsdiagnostic_msgs/msg/DiagnosticArray
LOGS/rosoutrcl_interfaces/msg/Log

Command & mission services (Gateway calls)

ConstantService / topicROS type
SET_MODE/mavros/set_modemavros_msgs/srv/SetMode
ARM_THROTTLE/mavros/cmd/armingmavros_msgs/srv/CommandBool
TAKEOFF/mavros/cmd/takeoffmavros_msgs/srv/CommandTOL
COMMAND_LONG/mavros/cmd/commandmavros_msgs/srv/CommandLong
PUSH_MISSION/mavros/mission/pushmavros_msgs/srv/WaypointPush
SET_CURRENT_MISSION/mavros/mission/set_currentmavros_msgs/srv/WaypointSetCurrent
PUSH_FENCE/mavros/geofence/pushmavros_msgs/srv/WaypointPush
GLOBAL_POSITION_PUBLISHER/mavros/setpoint_position/globalmavros_msgs/msg/GlobalPositionTarget
GLOBAL_POSITION_RAW/mavros/setpoint_raw/globalmavros_msgs/msg/GlobalPositionTarget

Parameter services (ArduPilot params)

ROS2 MAVROS uses ParamSetV2 (not ROS1’s ParamSet) and has no param/get service, so reads go through rosapi or the batch get_parameters service.
ConstantServiceROS type
PARAM_SET/mavros/param/setmavros_msgs/srv/ParamSetV2
PARAM_GET/mavros/param/get_parametersrcl_interfaces/srv/GetParameters
PARAM_LIST/mavros/param/list_parametersrcl_interfaces/srv/ListParameters
PARAM_PULL/mavros/param/pullmavros_msgs/srv/ParamPull
PARAM_EVENT/mavros/param/eventmavros_msgs/msg/ParamEvent
ROSAPI_GET_PARAM/rosapi/get_paramrosapi_msgs/srv/GetParam
COMMON_ARDUPILOT_PARAMS in the same file lists the fence / RTL / arming / battery / failsafe / flight / GPS / EKF / serial / logging parameter names the Gateway batch-reads. See DroneControlService & Rosbridge Dispatch for how these are invoked and Mission & Geofence MAVLink Format for the waypoint frame/command encoding.

Video-room control topics (Gateway publishes)

The Gateway pushes Janus room details to the drone over rosbridge topics (std_msgs/msg/String JSON) — consumed by core’s video_stream module:
ConstantTopicROS type
VIDEO_ROOM_DETAILS/video_room_detailsstd_msgs/msg/String
VIDEO_ROOM_STATE/video_room_statestd_msgs/msg/String
VIDEO_ROOM_SOURCE/video_room_sourcestd_msgs/msg/String

Redis Message Bus

Drone-side Redis handlers, WebSocket interfaces, and message routing internals.

MAVLink Routing (mavp2p)

Why the port map is what it is and how to add a MAVLink consumer.

Rosbridge Connection

How the Gateway pools and drives these topics over WebSocket :9090.

Environment Variables

Platform-wide config including IP_OVERRIDE, REDIS_HOST, FCUURL, throttle rates.

Full API Reference

HTTP + Socket.IO surface that fronts these channels.

Database Schema

The drone/user tables the WS Proxy uses to resolve IP prefixes.