From 929b559cda02b41113f8979e2d7e8530659f8495 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 26 Jun 2026 23:17:01 +0100 Subject: [PATCH 1/3] Add rosbag recording task with 10-minute splits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pixi run record` (record_launch.py) records the perception-relevant topics — compressed image, camera_info, tf, and filtered scan — to a timestamped dir under ~/.mote/bags, split into 10-minute segments (--max-bag-duration; override with split:=). Adds the ros2bag CLI verb, the recorder, and the mcap storage plugin to the robot env, which were absent. Co-Authored-By: Claude Opus 4.8 --- mote_bringup/launch/record_launch.py | 53 ++++++++++++++++++++++++++++ pixi.lock | 44 +++++++++++++++++++++++ pixi.toml | 5 +++ 3 files changed, 102 insertions(+) create mode 100644 mote_bringup/launch/record_launch.py diff --git a/mote_bringup/launch/record_launch.py b/mote_bringup/launch/record_launch.py new file mode 100644 index 0000000..e324885 --- /dev/null +++ b/mote_bringup/launch/record_launch.py @@ -0,0 +1,53 @@ +"""Record a rosbag of the topics needed to develop and validate perception. + +Bags are split into fixed-duration segments (default 10 minutes) and written to +~/.mote/bags (per-robot, outside the repo), one timestamped directory per run. +The compressed image stream is recorded rather than raw to keep bag size sane; +republish it to /image_raw on playback with image_transport. +""" + +import os +from datetime import datetime + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, ExecuteProcess +from launch.substitutions import LaunchConfiguration + +TOPICS = [ + "/image_raw/compressed", + "/camera_info", + "/tf", + "/tf_static", + "/scan_filtered", +] + + +def generate_launch_description(): + bags_dir = os.path.expanduser("~/.mote/bags") + os.makedirs(bags_dir, exist_ok=True) + output = os.path.join(bags_dir, datetime.now().strftime("%Y%m%d_%H%M%S")) + + record = ExecuteProcess( + cmd=[ + "ros2", + "bag", + "record", + "--max-bag-duration", + LaunchConfiguration("split"), + "-o", + output, + *TOPICS, + ], + output="screen", + ) + + return LaunchDescription( + [ + DeclareLaunchArgument( + "split", + default_value="600", + description="Seconds per bag segment (rosbag2 --max-bag-duration)", + ), + record, + ] + ) diff --git a/pixi.lock b/pixi.lock index ed66d9d..556edb9 100644 --- a/pixi.lock +++ b/pixi.lock @@ -258,6 +258,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.3-h49c6c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lttng-ust-2.13.9-hf5eda4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py312h3d67a73_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda @@ -548,6 +549,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-joint-state-broadcaster-4.37.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-joint-trajectory-controller-4.37.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-kdl-parser-2.11.0-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-keyboard-handler-0.3.2-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-kinematics-interface-1.7.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-laser-filters-2.0.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-laser-geometry-2.7.2-np2py312h2ed9cc7_16.conda @@ -558,10 +560,12 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-launch-xml-3.4.10-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-launch-yaml-3.4.10-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-libcurl-vendor-3.4.4-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-liblz4-vendor-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-libstatistics-collector-1.7.4-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-libyaml-vendor-1.6.3-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-lifecycle-msgs-2.0.3-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-map-msgs-2.4.1-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-mcap-vendor-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-mecanum-drive-controller-4.37.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-message-filters-4.11.10-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-nav-2d-msgs-1.3.11-np2py312h2ed9cc7_16.conda @@ -638,6 +642,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2-control-4.43.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2-control-test-assets-4.43.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2-controllers-4.37.0-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2bag-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2cli-0.32.8-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2controlcli-4.43.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2launch-0.26.11-np2py312h2ed9cc7_16.conda @@ -648,8 +653,13 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2run-0.32.8-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2service-0.32.8-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2topic-0.32.8-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-compression-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-cpp-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-interfaces-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-py-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-storage-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-storage-mcap-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-transport-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosgraph-msgs-2.0.3-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-adapter-4.6.7-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-cli-4.6.7-np2py312h2ed9cc7_16.conda @@ -729,6 +739,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-xacro-2.1.1-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-yaml-cpp-vendor-9.0.1-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-zstd-image-transport-4.0.6-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-zstd-vendor-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-distro-mutex-0.14.0-jazzy_16.conda - conda: https://prefix.dev/mote/linux-64/scservo-linux-1.0.0-hb0f4dca_0.conda linux-aarch64: @@ -973,6 +984,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-devel-2.15.3-h869d058_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lttng-ust-2.13.9-h8d236e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-4.4.5-py312he78555a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h80f16a2_1002.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py312hd077ced_1.conda @@ -1260,6 +1272,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-joint-state-broadcaster-4.37.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-joint-trajectory-controller-4.37.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-kdl-parser-2.11.0-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-keyboard-handler-0.3.2-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-kinematics-interface-1.7.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-laser-filters-2.0.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-laser-geometry-2.7.2-np2py312h61f2ce4_16.conda @@ -1270,10 +1283,12 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-launch-xml-3.4.10-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-launch-yaml-3.4.10-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-libcurl-vendor-3.4.4-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-liblz4-vendor-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-libstatistics-collector-1.7.4-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-libyaml-vendor-1.6.3-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-lifecycle-msgs-2.0.3-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-map-msgs-2.4.1-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-mcap-vendor-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-mecanum-drive-controller-4.37.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-message-filters-4.11.10-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-nav-2d-msgs-1.3.11-np2py312h61f2ce4_16.conda @@ -1350,6 +1365,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2-control-4.43.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2-control-test-assets-4.43.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2-controllers-4.37.0-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2bag-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2cli-0.32.8-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2controlcli-4.43.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2launch-0.26.11-np2py312h61f2ce4_16.conda @@ -1360,8 +1376,13 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2run-0.32.8-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2service-0.32.8-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2topic-0.32.8-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-compression-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-cpp-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-interfaces-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-py-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-storage-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-storage-mcap-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-transport-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosgraph-msgs-2.0.3-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosidl-adapter-4.6.7-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosidl-cli-4.6.7-np2py312h61f2ce4_16.conda @@ -1441,6 +1462,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-xacro-2.1.1-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-yaml-cpp-vendor-9.0.1-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-zstd-image-transport-4.0.6-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-zstd-vendor-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros2-distro-mutex-0.14.0-jazzy_16.conda - conda: https://prefix.dev/mote/linux-aarch64/scservo-linux-1.0.0-he8cfe8b_0.conda dev: @@ -4325,6 +4347,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-joint-state-broadcaster-4.37.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-joint-trajectory-controller-4.37.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-kdl-parser-2.11.0-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-keyboard-handler-0.3.2-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-kinematics-interface-1.7.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-laser-filters-2.0.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-laser-geometry-2.7.2-np2py312h2ed9cc7_16.conda @@ -4335,10 +4358,12 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-launch-xml-3.4.10-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-launch-yaml-3.4.10-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-libcurl-vendor-3.4.4-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-liblz4-vendor-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-libstatistics-collector-1.7.4-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-libyaml-vendor-1.6.3-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-lifecycle-msgs-2.0.3-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-map-msgs-2.4.1-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-mcap-vendor-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-mecanum-drive-controller-4.37.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-message-filters-4.11.10-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-nav-2d-msgs-1.3.11-np2py312h2ed9cc7_16.conda @@ -4419,6 +4444,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2-control-cmake-0.3.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2-control-test-assets-4.43.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2-controllers-4.37.0-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2bag-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2cli-0.32.8-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2controlcli-4.43.0-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2launch-0.26.11-np2py312h2ed9cc7_16.conda @@ -4429,8 +4455,13 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2run-0.32.8-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2service-0.32.8-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros2topic-0.32.8-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-compression-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-cpp-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-interfaces-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-py-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-storage-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-storage-mcap-0.26.9-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosbag2-transport-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosgraph-msgs-2.0.3-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-adapter-4.6.7-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-cli-4.6.7-np2py312h2ed9cc7_16.conda @@ -4512,6 +4543,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-xacro-2.1.1-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-yaml-cpp-vendor-9.0.1-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-zstd-image-transport-4.0.6-np2py312h2ed9cc7_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-zstd-vendor-0.26.9-np2py312h2ed9cc7_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-distro-mutex-0.14.0-jazzy_16.conda - conda: https://prefix.dev/mote/linux-64/scservo-linux-1.0.0-hb0f4dca_0.conda linux-aarch64: @@ -5184,6 +5216,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-joint-state-broadcaster-4.37.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-joint-trajectory-controller-4.37.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-kdl-parser-2.11.0-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-keyboard-handler-0.3.2-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-kinematics-interface-1.7.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-laser-filters-2.0.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-laser-geometry-2.7.2-np2py312h61f2ce4_16.conda @@ -5194,10 +5227,12 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-launch-xml-3.4.10-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-launch-yaml-3.4.10-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-libcurl-vendor-3.4.4-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-liblz4-vendor-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-libstatistics-collector-1.7.4-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-libyaml-vendor-1.6.3-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-lifecycle-msgs-2.0.3-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-map-msgs-2.4.1-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-mcap-vendor-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-mecanum-drive-controller-4.37.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-message-filters-4.11.10-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-nav-2d-msgs-1.3.11-np2py312h61f2ce4_16.conda @@ -5278,6 +5313,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2-control-cmake-0.3.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2-control-test-assets-4.43.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2-controllers-4.37.0-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2bag-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2cli-0.32.8-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2controlcli-4.43.0-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2launch-0.26.11-np2py312h61f2ce4_16.conda @@ -5288,8 +5324,13 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2run-0.32.8-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2service-0.32.8-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-ros2topic-0.32.8-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-compression-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-cpp-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-interfaces-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-py-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-storage-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-storage-mcap-0.26.9-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosbag2-transport-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosgraph-msgs-2.0.3-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosidl-adapter-4.6.7-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-rosidl-cli-4.6.7-np2py312h61f2ce4_16.conda @@ -5371,6 +5412,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-xacro-2.1.1-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-yaml-cpp-vendor-9.0.1-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-zstd-image-transport-4.0.6-np2py312h61f2ce4_16.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros-jazzy-zstd-vendor-0.26.9-np2py312h61f2ce4_16.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-aarch64/ros2-distro-mutex-0.14.0-jazzy_16.conda - conda: https://prefix.dev/mote/linux-aarch64/scservo-linux-1.0.0-he8cfe8b_0.conda packages: @@ -10709,6 +10751,7 @@ packages: - lz4-c >=1.10.0,<1.11.0a0 license: BSD-3-Clause license_family: BSD + run_exports: {} size: 44154 timestamp: 1765026394687 - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda @@ -18265,6 +18308,7 @@ packages: - lz4-c >=1.10.0,<1.11.0a0 license: BSD-3-Clause license_family: BSD + run_exports: {} size: 51745 timestamp: 1765026442641 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda diff --git a/pixi.toml b/pixi.toml index 97742e0..a89698c 100644 --- a/pixi.toml +++ b/pixi.toml @@ -28,6 +28,7 @@ robot = "ros2 launch mote_bringup robot_launch.py" slam = "ros2 launch mote_bringup slam_launch.py" teleop = "ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -p stamped:=true -r /cmd_vel:=/diff_drive_controller/cmd_vel" perception = "ros2 launch mote_perception perception_launch.py" +record = "ros2 launch mote_bringup record_launch.py" [dependencies] @@ -68,6 +69,10 @@ ros-jazzy-ros2lifecycle = ">=0.18.0" ros-jazzy-ros2run = ">=0.18.0" ros-jazzy-rosbag2-cpp = ">=0.26.0" ros-jazzy-rosbag2-storage = ">=0.26.0" +# `ros2 bag record` CLI verb, recorder, and storage plugins (pixi run record) +ros-jazzy-ros2bag = ">=0.26.0" +ros-jazzy-rosbag2-transport = ">=0.26.0" +ros-jazzy-rosbag2-storage-mcap = ">=0.26.0" ros-jazzy-slam-toolbox = ">=2.8.4,<3" ros-jazzy-teleop-twist-keyboard = ">=1.0.0" ros-jazzy-v4l2-camera = ">=0.7.1,<0.8" From 7a211c34e10bcf45eb588f0cef95f04e4fe8365b Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Sat, 27 Jun 2026 13:57:24 +0100 Subject: [PATCH 2/3] Make recording production-ready: dual streams, disk caps, autostart Replace the single rosbag recorder with two purpose-split streams written under ~/.mote/bags: a light "min" stream (lidar + TF, 10-minute segments) and a heavy "perception" stream (compressed camera + the same lidar/TF, 1-minute segments). Each stream gets its own rolling disk budget via a new bag_pruner that deletes the oldest .mcap segments once the cap is exceeded, so continuous recording never fills the disk. A mote-record systemd service (enabled by install-systemd) records automatically on boot. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BuMPbaua66V8R1KsRUUPYD --- mote_bringup/launch/record_launch.py | 105 ++++++++++++++++++----- mote_bringup/mote_bringup/bag_pruner.py | 87 +++++++++++++++++++ mote_bringup/setup.py | 1 + mote_bringup/systemd/install.sh | 2 +- mote_bringup/systemd/mote-record.service | 16 ++++ 5 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 mote_bringup/mote_bringup/bag_pruner.py create mode 100644 mote_bringup/systemd/mote-record.service diff --git a/mote_bringup/launch/record_launch.py b/mote_bringup/launch/record_launch.py index e324885..9fd21c7 100644 --- a/mote_bringup/launch/record_launch.py +++ b/mote_bringup/launch/record_launch.py @@ -1,9 +1,17 @@ -"""Record a rosbag of the topics needed to develop and validate perception. +"""Record rosbags for perception development, split by purpose with disk caps. -Bags are split into fixed-duration segments (default 10 minutes) and written to -~/.mote/bags (per-robot, outside the repo), one timestamped directory per run. -The compressed image stream is recorded rather than raw to keep bag size sane; -republish it to /image_raw on playback with image_transport. +Two streams are recorded in parallel under ~/.mote/bags (per-robot, outside the +repo), each in its own kind/ run directory: + +- min/ lidar + TF/odometry only, in long 10-minute segments. The + lightweight stream, enough to replay localisation and SLAM. +- perception/ the camera stream (compressed) plus the same lidar + TF, in short + 1-minute segments so a single clip is cheap to copy off the robot. + +bag_pruner gives each stream its own rolling disk budget, deleting the oldest +segments once a stream exceeds its cap so continuous recording never fills the +disk. The compressed image stream is recorded rather than raw; republish it to +/image_raw on playback with image_transport. """ import os @@ -11,43 +19,96 @@ from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, ExecuteProcess +from launch.conditions import IfCondition from launch.substitutions import LaunchConfiguration -TOPICS = [ - "/image_raw/compressed", - "/camera_info", - "/tf", - "/tf_static", - "/scan_filtered", -] +MIN_TOPICS = ["/tf", "/tf_static", "/scan_filtered"] +PERCEPTION_TOPICS = MIN_TOPICS + ["/image_raw/compressed", "/camera_info"] -def generate_launch_description(): - bags_dir = os.path.expanduser("~/.mote/bags") - os.makedirs(bags_dir, exist_ok=True) - output = os.path.join(bags_dir, datetime.now().strftime("%Y%m%d_%H%M%S")) +def _bags_dir(kind): + base = os.path.join(os.path.expanduser("~/.mote/bags"), kind) + os.makedirs(base, exist_ok=True) + return base - record = ExecuteProcess( + +def _record(kind, topics, split, condition=None): + output = os.path.join(_bags_dir(kind), datetime.now().strftime("%Y%m%d_%H%M%S")) + return ExecuteProcess( cmd=[ "ros2", "bag", "record", "--max-bag-duration", - LaunchConfiguration("split"), + split, "-o", output, - *TOPICS, + *topics, + ], + output="screen", + condition=condition, + ) + + +def _pruner(kind, max_gb, condition=None): + return ExecuteProcess( + cmd=[ + "ros2", + "run", + "mote_bringup", + "bag_pruner", + "--dir", + _bags_dir(kind), + "--max-gb", + max_gb, ], output="screen", + condition=condition, ) + +def generate_launch_description(): + perception_enabled = IfCondition(LaunchConfiguration("perception")) + return LaunchDescription( [ DeclareLaunchArgument( - "split", + "min_split", default_value="600", - description="Seconds per bag segment (rosbag2 --max-bag-duration)", + description="Seconds per minimal (lidar+TF) bag segment", + ), + DeclareLaunchArgument( + "perception_split", + default_value="60", + description="Seconds per perception (camera) bag segment", + ), + DeclareLaunchArgument( + "min_max_gb", + default_value="2.0", + description="Rolling disk cap for the minimal stream, in GB", + ), + DeclareLaunchArgument( + "perception_max_gb", + default_value="10.0", + description="Rolling disk cap for the perception stream, in GB", + ), + DeclareLaunchArgument( + "perception", + default_value="true", + description="Also record the camera (perception) stream", + ), + _record("min", MIN_TOPICS, LaunchConfiguration("min_split")), + _pruner("min", LaunchConfiguration("min_max_gb")), + _record( + "perception", + PERCEPTION_TOPICS, + LaunchConfiguration("perception_split"), + condition=perception_enabled, + ), + _pruner( + "perception", + LaunchConfiguration("perception_max_gb"), + condition=perception_enabled, ), - record, ] ) diff --git a/mote_bringup/mote_bringup/bag_pruner.py b/mote_bringup/mote_bringup/bag_pruner.py new file mode 100644 index 0000000..e555677 --- /dev/null +++ b/mote_bringup/mote_bringup/bag_pruner.py @@ -0,0 +1,87 @@ +"""Enforce a rolling disk budget on a rosbag2 output tree. + +Periodically sums the size of every .mcap segment under a directory and deletes +the oldest segments once the total exceeds a cap, so a robot that records +continuously never fills its disk. The newest segment (the one currently being +written) is never deleted. Run directories left empty by pruning are removed +along with their now-stale metadata. + +Each .mcap segment is self-contained, so pruning older segments out of a still +active run does not affect playback of the segments that remain. +""" + +import argparse +import os +import shutil +import time + + +def _segments(root): + files = [] + for dirpath, _dirnames, filenames in os.walk(root): + for name in filenames: + if not name.endswith(".mcap"): + continue + path = os.path.join(dirpath, name) + try: + st = os.stat(path) + except FileNotFoundError: + continue + files.append((path, st.st_mtime, st.st_size)) + files.sort(key=lambda f: f[1]) # oldest first + return files + + +def _remove_empty_runs(root): + for entry in os.scandir(root): + if not entry.is_dir(): + continue + has_mcap = any( + name.endswith(".mcap") + for _d, _sub, names in os.walk(entry.path) + for name in names + ) + if not has_mcap: + shutil.rmtree(entry.path, ignore_errors=True) + print(f"bag_pruner: removed empty run {entry.path}", flush=True) + + +def prune(root, cap_bytes): + segs = _segments(root) + total = sum(s[2] for s in segs) + # Keep the newest segment regardless of the cap — it is still being written. + for path, _mtime, size in segs[:-1]: + if total <= cap_bytes: + break + try: + os.remove(path) + print(f"bag_pruner: removed {path} ({size / 1e6:.0f} MB)", flush=True) + except FileNotFoundError: + pass + total -= size + _remove_empty_runs(root) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dir", required=True, help="rosbag2 output tree to prune") + parser.add_argument( + "--max-gb", type=float, required=True, help="disk cap for the tree, in GB" + ) + parser.add_argument( + "--interval", type=float, default=30.0, help="seconds between sweeps" + ) + args = parser.parse_args() + + cap_bytes = int(args.max_gb * 1e9) + os.makedirs(args.dir, exist_ok=True) + try: + while True: + prune(args.dir, cap_bytes) + time.sleep(args.interval) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/mote_bringup/setup.py b/mote_bringup/setup.py index 7c61e01..4a031c2 100644 --- a/mote_bringup/setup.py +++ b/mote_bringup/setup.py @@ -24,6 +24,7 @@ entry_points={ "console_scripts": [ "odom_tf_relay = mote_bringup.odom_tf_relay:main", + "bag_pruner = mote_bringup.bag_pruner:main", ], }, ) diff --git a/mote_bringup/systemd/install.sh b/mote_bringup/systemd/install.sh index 47cc77c..42fe887 100755 --- a/mote_bringup/systemd/install.sh +++ b/mote_bringup/systemd/install.sh @@ -13,4 +13,4 @@ for unit in "$SRC_DIR"/*.service; do done sudo systemctl daemon-reload -sudo systemctl enable mote-bringup mote-slam mote-nav +sudo systemctl enable mote-bringup mote-slam mote-nav mote-record diff --git a/mote_bringup/systemd/mote-record.service b/mote_bringup/systemd/mote-record.service new file mode 100644 index 0000000..6da2aef --- /dev/null +++ b/mote_bringup/systemd/mote-record.service @@ -0,0 +1,16 @@ +[Unit] +Description=Mote Data Recording +After=mote-bringup.service +Wants=mote-bringup.service + +[Service] +Type=simple +User=@USER@ +WorkingDirectory=@HOME@/Mote +ExecStart=@HOME@/.pixi/bin/pixi run record +Restart=on-failure +RestartSec=5 +Environment=HOME=@HOME@ + +[Install] +WantedBy=multi-user.target From 994c9bb13612e0504c6d94b92cc937616884f8f1 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Mon, 29 Jun 2026 12:58:16 +0100 Subject: [PATCH 3/3] Move to using a config for bags --- mote_bringup/config/record.yaml | 22 ++++++ mote_bringup/launch/record_launch.py | 92 ++++++++----------------- mote_bringup/launch/rviz_launch.py | 30 ++++---- mote_bringup/mote_bringup/bag_pruner.py | 10 +++ 4 files changed, 76 insertions(+), 78 deletions(-) create mode 100644 mote_bringup/config/record.yaml diff --git a/mote_bringup/config/record.yaml b/mote_bringup/config/record.yaml new file mode 100644 index 0000000..51f1701 --- /dev/null +++ b/mote_bringup/config/record.yaml @@ -0,0 +1,22 @@ +# Rosbag recording streams — consumed by record_launch.py. +# Each stream writes to ~/.mote/bags/// with its own segment +# length and rolling disk cap (enforced by bag_pruner). + +streams: + lite: + topics: + - /tf + - /tf_static + - /scan_filtered + split: 600 # seconds per bag segment + max_gb: 2.0 # rolling disk cap + + perception: + topics: + - /tf + - /tf_static + - /scan_filtered + - /image_raw/compressed + - /camera_info + split: 60 + max_gb: 10.0 diff --git a/mote_bringup/launch/record_launch.py b/mote_bringup/launch/record_launch.py index 9fd21c7..6f77bfc 100644 --- a/mote_bringup/launch/record_launch.py +++ b/mote_bringup/launch/record_launch.py @@ -1,29 +1,29 @@ """Record rosbags for perception development, split by purpose with disk caps. -Two streams are recorded in parallel under ~/.mote/bags (per-robot, outside the -repo), each in its own kind/ run directory: +Streams are defined in config/record.yaml. Each enabled stream is recorded in +parallel under ~/.mote/bags// with its own segment length and +rolling disk cap (bag_pruner deletes the oldest segments once a stream exceeds +its cap so continuous recording never fills the disk). -- min/ lidar + TF/odometry only, in long 10-minute segments. The - lightweight stream, enough to replay localisation and SLAM. -- perception/ the camera stream (compressed) plus the same lidar + TF, in short - 1-minute segments so a single clip is cheap to copy off the robot. - -bag_pruner gives each stream its own rolling disk budget, deleting the oldest -segments once a stream exceeds its cap so continuous recording never fills the -disk. The compressed image stream is recorded rather than raw; republish it to +The compressed image stream is recorded rather than raw; republish it to /image_raw on playback with image_transport. """ import os from datetime import datetime +import yaml +from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, ExecuteProcess -from launch.conditions import IfCondition -from launch.substitutions import LaunchConfiguration +from launch.actions import ExecuteProcess + -MIN_TOPICS = ["/tf", "/tf_static", "/scan_filtered"] -PERCEPTION_TOPICS = MIN_TOPICS + ["/image_raw/compressed", "/camera_info"] +def _load_streams(): + config_path = os.path.join( + get_package_share_directory("mote_bringup"), "config", "record.yaml" + ) + with open(config_path) as f: + return yaml.safe_load(f)["streams"] def _bags_dir(kind): @@ -32,7 +32,7 @@ def _bags_dir(kind): return base -def _record(kind, topics, split, condition=None): +def _record(kind, topics, split): output = os.path.join(_bags_dir(kind), datetime.now().strftime("%Y%m%d_%H%M%S")) return ExecuteProcess( cmd=[ @@ -40,17 +40,16 @@ def _record(kind, topics, split, condition=None): "bag", "record", "--max-bag-duration", - split, + str(split), "-o", output, *topics, ], output="screen", - condition=condition, ) -def _pruner(kind, max_gb, condition=None): +def _pruner(kind, max_gb, interval): return ExecuteProcess( cmd=[ "ros2", @@ -60,55 +59,18 @@ def _pruner(kind, max_gb, condition=None): "--dir", _bags_dir(kind), "--max-gb", - max_gb, + str(max_gb), + "--interval", + str(interval), ], output="screen", - condition=condition, ) def generate_launch_description(): - perception_enabled = IfCondition(LaunchConfiguration("perception")) - - return LaunchDescription( - [ - DeclareLaunchArgument( - "min_split", - default_value="600", - description="Seconds per minimal (lidar+TF) bag segment", - ), - DeclareLaunchArgument( - "perception_split", - default_value="60", - description="Seconds per perception (camera) bag segment", - ), - DeclareLaunchArgument( - "min_max_gb", - default_value="2.0", - description="Rolling disk cap for the minimal stream, in GB", - ), - DeclareLaunchArgument( - "perception_max_gb", - default_value="10.0", - description="Rolling disk cap for the perception stream, in GB", - ), - DeclareLaunchArgument( - "perception", - default_value="true", - description="Also record the camera (perception) stream", - ), - _record("min", MIN_TOPICS, LaunchConfiguration("min_split")), - _pruner("min", LaunchConfiguration("min_max_gb")), - _record( - "perception", - PERCEPTION_TOPICS, - LaunchConfiguration("perception_split"), - condition=perception_enabled, - ), - _pruner( - "perception", - LaunchConfiguration("perception_max_gb"), - condition=perception_enabled, - ), - ] - ) + actions = [] + for name, stream in _load_streams().items(): + split = stream["split"] + actions.append(_record(name, stream["topics"], split)) + actions.append(_pruner(name, stream["max_gb"], split)) + return LaunchDescription(actions) diff --git a/mote_bringup/launch/rviz_launch.py b/mote_bringup/launch/rviz_launch.py index f793876..76c8da2 100644 --- a/mote_bringup/launch/rviz_launch.py +++ b/mote_bringup/launch/rviz_launch.py @@ -5,17 +5,21 @@ def generate_launch_description(): - rviz_config = PathJoinSubstitution([ - FindPackageShare("mote_bringup"), - "config", - "mote.rviz", - ]) + rviz_config = PathJoinSubstitution( + [ + FindPackageShare("mote_bringup"), + "config", + "mote.rviz", + ] + ) - return LaunchDescription([ - Node( - package="rviz2", - executable="rviz2", - arguments=["-d", rviz_config], - output="screen", - ) - ]) + return LaunchDescription( + [ + Node( + package="rviz2", + executable="rviz2", + arguments=["-d", rviz_config], + output="screen", + ) + ] + ) diff --git a/mote_bringup/mote_bringup/bag_pruner.py b/mote_bringup/mote_bringup/bag_pruner.py index e555677..8be7a5c 100644 --- a/mote_bringup/mote_bringup/bag_pruner.py +++ b/mote_bringup/mote_bringup/bag_pruner.py @@ -16,6 +16,9 @@ import time +EMPTY_RUN_MIN_AGE_SECONDS = 120.0 + + def _segments(root): files = [] for dirpath, _dirnames, filenames in os.walk(root): @@ -33,9 +36,16 @@ def _segments(root): def _remove_empty_runs(root): + now = time.time() for entry in os.scandir(root): if not entry.is_dir(): continue + try: + age = now - entry.stat().st_mtime + except FileNotFoundError: + continue + if age < EMPTY_RUN_MIN_AGE_SECONDS: + continue has_mcap = any( name.endswith(".mcap") for _d, _sub, names in os.walk(entry.path)