How an underwater robot writes things down

Five animated patterns that describe how autonomous underwater vehicles organize their data as ROS 2 messages and topics — and why, once you know them, a multi-terabyte mission recording is just a well-behaved dataset.

← brucekrogman.com · Bruce Krogman · researched August 2026 · sources at the bottom


An autonomous underwater vehicle (AUV) on a long mission is cut off from the world: radio doesn't travel through seawater, so for days or weeks the vehicle simply records. What it records is not a pile of text logs. Modern vehicles run ROS 2, and everything every sensor says is written into a bag — a self-describing binary archive of typed, timestamped messages organized by topic. Surveying the drivers, simulators, message standards, and public datasets of the marine robotics world turns up the same handful of structural patterns over and over. Here are the five that matter, animated.

New to the jargon? Every dotted term is clickable — each opens a short three-part explanation: the technical definition, a plain-English version, and what the thing is actually used for. A full glossary sits at the bottom of the page.

The five patterns at a glance A summary of the whole article. One: many sensors, few message types — about a dozen standard types cover nearly any vehicle. Two: every topic keeps its own clock — a bag is not one table but many time series at different rates. Three: where standards are missing, conventions grow anyway — no official Doppler velocity log message exists, yet five projects converged on the same shape. Four: the public data landscape has an empty bin — the format modern vehicles actually log has almost no public data. Five: the bandwidth pyramid — up to 250 gigabytes per hour on disk, but only a few kilobits per second back to shore. Together they make post-mission analysis ordinary data science. 01 Many sensors, few message types About a dozen standard types cover nearly any vehicle. 02 Every topic keeps its own clock A bag is not one table — it is many time series at many different rates. 03 Where standards are missing, conventions grow anyway No official DVL message exists, yet five projects built the same shape. 04 The public data landscape has an empty bin The format modern vehicles actually log has almost no public data. 05 on disk to shore The bandwidth pyramid Up to 250 GB an hour onto disk; a few kbit/s back to the surface. Together they are why post-mission analysis is ordinary data science: bag → one DataFrame per topic → aligned on time

The whole guide in one picture. Each pattern below expands one of these rows, animated; the last section maps them onto the Python you would actually write.

PATTERN 01

Many sensors, few message types

IMU
DVL
Depth sensor
Side-scan sonar
Camera
GPS (mast up)
Nav EKF
mission.mcap
0
messages
Seven different instruments; every one of them lands in the same file as a typed, timestamped message.

What you're watching: each sensor on the vehicle publishes its readings as messages on a named topic, and the bag recorder subscribes to everything and writes it all into one file. The message types flying by are not invented per vehicle — they come from a small shared vocabulary. Across every driver, simulator, and dataset surveyed, the same core set covers nearly everything:

Message typeWhat it carriesTypical rate
sensor_msgs/Imuattitude, angular rates, acceleration100–333 Hz
nav_msgs/Odometrythe fused navigation solution10–50 Hz
sensor_msgs/FluidPressuredepth30–60 Hz
sensor_msgs/NavSatFixGPS — only when surfacedepisodic
DVL message (see Pattern 3)velocity over the seabed, altitude2–15 Hz
sensor_msgs/Image / CompressedImagecameras and imaging sonar1–20 Hz
sensor_msgs/PointCloud2multibeam / 3-D sonar1–10 Hz
sensor_msgs/LaserScanprofiling & scanning sonar1–10 Hz
sensor_msgs/BatteryStatepower system telemetry~1 Hz
tf2 transformsthe frame tree tying it all togetherwith nav

Learn roughly a dozen message types and you can read the recordings of nearly any vehicle — regardless of who built it. The data vocabulary is small; the volume is what's big.

PATTERN 02

Every topic keeps its own clock

Blink rates are slowed ~25× so the slow topics are visible; the counters show the real divergence. One hour of mission time means millions of IMU rows and roughly 900 acoustic-modem packets.

What you're watching: eight real topics publishing at their characteristic rates. The hierarchy is remarkably consistent across vehicles — inertial sensing fastest, then depth, the fused nav solution, cameras, the Doppler velocity log, sonar frames, health telemetry, and finally the acoustic modem, which manages about thirty bytes every four seconds. The bars are on a log scale; within seconds they sort themselves into the same pyramid you'd find in any real mission bag.

This is the single most important thing to internalize before analyzing this data: a bag is not one table. It is a bundle of independent time series, each at its own rate, sharing nothing but a clock. Any cross-sensor question — what was the battery doing when the vehicle was at 40 m depth? — is a time-alignment problem: resample or as-of-join on timestamps, then analyze.

One DataFrame per topic, aligned on time. That's the entire mental model — and time-series alignment is a solved problem in the pandas ecosystem (merge_asof, resampling).

The same problem exists on the vehicle, and ROS 2 solves it the same way under a different name: message_filters and its ApproximateTimeSynchronizer match messages arriving on several topics at different rates, in the live stream, before anything is recorded. Offline it is an as-of join over a recorded table; online it is a synchronizer over a message queue. Learning one teaches the other.

PATTERN 03

Where standards are missing, conventions grow anyway

The de facto DVL message
Five packages, five maintainers, no coordination — one shape.

What you're watching: there is no official ROS 2 message for a Doppler velocity log — the workhorse sensor of underwater navigation. Instead, five independent projects (a Swedish research fleet, two simulators, a community driver, and a multi-institution standards effort) each defined their own. Cycle through them and the field list barely moves: a header, a 3-D velocity vector, a covariance matrix, an altitude, and a per-beam array.

The same story repeats elsewhere. Imaging sonar has no standard message either — in practice it travels as sensor_msgs/Image, or as a vendor message published alongside a standard-type twin topic. And the whole marine domain is permanently bilingual between ROS's east-north-up coordinate convention and the maritime world's north-east-down, resolved by suffixing frames (odom_ned).

Missing standards are not chaos — they're convergent evolution. For an analyst the practical consequence is even better: bag files embed the schema of every custom message inside the file, so even a message you've never seen deserializes cleanly. The adapters you have to write are thin.

PATTERN 04

The public data landscape has an empty bin

A survey of public marine robotics datasets, sorted by distribution format. The bin for the format that current-generation ROS 2 vehicles actually log — rosbag2/MCAP — is empty.

What you're watching: the marine datasets that exist in public, sorting themselves into bins. Academic navigation datasets ship as legacy rosbag1 files. Industrial and institutional missions (Kongsberg HUGIN surveys, WHOI's Sentry, MBARI, NOAA archives) publish in vendor formats — .kmall, XTF, SEG-Y, netCDF. Sonar machine-learning datasets are distributed as plain image files, stripped of their context. And the modern format, rosbag2/MCAP, has essentially no established public data at all.

That empty bin is an opportunity. Anyone who can bridge the gap — convert legacy bags forward, parse vendor formats, and build first-look tooling for MCAP mission data — is working in territory the public tooling hasn't reached yet.

PATTERN 05

The bandwidth pyramid: terabytes on board, kilobytes ashore

AUV — onboard
day 0 of 38
0.0 TB recorded
satellite / acoustic — ~340 B per burst
dock offload — full recordings
Shore
live ops · post-mission analysis
operator has: 0.0 kB
analysts have: 0.0 TB
Underway: the operator steers a 22 TB mission through a soda straw.

What you're watching: an entire long-endurance sortie compressed into a loop. Underwater, side-scan and synthetic-aperture sonar accumulate on the order of 60–250 GB per surveying hour, so the onboard disk climbs toward tens of terabytes. Meanwhile the only paths to shore mid-mission are an acoustic modem at a few kilobits per second and satellite bursts of a few hundred bytes — roughly nine orders of magnitude narrower than the disk. Only when the vehicle is recovered does the flood arrive.

This single constraint shapes the entire software stack: onboard edge processing must reduce raw sensor data to tiny contact reports and health summaries the operator can act on now, and post-mission analysis must digest the full terabytes fast once they land — which questions did the mission answer, what did the detectors find, how is the vehicle aging.

The scarce resource isn't data — it's the time of whoever makes sense of it. That's a data engineering problem, and it's the one I find most interesting.


SO WHAT

From bag to DataFrame in fifteen lines

The payoff of these five patterns: mission data that looks exotic from the outside is, structurally, a well-organized multi-rate time-series database. Because bags are self-describing, they open with an ordinary pure-Python library — no robot, no ROS installation — and each topic falls out as a tidy table:

from pathlib import Path
import pandas as pd
from rosbags.highlevel import AnyReader

rows = []
with AnyReader([Path("mission_bag")]) as reader:
    conns = [c for c in reader.connections if c.topic == "/fuel_cell"]
    for conn, timestamp, raw in reader.messages(connections=conns):
        msg = reader.deserialize(raw, conn.msgtype)
        rows.append({"t": timestamp / 1e9,
                     "voltage": msg.voltage,
                     "current": msg.current,
                     "temp": msg.temperature})

df = pd.DataFrame(rows).set_index("t")   # and now it's just data science

From here it's resampling, as-of joins, anomaly detection, dashboards, and model-building — the standard toolkit, pointed at an unusually interesting ocean.


ROS 2 → PYTHON

Where the ROS side meets the Python side

Everything above describes what the data means. This is where it lives in software — the path from a sensor on the vehicle to a table you can compute on, and which library you reach for at each step. The useful thing about this picture is how narrow the ROS-specific part turns out to be: it ends at the recording, and everything after it is ordinary Python.

From vehicle sensors to a pandas DataFrame A layered diagram: sensors on the vehicle publish through the ROS 2 runtime into standard message types, which are recorded to a rosbag2 or MCAP file. From there two paths diverge — an on-vehicle live path using rosbag2_py, tf2_ros and message_filters, and an off-vehicle post-mission path using the pure-Python rosbags library into pandas. Both solve the same multi-rate time-alignment problem. UNDERWATER VEHICLE IMU DVL SONAR DEPTH GPS BATTERY ROS 2 RUNTIME rclpy · node · publisher · subscription · QoS · parameter · executor MESSAGE / TOPIC LAYER sensor_msgs/ Imu · NavSatFix · FluidPressure · LaserScan · PointCloud2 · BatteryState nav_msgs/Odometry  ·  tf2_msgs/TFMessage  ·  vendor custom types THE RECORDING rosbag2 / MCAP — schemas embedded ON THE VEHICLE — LIVE rosbag2_py · tf2_ros · cv_bridge message_filters ApproximateTimeSynchronizer reduce to contacts before the acoustic link OFF THE VEHICLE — POST-MISSION rosbags — pure Python, no ROS pandas — one DataFrame per topic merge_asof then NumPy · SciPy · matplotlib · pymap3d ApproximateTimeSynchronizer ≡ merge_asof the same multi-rate alignment problem — once over a message queue, once over a recorded table

The ROS-specific span is only the middle three bands. Above them are sensors; below the recording it is ordinary Python, which is why a mission bag opens on a laptop that has never had ROS installed.

Read across rather than down and the same picture becomes a lookup table. Nearly every concept on this page has one standard message type behind it, and one Python move that actually matters once you have it in hand:

ConceptROS 2 interfaceWhat the Python side has to do with it
IMUsensor_msgs/Imuorientation arrives as a quaternion — canonicalize sign before differencing (scipy.spatial.transform.Rotation)
pressure / depthsensor_msgs/FluidPressurea scalar; straight to a DataFrame column, no unpacking
NavSatFixsensor_msgs/NavSatFixgeodetic to local ENU or NED with pymap3d
PointCloud2sensor_msgs/PointCloud2packed binary — unpack with sensor_msgs_py.point_cloud2.read_points, never by hand
LaserScansensor_msgs/LaserScanan array payload; summarise it, don't explode 6,000 bins into 6,000 columns
BatteryStatesensor_msgs/BatteryStatea health time series; resample before trending
odometrynav_msgs/Odometrynested pose and twist; flatten to dotted columns — and check the twist is actually populated
tf2 frame treetf2_msgs/TFMessagetf2_ros Buffer.lookup_transform composes the chain and interpolates to a timestamp
vendor custom typedefined by the manufacturernothing special — the schema travels inside the bag, so it deserializes without the vendor's packages

The ROS-side vocabulary you need in order to read mission data is small and mostly nouns — a dozen message types and the frame tree. The verbs are all on the Python side, and they are the ones any data engineer already owns.

GLOSSARY

Every term, three ways

All 34 terms used on this page now live on their own page, each defined three ways — technical, plain English, and what it is actually used for — and each with a full infographic panel. The dotted terms above still explain themselves in place; the glossary is there when you want to read or search the whole vocabulary at once.

Open the glossary →