mavproxy compose service, which runs mavp2p — a lightweight, stateless MAVLink multiplexer. Every other service (core MAVROS, gamepad, rtk-ntrip, the e-ArUco lander, an external ground station) is a UDP client that connects to a mavp2p endpoint. mavp2p forwards each MAVLink frame from the serial FCU out to every UDP endpoint and forwards frames from any endpoint back to the FCU — a full mesh with the serial port at the hub.
This page is the load-bearing port map. If a service can’t reach the FCU, the answer is almost always here: the endpoint list is a hardcoded CLI argument in docker-compose.yml, not env-configurable, and each consumer’s connection string must match it byte-for-byte.
The endpoint map
Exact command fromdocker-compose.yml:139:
docker-compose.yml
| Port | Consumer | mavp2p role | Consumer connection string | Configured by | Source |
|---|---|---|---|---|---|
| serial | ArduPilot FCU | serial /dev/ttyACM0:115200 | — (physical link) | command: arg | docker-compose.yml:139 |
14550 | core MAVROS | udpc (client → MAVROS) | udp://127.0.0.1:14550@ | FCUURL | .env.example:24, entrypoint.sh:8 |
14777 | gamepad / ws_proxy | udps (server) | udpout:127.0.0.1:14777 | MAVLINK | .env.example:177, gamepad/src/shared/config.py:15 |
14560 | rtk-ntrip | udps (server) | udpout:127.0.0.1:14560 | RTK_MAVLINK_CONNECTION | .env.example:167, rtk-ntrip/src/shared/config.py:20 |
14561 | core e-ArUco lander | udps (server) | udpout:127.0.0.1:14561 | DEFAULT_MAVLINK_CONNECTION | core/src/modules/aruco_landing/controller.py:45 |
14900 | external GCS / debug | udps (server) | e.g. udp:<drone-vpn-ip>:14900 | operator’s GCS | docker-compose.yml:139 |
Why one endpoint is
udpc and the rest are udps. MAVROS is launched with FCUURL=udp://127.0.0.1:14550@, whose trailing @ makes MAVROS bind UDP/14550 as a server. So mavp2p must reach out to it as a client — hence udpc:127.0.0.1:14550. The gamepad, rtk, and aruco consumers all use udpout: (pymavlink’s “connect out as a client”), so mavp2p must be the server for them — hence udps:0.0.0.0:14777/14560/14561. Flip either side’s role and the socket never pairs up.network_mode: host, 127.0.0.1 is the shared host loopback — every container sees the same MAVLink ports on localhost. There is no Docker port remapping. (See Microservices & Container Profiles for the host-networking model and Redis Message Bus & WebSocket Interfaces for the non-MAVLink channels.)
The serial FCU link
The single serial endpointserial:/dev/ttyACM0:115200 is the physical USB link to the Cube/Pixhawk running ArduPilot. Two things about it are worth pinning down:
/dev/ttyACM0is owned by mavp2p at115200baud. Nothing else may open that device while mavp2p is up. This matters for the Isaac SLAM VIO path below, which wants its own serial link.- The host
skycore_cli.pydoes not go through mavp2p. It is a maintenance tool that opens its own MAVLink connection, auto-probing/dev/ttyACM*,/dev/ttyUSB*, and UDP14550/14551directly on the host — outside the container topology. See SkyCore CLI Reference.
MAVROS stream-rate bootstrap
Getting frames routed to14550 is necessary but not sufficient: ArduPilot will not stream sensor topics at a useful rate until a ground station asks it to. Inside the core container this is handled by set_stream_rates.sh, a one-shot step run under supervisord after MAVROS comes up.
The startup order is encoded as supervisord priority values (supervisord.conf): sshd(50) → MAVROS(100) → rosbridge(200) → stream_rates(300) → drone_node(999). Reordering these can break the /mavros/state subscription or the stream-rate call.
set_stream_rates.sh does three things, with a 120-second overall timeout:
Wait for FCU connection
Echoes
/mavros/state until it reads connected: true, then waits 5s for MAVROS plugins to finish initializing./mavros/global_position/global, /mavros/battery, and /mavros/vfr_hud publish slowly or not at all — which is why telemetry can look “dead” in the UI even when rosbridge (:9090) is connected. The Gateway subscribes to those same topics over rosbridge; see DroneControlService & Rosbridge Dispatch.
MAVROS itself is launched by entrypoint.sh:
docker/core/entrypoint.sh
Why isaac-slam does not use a MAVLink UDP endpoint
Notice thatisaac-slam is absent from the mavp2p port map. It never opens a UDP endpoint on mavp2p. It talks to the vehicle two other ways:
-
Reading vehicle state over ROS 2 DDS.
MavrosStateMonitorsubscribes directly to the/mavros/statetopic that thecorecontainer’s MAVROS already publishes — over the shared DDS domain, not MAVLink (.../navigation/mavros_state_monitor.py:201). Any node on the sameROS_DOMAIN_IDgets/mavros/*for free; re-parsing MAVLink would be redundant. -
Injecting VIO pose over a dedicated serial link.
pose_bridge_with_covariance.pytakes Isaac Visual SLAM’s/visual_slam/tracking/vo_pose, transforms ENU→NED, and sendsVISION_POSITION_ESTIMATEframes straight into the FCU — but over its own serial connection at 921600 baud, opened as MAVLink componentMAV_COMP_ID_VISUAL_INERTIAL_ODOMETRY:
docker/issac-slam/.../navigation/pose_bridge_with_covariance.py
14562-style mavp2p endpoint? The VIO feedback loop is latency- and rate-sensitive (30 Hz VISION_POSITION_ESTIMATE, plus EKF-origin, DO_SET_HOME, and SYSTEM_TIME sync), and ArduPilot’s vision-pose fusion expects it on a dedicated, high-baud link, distinct from the shared telemetry hub. Keeping it off mavp2p isolates the EKF-critical path from congestion on the 115200 serial hub.
Environment variables
| Var | Default | Used by | Purpose |
|---|---|---|---|
FCUURL | udp://127.0.0.1:14550@ | core MAVROS | MAVROS ↔ mavp2p endpoint (apm.launch fcu_url:=) |
MAVLINK | udpout:127.0.0.1:14777 | gamepad | gamepad MAVLink client → mavp2p |
RTK_MAVLINK_CONNECTION | udpout:127.0.0.1:14560 | rtk-ntrip | RTCM injection endpoint |
ROS_DOMAIN_ID | 1 | all ROS2 | DDS domain; MAVROS launch hardcodes 1 regardless |
ROSBRIDGE_PORT | 9090 | core rosbridge | ROS↔WebSocket port the Gateway connects to |
udpout:127.0.0.1:14561 default is a hardcoded class constant (controller.py:45) rather than something defined in .env.example, but it can be overridden via the ARUCO_MAVLINK_CONNECTION env var (controller.py:65). See MAVROS/MAVLink env vars in the platform reference for the full list.
Deployment divergences a future editor must preserve
Installer compose defines a smaller port map
Installer compose defines a smaller port map
docker/docker-compose.installer.yml (the templated first-boot compose) runs mavp2p on a different device and baud and with only three UDP endpoints:docker/docker-compose.installer.yml
14560 (rtk) and no 14561 (aruco). A drone provisioned from the installer compose will silently fail RTK and e-ArUco landing until it is upgraded to the full docker-compose.yml. Don’t assume the two composes agree.SITL replaces mavp2p entirely
SITL replaces mavp2p entirely
Under
docker-compose.local.yml the mavproxy service is disabled (moved to profiles: ["disabled"]) — there is no serial FCU and no mavp2p. The skyhub-sitl container is the ArduPilot FCU + MAVROS + rosbridge. Consumers point at the SITL instead:core:FCUURL=udp://:14550@(bind-only, learns SITL’s address)ws_proxy:MAVLINK=udpout:host.docker.internal:14777
14900 is for humans, not services
14900 is for humans, not services
Port
14900 (udps) has no in-repo consumer. It is the external/GCS debug endpoint: point QGroundControl or MAVProxy at udp:<drone-wg0-ip>:14900 over the WireGuard VPN to watch or command the FCU directly. It is intentionally always present in the port map even though nothing in SkyCore binds it.Adding a new MAVLink consumer
Pick a free UDP port
Extend the mavp2p
command: list in docker-compose.yml with a new udps:0.0.0.0:<port> (use udps unless your consumer binds/listens itself, in which case use udpc). Mirror the change in docker-compose.installer.yml if the feature must survive first boot.Connect as a client
Point your service at
udpout:127.0.0.1:<port> (via a new env var, following the MAVLINK / RTK_MAVLINK_CONNECTION pattern).Do not touch the FCU serial line
Never open
/dev/ttyACM0 directly — go through mavp2p. The only sanctioned exceptions are the host skycore_cli.py and the Isaac pose bridge on its separate high-baud device.Debugging a dead MAVLink link
Nothing reaches the FCU at all
Check the
mavproxy container is up and holds /dev/ttyACM0. If set_stream_rates.sh timed out (connected: true never seen), the serial/USB link — not ROS — is the problem.One service is blind
Compare that service’s connection string against the port map above. A
udpout: pointed at a port mavp2p isn’t serving (e.g. 14560 on an installer-compose drone) fails silently.Telemetry topics are slow/empty
MAVLink is flowing but stream rates weren’t set — inspect the
stream_rates supervisord log inside the core container.SLAM state or VIO missing
Not a mavp2p issue. Check the shared
ROS_DOMAIN_ID (must be 1) for /mavros/state, and the pose bridge’s dedicated serial device for VIO injection.Related pages
Microservices & Container Profiles
The compose services, host networking, and startup ordering that surround mavp2p.
Isaac Visual SLAM & Pose Bridge
The DDS + serial VIO path that deliberately bypasses mavp2p.
RTK NTRIP GPS Corrections
The 14560 consumer: RTCM injection and GGA feedback.
Guided Velocity Control & Safety Model
The 14777 gamepad consumer and its GUIDED-mode velocity safety guards.

