MAVROS Transport¶
MavrosTransport connects the shared vehicle core to a running mavros_node. It backs the same vehicle over MAVROS for both firmwares (MavrosDrone for ArduPilot, Px4MavrosDrone for PX4) — the firmware-agnostic flight, navigation, takeoff/land, GPS, RTL, and PID behavior is shared and documented in the vehicle core (ArduPilot setpoint/parameter specifics in ArduPilot).
For the public
DroneAPI, navigation methods, references, altitude sources, takeoff/land detection, and PID configuration, see the vehicle core README.
Role¶
MavrosTransport implements the VehicleTransport interface against MAVROS:
- Telemetry ← ROS subscriptions. Each callback converts a
mavros_msgs/geometry_msgs/sensor_msgsmessage into a plainvehicle/typesvalue and stores it atomically. - Commands → ROS service clients (
/mavros/set_mode,/mavros/cmd/*,/mavros/param/set_parameters). - Setpoints → ROS publishers (
/mavros/setpoint_raw/local,/mavros/setpoint_position/global). - Frame conversion is handled by MAVROS itself (ENU↔NED), so the transport publishes ROS-convention values directly.
classDiagram
class VehicleTransport {
<<abstract>>
+state local_pose vision_pose gps heading rel_alt rangefinder distance_sensors
+arm() set_mode() command_takeoff() command_land() set_param()
+send_velocity_target() send_local_target() send_global_target()
}
class MavrosTransport {
-_vehicle_state VehicleState
-_local_plain _vision_plain _gps_plain LocalPose/GeoPoint
+start() close() driver_command()
+send_command_long(command, *params) bool
}
VehicleTransport <|.. MavrosTransport
MavrosTransport ..> mavros_node : ROS topics/services
MavrosDrone is simply ArduPilotDrone built with a MavrosTransport, and Px4MavrosDrone is Px4Drone built with the same transport (see drone.py and ../px4/mavros_drone.py).
Requirements¶
MAVROS is an opt-in dependency (it is not part of the core SDK install). Install it once with make drone-mavros (or ./scripts/setup.sh drone mavros), which adds ros-<distro>-mavros/-mavros-extras and the GeographicLib datasets.
A mavros_node must then be bridging the FCU. The transport can launch it for you when MavrosConfig.start_driver=True:
MavrosConfig.connection_string is a MAVROS fcu_url (e.g. serial:///dev/ttyUSB0:921600, tcp://127.0.0.1:5760), not a pymavlink string. See the MAVLink transport for the direct-connection alternative.
Configuration¶
import nectar
from nectar.control import DroneFactory, MavrosConfig, PoseSource
nectar.init()
config = MavrosConfig(
pose_source=PoseSource.VISION, # VISION (indoor) or GPS (outdoor)
expect_lidar=True, # wait for rangefinder at startup
connection_string="serial:///dev/ttyUSB0:921600",
apply_setpoint_params=False, # True to push WPNAV/GUID params to the FCU on arm
)
drone = DroneFactory.create("mavros", config)
MavrosConfig exposes a topic name for every subscription (state_topic, lidar_topic, local_position_topic, vision_topic, gps_topic, heading_topic, rel_alt_topic), an optional distance_sensors tuple (see Distance Sensors), plus pid_config_file / setpoint_config_file / apply_setpoint_params (consumed by the shared core). The pose source selects the indoor vs outdoor subscription set.
SITL presets (in config.py): SITL_CONFIG, SITL_GPS_CONFIG, SITL_GAZEBO_CONFIG, SITL_VISION_CONFIG.
Telemetry Mapping¶
Subscriber callbacks convert ROS messages to the core's plain types:
| ROS topic | ROS type | Core type | Stored as |
|---|---|---|---|
/mavros/state |
mavros_msgs/State |
VehicleState |
state |
/mavros/local_position/pose |
geometry_msgs/PoseStamped |
LocalPose |
local_pose |
/mavros/vision_pose/pose_cov |
geometry_msgs/PoseWithCovarianceStamped |
LocalPose |
vision_pose |
/mavros/vision_pose/pose |
geometry_msgs/PoseStamped |
LocalPose |
vision_pose |
/mavros/global_position/global |
sensor_msgs/NavSatFix |
GeoPoint |
gps |
/mavros/global_position/rel_alt |
std_msgs/Float64 |
float |
rel_alt |
/mavros/global_position/compass_hdg |
std_msgs/Float64 |
float |
heading |
/mavros/rangefinder/rangefinder |
sensor_msgs/Range |
float |
rangefinder |
/mavros/state is subscribed with TRANSIENT_LOCAL durability so the latest cached state is delivered on subscribe (reliably catching arm/mode changes). The vision subscription is chosen at runtime: a PoseWithCovarianceStamped callback when the topic name contains pose_cov, otherwise a plain PoseStamped. Indoor (pose_source=VISION) subscribes to the vision topic; outdoor subscribes to GPS, rel-alt, and compass heading.
Command topics (publishers) and MAVROS services are listed below.
Distance Sensors¶
lidar_topic feeds the downward rangefinder used for altitude. To expose additional rangefinders or proximity sectors (distance_sensors / get_distance(orientation) on the drone), two sides must line up:
- MAVROS must publish them. Its
distance_sensorplugin maps each FCUDISTANCE_SENSORid to asensor_msgs/Rangetopic through the plugin's per-sensor parameters. - The SDK must subscribe to those topics.
Rangecarries no id or orientation, so declare each one inMavrosConfig.distance_sensorsto tag the incoming readings:
from nectar.control import MavrosConfig, DistanceSensorTopic, SensorOrientation
config = MavrosConfig(
distance_sensors=(
DistanceSensorTopic("/mavros/distance_sensor/rangefinder_fwd", SensorOrientation.FORWARD, sensor_id=1),
DistanceSensorTopic("/mavros/distance_sensor/rangefinder_left", SensorOrientation.LEFT, sensor_id=2),
),
)
sensor_type is derived from Range.radiation_type; signal_quality is not available over MAVROS and stays None. The direct MAVLink transport needs none of this, reading id and orientation straight from DISTANCE_SENSOR. See the vehicle core README for the data model.
ROS2 Topics and Services¶
Publishers¶
| Topic | Type | Purpose |
|---|---|---|
/mavros/setpoint_raw/local |
PositionTarget | Velocity and local-position setpoints |
/mavros/setpoint_position/global |
GeoPoseStamped | GPS position setpoints (outdoor) |
/mavros/setpoint_raw/global |
GlobalPositionTarget | GPS raw setpoints (outdoor) |
Local setpoints use a velocity bitmask (velocity + yaw-rate active) for move_velocity and a position bitmask (position + yaw active) for local position targets. MAVROS translates PositionTarget to SET_POSITION_TARGET_LOCAL_NED (see setpoint_mixin.hpp).
Services¶
| Service | Type | Purpose |
|---|---|---|
/mavros/set_mode |
SetMode | Change flight mode |
/mavros/cmd/arming |
CommandBool | Arm/disarm motors |
/mavros/cmd/takeoff |
CommandTOL | Takeoff command |
/mavros/cmd/land |
CommandTOL | Land command |
/mavros/cmd/set_home |
CommandHome | Set home position |
/mavros/cmd/command |
CommandLong | Generic MAVLink commands (set_speed, do_servo, disarm) |
/mavros/param/set_parameters |
SetParameters | Set FCU parameters |
Service-Call Behavior¶
All service calls go through BaseDrone._call_service, which is deadlock-safe: per the ROS 2 sync/async guidance it calls call_async and blocks on the future while the SDK executor keeps spinning callbacks on a background thread. It returns the response object (or None on timeout/error); it only validates response fields when a validator is passed.
The transport interprets responses as follows:
| Command | Method | Success condition |
|---|---|---|
| set_mode, arm, takeoff, land, set_home, disarm | bool(res) |
Service-ACK heuristic — a returned response counts as success |
generic COMMAND_LONG (set_speed, do_servo) |
send_command_long |
bool(res) and res.success |
set_param |
— | all results[].successful |
Why the service-ACK heuristic for arm/takeoff/land: validating res.success / res.result on these services produced false negatives and unnecessary retries, so the transport treats a well-formed MAVROS response as acceptance and lets the vehicle core confirm the outcome from telemetry: arm() polls is_armed/mode, and takeoff/land use FlightSequencer settle detection. Generic COMMAND_LONG has no follow-up state to poll, so it still checks res.success.
MAV_RESULT Codes¶
MAVROS command services report MAVLink MAV_RESULT in the result field: 0 ACCEPTED, 1 TEMPORARILY_REJECTED, 2 DENIED, 3 UNSUPPORTED, 4 FAILED, 5 IN_PROGRESS.
Indoor Vision¶
With MAVROS, the FCU's EKF (ArduPilot EKF3 / PX4 EKF2) receives external navigation through MAVROS' own vision plugin: an external pose source publishes to /mavros/vision_pose/pose_cov (or /pose), and MAVROS converts ENU→NED and sends VISION_POSITION_ESTIMATE to the FCU. The transport only subscribes to the same topic to expose vision_pose for companion-side PID navigation.
- D435i + Isaac ROS Visual SLAM (current):
D435i → isaac_ros_visual_slam → relay → /mavros/vision_pose/pose_cov - T265 + vision_to_mavros (legacy/fallback):
T265 VIO → /tf → vision_to_mavros (ENU alignment) → /mavros/vision_pose/pose
Per-firmware FCU parameters (sources, rate, height source) and the EKF-origin step are documented in Localization → FCU setup (ArduPilot EKF-origin detail: EKF Origin). The direct MAVLink transport replaces this MAVROS relay with a built-in VisionPoseBridge.
References¶
- MAVROS GitHub · MAVROS ROS2 API · MAVROS Wiki
- SET_POSITION_TARGET_LOCAL_NED · MAV_RESULT · VISION_POSITION_ESTIMATE
- ROS 2 Sync vs Async Service Clients · ROS 2 Executors
- vision_to_mavros · Isaac ROS cuVSLAM with RealSense
- Shared flight logic: Vehicle core · ArduPilot specifics: ArduPilot