The service API¶
Everything else in this project (the CLI, the Python library, the browser UI) is a client of these services. A node that speaks ROS needs nothing else.
The names below are relative to the recorder node, so with the default name ~/pause is
/rosbag2_dynamic_recorder/pause.
Topic management¶
Service |
Type |
Purpose |
|---|---|---|
|
|
Add topics. Types discovered from the graph if not given. |
|
|
Drop topics. Recorded messages are kept. |
|
|
Replace the whole set. |
|
|
The current set. |
|
|
Apply a named topic set. |
|
|
List the configured profiles. |
The guarantee that matters: on set_topics and set_profile, topics present in both the old and
the new set are never torn down. They keep recording without a gap, which is what makes a mode
switch mid-recording safe. Measured, the largest gap on such a topic across two changes is 0.05s,
one publish interval.
return_code is 0 on success and 1 when nothing was actioned or on error. An empty
set_topics is a valid request that unsubscribes everything, and is reported as success. The
reused rosbag2_interfaces services under Recording control keep their own return codes instead.
Selecting by pattern¶
subscribe_topics, unsubscribe_topics and set_topics all take regex and exclude_regex.
The semantics are stock ros2 bag record -e’s: an ECMAScript search, not a full match, so camera
finds /robot/camera/image without anchors. Anchor it yourself (^/robot/) when you mean a prefix.
exclude_regex is applied to the combined selection, after names and matches are merged, so
“everything under /robot except the depth camera” takes one call rather than two.
Three rules:
A pattern never matches the recorder’s own topics. Its event channels are already written into the bag directly, so subscribing to them would record every event twice. Hidden topics are excluded too, as stock rosbag2 does.
unsubscribe_topicsmatches what is being recorded, not the graph. Only a recorded topic can be dropped, and matching the graph would silently do nothing for one that has since left it.An invalid expression is refused with the reason. Quietly matching nothing would be indistinguishable from a pattern that legitimately found nothing, and the likely cause is a typo.
Naming a type explicitly¶
topic_types lines up with topics by index. An empty entry requests discovery from the graph, which
succeeds only when the topic currently offers exactly one type, and cannot work before the publisher
exists. Naming the type is how you record a topic whose publisher has not started yet.
A pattern match has no index, so it cannot carry an explicit type; matched topics always take their type from the graph.
Recording control¶
These use the stock rosbag2_interfaces definitions, so a client written against standard rosbag2
drives this node unchanged.
That holds fully on Rolling, which is the rosbag2 (0.34) this project is written against. Jazzy and
Kilted ship older definitions: Record, Resume and SplitBagfile without the scheduling fields
(Kilted has no Record at all) and Stop without a return code. There the recorder offers those
four under field-identical copies in rosbag2_dynamic_recorder_interfaces, with the same
behaviour, so scheduling works on every distro; ros2 service type ~/resume tells you which is in
use. pause, toggle_paused, is_paused and snapshot are the same on every distro and always
stock. Python clients should take these types from dynrec.services, which resolves them the same
way the recorder does; see Install.
Service |
Type |
Purpose |
|---|---|---|
|
|
Stop writing without tearing down subscriptions. |
|
|
Close the current file and open the next. |
|
|
Flush the in-memory buffer. Requires |
|
|
Close the bag. |
|
|
Open a new bag, restoring the previous topic selection. |
|
|
Everything needed for a status line, in one round trip. |
Stopping is not final: ~/record opens a fresh bag and re-subscribes whatever was being recorded at
stop. Since rosbag2 will not open over an existing bag directory, the new one takes the next free
suffix: mybag, then mybag(1).
Scheduled operations¶
resume, split_bagfile and record accept a future timestamp instead of acting immediately. A
zero stamp means “now”, so an untouched request is already the immediate request.
Node time (
mode: 0, and the only option forrecord) is driven by a timer, so it fires even if the robot has gone quiet.Publish time (
1) and receive time (2) are compared against arriving messages, on any recorded topic or ontracking_topic_namealone. They cannot fire while no messages are arriving, which is useful for “when data resumes” and a trap for “in five minutes”.
A mode outside 0-2, or a tracking_topic_name nobody is recording, is rejected: a schedule keyed
to a topic that is not being recorded would wait forever.
Only the newest schedule of each operation is live, whichever clock either is on: a second
resume replaces the first, a timer or a queued message-time check alike. There is no call to
cancel one; scheduling the same operation again replaces it.
~/stop clears a queued resume or split_bagfile, so it cannot fire against the next bag. A
queued record is not cleared and will still fire.
Whatever is queued, by any client, is listed in the status as schedules, until it fires or a stop
clears it.
Simulation time¶
With use_sim_time:=true the recorder behaves as ros2 bag record --use-sim-time does, and then
some, because everything it writes has to agree with itself:
Messages are stamped with the node clock, driven by
/clock, instead of the middleware’s receive time, so the bag’s timeline is the simulation’s:ros2 bag info, playback anddescribe()all measure in simulated seconds, and a paused simulation produces a paused bag. The send stamp stays the middleware’s wall-clock one, as upstream leaves it. The recorder’s own events were always on the node clock, so the two timelines in one bag now match.Nothing is opened or subscribed until
/clockhas been heard: a node clock reads zero until then, and a bag opened on it would begin in 1970. The recorder is up and answering in the meantime,~/statussayswaiting_for_clock, and a call that needs an open bag, such as adding a topic or~/record, is refused with a message that says why rather than left to time out.Node-time schedules (
resume,split_bagfile,recordwith a future stamp) run on the node clock too, so they keep to simulated time when the simulation runs slow or stops.
Status¶
~/get_status is the one-shot form; ~/status is the same message published periodically, on every
change, and latched, so a client attaching mid-recording sees current state immediately rather than
waiting for the next tick. A UI or a long-lived script should prefer the topic.
A few fields need reading carefully:
messages_missedDetected by watching for gaps in each publisher’s sequence numbers. It does not depend on the transport raising an event, so it catches samples a publisher overwrote in its own history before the recorder ever saw them. Meaningful only when
sequence_numbers_availableis true; treat it as unknown, not zero, when that is false.messages_lost_in_transportWhat the transport reported as dropped before delivery. Observed reading 0 while roughly 3-4% of messages were absent from a bag. Treat it as a floor, not a total. A large
messages_missedwith this at 0 means the recorder was blocked long enough for publisher history to overflow. When this is the figure that climbs, the remedy is on the publisher’s side: its QoS, the network, or load.messages_lost_in_recorderMessages that did reach the recorder and were then dropped by the writer, because the cache filled while the disk could not keep up or because a storage write failed. Known with certainty, and the remedy is local:
max_cache_sizeormax_cache_duration, a lighterstorage_preset_profile, fewer or lighter topics, or faster storage. The name matches the same field in rosbag2’s ownMessagesLostEvent, which~/events/messages_lostalso now fills correctly. Only Rolling’s writer reports these losses; on Jazzy and Kilted this reads 0 and means unknown, not zero.messages_lostThe two above summed, for anything that only wants a total. Inherits the transport caveat.
topic_lossesThe three figures above broken down by topic (
TopicLoss), for every topic that has lost anything in this bag, sorted by name, and empty when nothing has been lost. Counted over the bag rather than the subscription: dropping a topic and adding it back keeps its figures, and~/recordstarts them at zero. The per-topicmessages_missedcarries the same caveat as the total.schedulesResumes, splits and records queued for later and not yet fired (
ScheduledAction), at most one of each, in that order, with the time, the clock it is compared against and the tracking topic. It lists schedules any client set, so a UI shows what a script queued.bag_size_bytesBytes flushed to disk, not bytes captured. The writer caches, so this reads 0 early in a perfectly healthy recording. Use
messages_writtento answer “is it recording?”.write_errorsMessages that reached the recorder but could not be written, for example when the disk filled. Any non-zero value means the bag is incomplete, and the recorder kept running deliberately, since stopping would lose the rest of the recording too.
free_space_bytes,total_space_bytesAvailable and total bytes on the filesystem holding the bag, or 0 when either could not be determined.
min_free_space/min_free_space_percentare compared against the first, so a UI can show how close the recorder is to stopping.stopped_for_low_diskTrue when the recorder stopped itself because free space fell below the configured minimum, rather than because of
~/stop. Cleared when~/recordopens a new bag.max_bag_size,stopped_for_max_bag_sizeThe configured cap on the bag directory in bytes (0 when unset), and whether the recorder stopped itself for exceeding it. Compared against
bag_size_bytes, so it inherits that field’s caveat. Cleared when~/recordopens a new bag.use_sim_time,waiting_for_clockWhether messages are stamped on the
/clock-driven node clock, and whether the recorder is still waiting for that clock’s first message. While it waits no bag is open and every call that needs one is refused with a message saying so; see Simulation time.stamp,recording_started,elapsed_secondsWhen the status was built, when the bag started, and how long it has been recording, all on the node clock. Once stopped,
recording_startedandelapsed_secondsdescribe the last bag and stop advancing;stampkeeps going. A client that needs the recorder’s current time, to count down to a schedule or to turn+30sinto a time, should usestamp.
The remaining fields are literal: recording, paused, snapshot_mode, subscribed_topics,
bag_splits (times the file has rolled over; closing the bag at a stop is not counted) and
messages_written (which counts the recorder’s own event messages too).
active_profile is derived from the live topic set on every publication, not remembered. Record one
extra topic and it goes empty, because no profile is in effect any more. Land exactly on another
profile’s set and it reports that one, whatever command got you there.
Events¶
Topic |
Message |
|
|---|---|---|
|
|
also written into the bag |
|
|
also written into the bag |
|
|
also written into the bag |
|
|
also written into the bag |
|
|
also written into the bag |
|
|
stock; no stamp or cause |
|
|
stock on Rolling, a field-identical copy on Jazzy and Kilted |
Writing the first five into the bag is what makes a gap legible: a gap mid-bag is otherwise indistinguishable from a dropout, a crash or a network fault. The event records what changed, when and why, in-stream and timestamped, and it survives bag splitting.
The first four cover different shapes of gap, and you need all of them:
SubscriptionChangeEventexplains a channel that stops: one topic goes sparse while the others carry on. Disable withrecord_subscription_events:=false.PauseEventexplains a bag that stops: a pause leaves a hole in every topic at once, which is what a crash or a network fault looks like too. The stockrosbag2_interfaces/srv/Pausehas an empty request and response and so can carry no explanation. Disable withrecord_pause_events:=false.LowDiskEventexplains a recording the recorder ended itself: whenmin_free_spaceormin_free_space_percentis set and free space falls below it, the recorder emits this, stops, and closes the bag. Without it the bag would simply end, indistinguishable from a crash or a power loss. Disable the in-bag copy withrecord_low_disk_events:=false.BagSizeLimitEventis the same explanation for the other guard: whenmax_bag_sizeis set and the bag directory grows past it, across every split, the recorder emits this, stops, and closes the bag.max_bagfile_sizeonly rolls to a new file; this is the cap on the recording as a whole. Disable the in-bag copy withrecord_bag_size_limit_events:=false.
FileSplitEvent explains a file boundary rather than a gap: when the bag rolls over, by
~/split_bagfile, a schedule, or max_bagfile_size / max_bagfile_duration, it names the file
that closed, the file recording continues in, and the cause. The stock WriteSplitEvent is still
published for clients written against it, but it carries neither a stamp nor a cause, so it cannot
be placed on a timeline. The in-bag copy goes into the new file; disable it with
record_split_events:=false. Closing the bag at a stop emits none.
While paused no messages are written, but these events still are. An event suppressed by the pause it describes would leave the hole it exists to account for.
Warning
Bag readers return messages in timestamp order, so the recorded order of events is only as monotonic
as the clock was. A backward clock step (NTP correcting, or a virtualised host, where ~120 ms steps
have been measured) reshuffles the events inside it, and a PAUSED can then appear after its
RESUMED. The recorder’s own transition sequence is always correct and no event is lost; only the
recorded order follows the clock. If a sequence fails to alternate, suspect the clock before the
recorder.
Profiles¶
A named topic set, switched in one call. The fleet case it exists for: a supervisor changes the robot’s operating mode and the recorded topic set follows, without interrupting the topics the two modes share.
rosbag2_dynamic_recorder:
ros__parameters:
profile_names: ["idle", "navigation", "inspection"]
profiles:
idle: ["/tf", "/odom", "/imu"]
navigation: ["/tf", "/odom", "/imu", "/scan"]
A worked example using the TurtleBot 4 simulator’s topics ships at
rosbag2_dynamic_recorder/config/profiles.example.yaml. Recording profiles covers
declaring, switching and designing them in full.
Parameters¶
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Output bag path. |
|
|
Storage plugin. |
|
|
Message serialization format. |
|
|
Topics to subscribe at startup. |
|
|
Start with recording paused. |
|
|
Stamp messages on the node clock driven by |
|
|
Buffer in memory, write only on |
|
|
Writer cache in bytes. |
|
|
Writer cache bound in seconds; |
|
|
Split when a file reaches this many bytes; |
|
|
Split every this many seconds; |
|
(empty) |
Storage plugin preset. mcap: |
|
(empty) |
Storage plugin YAML, overlaid on the preset. |
|
|
Write subscription changes into the bag. |
|
|
Write pauses and resumes into the bag. |
|
|
Write a low-disk stop into the bag. |
|
|
Write a bag-size-limit stop into the bag. |
|
|
Write each rollover to a new file into the bag. |
|
|
Stop recording below this many bytes free on the bag filesystem; |
|
|
Same, as a percentage of the filesystem; |
|
|
Stop recording once the bag directory, across every split, exceeds this many bytes; |
|
|
Seconds between free-space and bag-size checks when either limit is set. |
|
|
Seconds between |
|
|
Seconds between |
|
|
Names of the declared profiles. |
The five storage settings after max_cache_size are rosbag2’s own StorageOptions, passed
through untouched. They matter most on a small computer writing to an SD card. When
messages_lost_in_recorder climbs, the disk is not keeping up: max_cache_duration bounds how
much recording is at risk in terms an operator can reason about; storage_preset_profile:=fastwrite
cuts the per-message CPU cost; the zstd_* presets spend CPU to reduce disk bandwidth, which on a
slow card with a spare core is a net gain; and a size or duration split keeps any one file small
enough to survive a crash. With max_cache_size and max_cache_duration both 0 every message is
written synchronously from its callback, which is what makes publishers overwrite their own
history unseen; the recorder warns once at startup if you do that.
These are node parameters. The launch files forward only some of them: uri, storage_id,
serialization_format, topics, start_paused, use_sim_time, snapshot_mode, the six storage settings,
record_subscription_events, messages_lost_report_period, min_free_space,
min_free_space_percent and max_bag_size. Set record_pause_events, record_low_disk_events,
record_bag_size_limit_events, record_split_events, storage_check_period,
status_publish_period, profile_names
and the profiles themselves
through params_file (or -p name:=value when running the node directly). The full argument list
is in Install.
Calling them by hand¶
ros2 run rosbag2_dynamic_recorder dynamic_recorder --ros-args \
-p uri:=/tmp/mybag -p "topics:=['/scan','/odom']"
ros2 service call /rosbag2_dynamic_recorder/subscribe_topics \
rosbag2_dynamic_recorder_interfaces/srv/SubscribeTopics "{topics: ['/diagnostics']}"
ros2 service call /rosbag2_dynamic_recorder/set_topics \
rosbag2_dynamic_recorder_interfaces/srv/SetTopics "{topics: ['/scan','/odom','/tf']}"
The other clients exist so you do not have to call these by hand; ros2 dynrec is the
simplest of them.