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).
- MAVLink / mavp2p — UDP mesh between the flight controller and every consumer (this page).
- ROS2 DDS — MAVROS publishes
/mavros/*topics on a sharedROS_DOMAIN_ID(default1). - 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 indocker-compose.local.yml/SITL) (this page). - 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, charging and camera status, TTS audio, and video-stream state between the two halves of the on-droneskyhub container (ROS half and agent half) 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 WireGuardwg0 IP, resolved at service startup:
- The agent half resolves it via
get_wg0_ip()(or theIP_OVERRIDEenv, legacy typoIP_OVRIDEstill honored) —skyhub_core/docker/gamepad/main.py:96,1622andskyhub_core/docker/gamepad/src/shared/config.py:52. The IP-namespaced channel names are then built inskyhub_core/docker/gamepad/src/modules/redis/handler.py:46-55. - The ROS half’s modules format
{self._ip_address}:...—skyhub_core/docker/core/src/modules/aruco_landing/module.py:281-283.
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_input | Backend/UI (via WS Proxy), ROS half aruco_landing | Agent half (RedisHandler) | Inbound control bus — axis/button frames, camera/gimbal/mode commands |
output | Agent half | WS Proxy → UI | Gamepad telemetry/status output stream |
aruco_tracking | ROS half aruco_landing | Agent half → UI | e-ArUco marker pose for the frontend canvas overlay (not burned into video) |
charging_status | Agent half (publish_charging_status) | Backend/UI | Battery-swap / charging pad status |
camera_status | Agent half (publish_camera_status) | Backend/UI | Recording / camera state |
chat | Agent half (publish_chat) | — (no on-vehicle consumer since the isaac-slam AudioListener was removed) | System chat → TTS announcement |
audio | Agent half (publish_audio_announcement) | — (no on-vehicle consumer since the isaac-slam AudioListener was removed) | Explicit TTS audio triggers |
restart_control | Agent half (publish_system_command) | System | Service restart / system commands |
The agent half’s
RedisHandler actively subscribes to only three channels at startup — {ip}:gamepad_input, {ip}:aruco_tracking, and the global video_stream_state (handler.py:136-145). The other IP-namespaced channels above are ones it publishes; their consumer is the cloud, via the WS Proxy.Global (non-namespaced) channels
Two channels are not IP-prefixed because the ROS half’s video_stream module is IP-agnostic (skyhub_core/docker/core/src/modules/video_stream/module.py:34-35):
| Channel | Publisher | Subscriber | Purpose |
|---|---|---|---|
video_stream_state | ROS half video_stream | Agent half → UI | Stream START/STOP sync to the UI |
video_stream_status_request | Agent half MessageRouter | ROS half video_stream | Ask the ROS half for the current stream status |
Publisher/subscriber matrix
MAVLink port map (mavp2p)
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:184:
skyhub_core/docker-compose.yml
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).
mavp2p is now a built image (skyhub_core/docker/mavp2p/, buildspec/mavp2p.yml): FROM scratch holding one static Go binary, 7.2 MB. It had run on every vehicle since the beginning and was built by hand and pushed.
| Endpoint | Role | Consumer | Consumer env |
|---|---|---|---|
serial:/dev/ttyACM0:115200 | Flight controller | ArduPilot FCU (Cube/Pixhawk) | — |
udpc 127.0.0.1:14550 | FC ↔ MAVROS | skyhub ROS half (MAVROS) | FCUURL=udp://127.0.0.1:14550@ |
udps 0.0.0.0:14777 | Manual control in | skyhub agent half (MAV_TYPE_GCS) | MAVLINK=udpout:127.0.0.1:14777 |
udps 0.0.0.0:14560 | RTCM / GGA | rtk-ntrip | RTK_MAVLINK_CONNECTION=udpout:127.0.0.1:14560 |
udps 0.0.0.0:14561 | Direct velocity | skyhub ROS half, aruco_landing | udpout:127.0.0.1:14561 |
udps 0.0.0.0:14900 | External GCS | e.g. QGroundControl over VPN | — |
14777 path, RTK NTRIP GPS Corrections for 14560, and ArUco Landing & Docking for 14561.
MAVROS topic & service inventory
FC telemetry flowsFCU → 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)
| Constant | Topic | ROS type |
|---|---|---|
GPS | /mavros/global_position/global | sensor_msgs/msg/NavSatFix |
GPS_RAW | /mavros/gpsstatus/gps1/raw | mavros_msgs/msg/GPSRAW |
RELATIVE_ALT | /mavros/global_position/rel_alt | std_msgs/msg/Float64 |
COMPASS_HEADING | /mavros/global_position/compass_hdg | std_msgs/msg/Float64 |
VFR_HUD | /mavros/vfr_hud | mavros_msgs/msg/VFR_HUD |
IMU_ORIENTATION | /mavros/imu/data | sensor_msgs/msg/Imu |
SYSTEM_STAT | /mavros/battery | sensor_msgs/msg/BatteryState |
HOME_POSITION | /mavros/home_position/home | mavros_msgs/msg/HomePosition |
DIAGNOSTICS | /diagnostics | diagnostic_msgs/msg/DiagnosticArray |
LOGS | /rosout | rcl_interfaces/msg/Log |
Command & mission services (Gateway calls)
| Constant | Service / topic | ROS type |
|---|---|---|
SET_MODE | /mavros/set_mode | mavros_msgs/srv/SetMode |
ARM_THROTTLE | /mavros/cmd/arming | mavros_msgs/srv/CommandBool |
TAKEOFF | /mavros/cmd/takeoff | mavros_msgs/srv/CommandTOL |
COMMAND_LONG | /mavros/cmd/command | mavros_msgs/srv/CommandLong |
PUSH_MISSION | /mavros/mission/push | mavros_msgs/srv/WaypointPush |
SET_CURRENT_MISSION | /mavros/mission/set_current | mavros_msgs/srv/WaypointSetCurrent |
PUSH_FENCE | /mavros/geofence/push | mavros_msgs/srv/WaypointPush |
GLOBAL_POSITION_PUBLISHER | /mavros/setpoint_position/global | mavros_msgs/msg/GlobalPositionTarget |
GLOBAL_POSITION_RAW | /mavros/setpoint_raw/global | mavros_msgs/msg/GlobalPositionTarget |
Parameter services (ArduPilot params)
ROS2 MAVROS usesParamSetV2 (not ROS1’s ParamSet) and has no param/get service, so reads go through rosapi or the batch get_parameters service.
| Constant | Service | ROS type |
|---|---|---|
PARAM_SET | /mavros/param/set | mavros_msgs/srv/ParamSetV2 |
PARAM_GET | /mavros/param/get_parameters | rcl_interfaces/srv/GetParameters |
PARAM_LIST | /mavros/param/list_parameters | rcl_interfaces/srv/ListParameters |
PARAM_PULL | /mavros/param/pull | mavros_msgs/srv/ParamPull |
PARAM_EVENT | /mavros/param/event | mavros_msgs/msg/ParamEvent |
ROSAPI_GET_PARAM | /rosapi/get_param | rosapi_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 the ROS half’s video_stream module:
| Constant | Topic | ROS type |
|---|---|---|
VIDEO_ROOM_DETAILS | /video_room_details | std_msgs/msg/String |
VIDEO_ROOM_STATE | /video_room_state | std_msgs/msg/String |
VIDEO_ROOM_SOURCE | /video_room_source | std_msgs/msg/String |
Related references
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.

