Frames and conventions¶
Frame and sign errors cause more lost days in marine robotics than any algorithm. ROS uses one set of conventions, and marine instruments, textbooks, and autopilots often use another. This page gives the rules, the conversions, and a tested code example.
The two conventions¶
| ROS convention | Marine and aerospace convention | |
|---|---|---|
| Defined by | REP 103 | SNAME (1950), Fossen, most INS and DVL vendors, ArduPilot and PX4 internally |
| World frame | ENU: x east, y north, z up | NED: x north, y east, z down |
| Body frame | FLU: x forward, y left, z up | FRD: x forward, y right, z down |
| Heading / yaw | Yaw, counter-clockwise from east | Heading, clockwise from north (compass) |
| Vertical | z up; altitude positive | z down; depth positive |
Rule: inside ROS, publish ENU world data and FLU body data. Convert at the edge of your system, in the driver or the bridge, and say so in the package README. Some packages also publish NED and FRD companion frames for interoperability (for example mavros), but the main tree stays ENU and FLU.
The frame tree on a vessel (REP 105)¶
REP 105 defines the mobile platform frames. On a marine vehicle:
flowchart TD
earth["earth<br/>(ECEF, optional, for multi-vehicle)"] --> map
map["map<br/>ENU, fixed to a local datum.<br/>Jumps when GNSS or USBL fixes arrive"] --> odom
odom["odom<br/>ENU, continuous, drifts.<br/>DVL + INS dead reckoning"] --> base["base_link<br/>FLU, vehicle reference point"]
base --> dvl["dvl_link"]
base --> imu["imu_link"]
base --> depth["pressure_link"]
base --> gnss["gnss_link (antenna)"]
base --> sonar["sonar_link"]
base --> cam["camera_link → camera_optical_frame"]
base --> thr["thruster_N_link"]
- map → odom comes from the global estimator (for example a second robot_localization instance with GNSS or USBL). It may jump.
- odom → base_link comes from the local estimator and must be continuous. Underwater, this is DVL, INS, and depth fusion.
- base_link → sensors are static transforms from the URDF. Measure lever arms carefully: a 10 cm DVL offset combined with a 30°/s yaw rate gives about 5 cm/s of false lateral velocity if you ignore it.
- Choose one documented reference point for
base_link, for example the center of gravity or the hull reference point that the survey offsets use. - Camera images use an optical frame (z forward, x right, y down) under the camera link, per REP 103.
Converting NED and FRD data to ENU and FLU¶
Positions and velocities¶
| Quantity | From NED / FRD | To ENU / FLU |
|---|---|---|
| World position or velocity | \( (n, e, d) \) | \( (x, y, z) = (e, n, -d) \) |
| Body velocity or acceleration | \( (u, v, w) \) forward, right, down | \( (u, -v, -w) \) |
| Body angular rate | \( (p, q, r) \) | \( (p, -q, -r) \) |
Heading and yaw¶
Wrap the result to \([-\pi, \pi]\). A compass heading of 0° (north) is a ROS yaw of +90°, and a heading of 90° (east) is a yaw of 0°. Also check whether the device reports magnetic or true heading: ROS yaw in an ENU frame is relative to true east, so apply the magnetic declination for your location if needed.
Orientation quaternions¶
For a full attitude, convert the quaternion; do not convert Euler angles one by one. If \( q_{\text{NED}}^{\text{FRD}} \) rotates FRD body vectors into NED:
where \( q_{\text{ENU}\leftarrow\text{NED}} = (w, x, y, z) = (0, \tfrac{\sqrt{2}}{2}, \tfrac{\sqrt{2}}{2}, 0) \) is a 180° rotation about the north-east diagonal, and \( q_{\text{FRD}\leftarrow\text{FLU}} = (0, 1, 0, 0) \) is a 180° rotation about x. The result: roll keeps its sign, pitch changes sign, and yaw is 90° minus heading.
import math
import numpy as np
# Quaternions are (w, x, y, z).
Q_NED_TO_ENU = np.array([0.0, math.sqrt(0.5), math.sqrt(0.5), 0.0])
Q_FLU_TO_FRD = np.array([0.0, 1.0, 0.0, 0.0])
def qmul(a, b):
"""Hamilton product a * b."""
w1, x1, y1, z1 = a
w2, x2, y2, z2 = b
return np.array([
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
])
def ned_frd_to_enu_flu(q_ned_frd):
"""Convert an attitude quaternion from NED/FRD to ROS ENU/FLU."""
return qmul(qmul(Q_NED_TO_ENU, q_ned_frd), Q_FLU_TO_FRD)
def ned_to_enu(n, e, d):
"""Convert a world-frame vector from NED to ENU."""
return e, n, -d
Check your conversion
Point the vehicle north, level. The NED attitude is the identity quaternion, and the ENU yaw must be +90°. Then raise the bow: NED pitch is positive, and ROS pitch must be negative. Test both cases in a unit test of your driver.
Depth, pressure, and altitude¶
- ROS
zis up, so z = −depth in themapandodomframes. Keep depth positive only in names and displays (for example a topic calleddepth). sensor_msgs/FluidPressureis in pascals, not bar or dbar.- A simple conversion is \( d = (p - p_{\text{atm}}) / (\rho g) \) with \( g = 9.80665\ \text{m/s}^2 \), \( \rho \approx 1025\ \text{kg/m}^3 \) for seawater and \( \approx 998\ \text{kg/m}^3 \) for fresh water. A 1% error in density gives a 1% error in depth. For survey-grade depth, use the TEOS-10 equations (for example
gsw.z_from_pin the GSW Python package), which also account for latitude. - Measure \( p_{\text{atm}} \) at the surface before each dive, because weather changes it by several kilopascals (1 kPa is about 10 cm of water).
- Altitude is the distance to the seafloor (from the DVL or an altimeter). Publish it as
sensor_msgs/Range, not as a position.
Geographic coordinates¶
- Publish fixes as WGS 84 latitude, longitude, and ellipsoidal altitude in
sensor_msgs/NavSatFix. - For a local metric frame, use a local tangent plane (ENU) at a documented datum, or UTM. robot_localization's navsat_transform_node does this for the EKF.
- At survey scale (more than a few kilometers), a flat-earth approximation adds errors. Use GeographicLib (
LocalCartesian) or thegeodesypackage from geographic_info. - UTM grid north differs from true north by the grid convergence angle, up to a few degrees. Account for it when you compare compass heading with a UTM-aligned frame.
Units¶
Use SI units, per REP 103: meters, seconds, radians, meters per second, pascals, kilograms. Convert knots, feet, degrees, and decibars in the driver. Note that sensor_msgs/Temperature is in degrees Celsius.
Time¶
Stamp every message with the time of measurement, in ROS time:
- Synchronize computers with PTP (best) or NTP and chrony. Where a GNSS receiver is present, use its PPS output as the time source.
- If a device has its own clock, convert its timestamps to ROS time and record the offset. Do not stamp with the time that the serial line delivered the data: DVL and sonar data can arrive tens to hundreds of milliseconds after the ping.
- In simulation, set
use_sim_time: trueon every node, including the EKF and your own nodes. - Acoustic fixes (USBL, LBL) refer to the time the signal left or arrived, often seconds before the message reaches the vehicle. robot_localization can process delayed measurements if you set
smooth_lagged_data: trueand ahistory_lengthlonger than the delay.
Networks and QoS¶
- Use
SensorDataQoS(best effort, small queue) for high-rate sensor streams, and reliable QoS for commands and mission state. - DDS discovery over Wi-Fi, cellular, or radio links to shore often fails or floods the link. Limit discovery with
ROS_AUTOMATIC_DISCOVERY_RANGEandROS_DOMAIN_ID, or use rmw_zenoh or a bridge for the shore link. - Never carry raw DDS over an acoustic link. See Communication.
Checklist for a new vehicle¶
- All world data in ENU and all body data in FLU, with conversions documented per driver
- Heading converted to yaw, with true or magnetic reference documented
- Every sensor has a measured, static transform from
base_link - Depth published as negative z; pressure in pascals
- All drivers stamp at the time of measurement; clocks synchronized
-
ros2 run tf2_tools view_framesshows one connected tree:map → odom → base_link → sensors - Bow-up and north-facing unit tests pass for every attitude source