pymcap-cli 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/PKG-INFO +63 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/README.md +61 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/pyproject.toml +2 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cli.py +24 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/_run_processor.py +2 -2
- pymcap_cli-0.5.0/src/pymcap_cli/cmd/bag2mcap_cmd.py +285 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/cat_cmd.py +1 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/compress_cmd.py +2 -2
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/convert_cmd.py +3 -3
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/diag_cmd.py +99 -16
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/du_cmd.py +3 -3
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/filter_cmd.py +2 -2
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/info_cmd.py +5 -5
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/list_cmd.py +2 -2
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/merge_cmd.py +2 -2
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/plot_cmd.py +1 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/process_cmd.py +2 -2
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/rechunk_cmd.py +2 -2
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/records_cmd.py +1 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/recover_cmd.py +2 -2
- pymcap_cli-0.5.0/src/pymcap_cli/cmd/roscompress_cmd.py +806 -0
- pymcap_cli-0.5.0/src/pymcap_cli/cmd/rosdecompress_cmd.py +321 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/tftree_cmd.py +1 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +1 -1
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/video_cmd.py +8 -6
- {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/core}/mcap_processor.py +2 -2
- pymcap_cli-0.5.0/src/pymcap_cli/core/mcap_transform.py +138 -0
- pymcap_cli-0.5.0/src/pymcap_cli/core/msg_resolver.py +447 -0
- pymcap_cli-0.5.0/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/display}/display_utils.py +1 -1
- pymcap_cli-0.5.0/src/pymcap_cli/display/sparkline.py +75 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/__init__.py +0 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/decompress.py +213 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/encoder_common.py +275 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/pointcloud.py +212 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/video_factory.py +47 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/video_ffmpeg.py +793 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/video_protocols.py +40 -0
- pymcap_cli-0.5.0/src/pymcap_cli/encoding/video_pyav.py +372 -0
- pymcap_cli-0.5.0/src/pymcap_cli/py.typed +0 -0
- pymcap_cli-0.5.0/src/pymcap_cli/rosbag_reader/__init__.py +16 -0
- pymcap_cli-0.5.0/src/pymcap_cli/rosbag_reader/_reader.py +340 -0
- pymcap_cli-0.5.0/src/pymcap_cli/rosbag_reader/_types.py +48 -0
- pymcap_cli-0.5.0/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- pymcap_cli-0.5.0/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.4.0/src/pymcap_cli/cmd → pymcap_cli-0.5.0/src/pymcap_cli/types}/info_data.py +1 -1
- {pymcap_cli-0.4.0/src/pymcap_cli/cmd → pymcap_cli-0.5.0/src/pymcap_cli/types}/info_link.py +1 -1
- pymcap_cli-0.5.0/src/pymcap_cli/types/info_types.py +514 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/utils.py +1 -1
- pymcap_cli-0.4.0/src/pymcap_cli/cmd/roscompress_cmd.py +0 -618
- pymcap_cli-0.4.0/src/pymcap_cli/image_utils.py +0 -612
- pymcap_cli-0.4.0/src/pymcap_cli/msg_resolver.py +0 -284
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
- /pymcap_cli-0.4.0/src/pymcap_cli/py.typed → /pymcap_cli-0.5.0/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/core}/input_handler.py +0 -0
- {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/core}/processors.py +0 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/display}/osc_utils.py +0 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.4.0 → pymcap_cli-0.5.0}/src/pymcap_cli/info_types.py +0 -0
- {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/types}/types_manual.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pymcap-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: High-performance Python CLI for MCAP file processing with advanced recovery, filtering, and optimization capabilities
|
|
5
5
|
Keywords: mcap,cli,robotics,ros,ros2,recovery,filtering,compression
|
|
6
6
|
Author: Marko Bausch
|
|
@@ -26,6 +26,7 @@ Requires-Dist: mcap-ros2-support-fast
|
|
|
26
26
|
Requires-Dist: cyclopts>=4
|
|
27
27
|
Requires-Dist: ros-parser
|
|
28
28
|
Requires-Dist: platformdirs>=4.0.0
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
30
|
Requires-Dist: typing-extensions>=4.15.0
|
|
30
31
|
Requires-Dist: pymcap-cli[video,pointcloud,plot] ; extra == 'all'
|
|
31
32
|
Requires-Dist: plotly>=6.0.0 ; extra == 'plot'
|
|
@@ -133,6 +134,13 @@ pymcap-cli cat recording.mcap --query '/detections.objects[:]{confidence>0.8}'
|
|
|
133
134
|
|
|
134
135
|
# Pipe to file as JSONL
|
|
135
136
|
pymcap-cli cat recording.mcap > messages.jsonl
|
|
137
|
+
|
|
138
|
+
# Write to file with progress bar
|
|
139
|
+
pymcap-cli cat recording.mcap -o messages.jsonl
|
|
140
|
+
|
|
141
|
+
# Control binary field serialization
|
|
142
|
+
pymcap-cli cat recording.mcap --bytes base64 # base64-encoded
|
|
143
|
+
pymcap-cli cat recording.mcap --bytes skip # omit binary fields
|
|
136
144
|
```
|
|
137
145
|
|
|
138
146
|
### `tftree` — TF Transform Tree
|
|
@@ -153,6 +161,45 @@ pymcap-cli tftree data.mcap
|
|
|
153
161
|
pymcap-cli tftree data.mcap --static-only
|
|
154
162
|
```
|
|
155
163
|
|
|
164
|
+
### `diag` — ROS2 Diagnostics
|
|
165
|
+
|
|
166
|
+
Inspect ROS2 diagnostics with per-component health overview, sparkline timelines, frequency stats, and time-in-state tracking.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Show components with issues (WARN/ERROR/STALE)
|
|
170
|
+
pymcap-cli diag recording.mcap
|
|
171
|
+
|
|
172
|
+
# Show all components including OK
|
|
173
|
+
pymcap-cli diag recording.mcap --all
|
|
174
|
+
|
|
175
|
+
# Detailed inspection of specific components
|
|
176
|
+
pymcap-cli diag recording.mcap --inspect "encoder"
|
|
177
|
+
|
|
178
|
+
# Hierarchical tree view
|
|
179
|
+
pymcap-cli diag recording.mcap --tree
|
|
180
|
+
|
|
181
|
+
# JSON output for scripting
|
|
182
|
+
pymcap-cli diag recording.mcap --json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `plot` — Time-Series Visualization
|
|
186
|
+
|
|
187
|
+
Plot message fields over time using Plotly. Supports named labels, LTTB downsampling, XY trajectory mode, and saves to interactive HTML. Requires the `plot` extra (`uv add pymcap-cli[plot]`).
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Plot a single field
|
|
191
|
+
pymcap-cli plot recording.mcap /odom.pose.position.x
|
|
192
|
+
|
|
193
|
+
# Named series
|
|
194
|
+
pymcap-cli plot recording.mcap "Vel X=/odom.twist.twist.linear.x"
|
|
195
|
+
|
|
196
|
+
# XY trajectory plot
|
|
197
|
+
pymcap-cli plot recording.mcap --xy /odom.pose.position.x /odom.pose.position.y
|
|
198
|
+
|
|
199
|
+
# Downsample to 1000 points and save to file
|
|
200
|
+
pymcap-cli plot recording.mcap /odom.pose.position.x -d 1000 -o plot.html
|
|
201
|
+
```
|
|
202
|
+
|
|
156
203
|
### `process` — Unified Processing
|
|
157
204
|
|
|
158
205
|
The most powerful command — combines recovery, filtering, and optimization in a single pass.
|
|
@@ -315,6 +362,21 @@ pymcap-cli roscompress data.mcap -o compressed.mcap
|
|
|
315
362
|
pymcap-cli roscompress data.mcap -o compressed.mcap --quality 28 --codec h265
|
|
316
363
|
```
|
|
317
364
|
|
|
365
|
+
### `rosdecompress` — ROS Decompression
|
|
366
|
+
|
|
367
|
+
Decompress CompressedVideo and CompressedPointCloud2 topics back to standard ROS formats. Requires the `video` extra.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# Decompress to CompressedImage (JPEG)
|
|
371
|
+
pymcap-cli rosdecompress input.mcap output.mcap
|
|
372
|
+
|
|
373
|
+
# Decompress to raw Image
|
|
374
|
+
pymcap-cli rosdecompress input.mcap output.mcap --video-format raw
|
|
375
|
+
|
|
376
|
+
# Skip point cloud decompression
|
|
377
|
+
pymcap-cli rosdecompress input.mcap output.mcap --no-pointcloud
|
|
378
|
+
```
|
|
379
|
+
|
|
318
380
|
### Shell Autocompletion
|
|
319
381
|
|
|
320
382
|
```bash
|
|
@@ -89,6 +89,13 @@ pymcap-cli cat recording.mcap --query '/detections.objects[:]{confidence>0.8}'
|
|
|
89
89
|
|
|
90
90
|
# Pipe to file as JSONL
|
|
91
91
|
pymcap-cli cat recording.mcap > messages.jsonl
|
|
92
|
+
|
|
93
|
+
# Write to file with progress bar
|
|
94
|
+
pymcap-cli cat recording.mcap -o messages.jsonl
|
|
95
|
+
|
|
96
|
+
# Control binary field serialization
|
|
97
|
+
pymcap-cli cat recording.mcap --bytes base64 # base64-encoded
|
|
98
|
+
pymcap-cli cat recording.mcap --bytes skip # omit binary fields
|
|
92
99
|
```
|
|
93
100
|
|
|
94
101
|
### `tftree` — TF Transform Tree
|
|
@@ -109,6 +116,45 @@ pymcap-cli tftree data.mcap
|
|
|
109
116
|
pymcap-cli tftree data.mcap --static-only
|
|
110
117
|
```
|
|
111
118
|
|
|
119
|
+
### `diag` — ROS2 Diagnostics
|
|
120
|
+
|
|
121
|
+
Inspect ROS2 diagnostics with per-component health overview, sparkline timelines, frequency stats, and time-in-state tracking.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Show components with issues (WARN/ERROR/STALE)
|
|
125
|
+
pymcap-cli diag recording.mcap
|
|
126
|
+
|
|
127
|
+
# Show all components including OK
|
|
128
|
+
pymcap-cli diag recording.mcap --all
|
|
129
|
+
|
|
130
|
+
# Detailed inspection of specific components
|
|
131
|
+
pymcap-cli diag recording.mcap --inspect "encoder"
|
|
132
|
+
|
|
133
|
+
# Hierarchical tree view
|
|
134
|
+
pymcap-cli diag recording.mcap --tree
|
|
135
|
+
|
|
136
|
+
# JSON output for scripting
|
|
137
|
+
pymcap-cli diag recording.mcap --json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `plot` — Time-Series Visualization
|
|
141
|
+
|
|
142
|
+
Plot message fields over time using Plotly. Supports named labels, LTTB downsampling, XY trajectory mode, and saves to interactive HTML. Requires the `plot` extra (`uv add pymcap-cli[plot]`).
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Plot a single field
|
|
146
|
+
pymcap-cli plot recording.mcap /odom.pose.position.x
|
|
147
|
+
|
|
148
|
+
# Named series
|
|
149
|
+
pymcap-cli plot recording.mcap "Vel X=/odom.twist.twist.linear.x"
|
|
150
|
+
|
|
151
|
+
# XY trajectory plot
|
|
152
|
+
pymcap-cli plot recording.mcap --xy /odom.pose.position.x /odom.pose.position.y
|
|
153
|
+
|
|
154
|
+
# Downsample to 1000 points and save to file
|
|
155
|
+
pymcap-cli plot recording.mcap /odom.pose.position.x -d 1000 -o plot.html
|
|
156
|
+
```
|
|
157
|
+
|
|
112
158
|
### `process` — Unified Processing
|
|
113
159
|
|
|
114
160
|
The most powerful command — combines recovery, filtering, and optimization in a single pass.
|
|
@@ -271,6 +317,21 @@ pymcap-cli roscompress data.mcap -o compressed.mcap
|
|
|
271
317
|
pymcap-cli roscompress data.mcap -o compressed.mcap --quality 28 --codec h265
|
|
272
318
|
```
|
|
273
319
|
|
|
320
|
+
### `rosdecompress` — ROS Decompression
|
|
321
|
+
|
|
322
|
+
Decompress CompressedVideo and CompressedPointCloud2 topics back to standard ROS formats. Requires the `video` extra.
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
# Decompress to CompressedImage (JPEG)
|
|
326
|
+
pymcap-cli rosdecompress input.mcap output.mcap
|
|
327
|
+
|
|
328
|
+
# Decompress to raw Image
|
|
329
|
+
pymcap-cli rosdecompress input.mcap output.mcap --video-format raw
|
|
330
|
+
|
|
331
|
+
# Skip point cloud decompression
|
|
332
|
+
pymcap-cli rosdecompress input.mcap output.mcap --no-pointcloud
|
|
333
|
+
```
|
|
334
|
+
|
|
274
335
|
### Shell Autocompletion
|
|
275
336
|
|
|
276
337
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pymcap-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "High-performance Python CLI for MCAP file processing with advanced recovery, filtering, and optimization capabilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
"cyclopts>=4",
|
|
34
34
|
"ros-parser",
|
|
35
35
|
"platformdirs>=4.0.0",
|
|
36
|
+
"pyyaml>=6.0",
|
|
36
37
|
"typing-extensions>=4.15.0",
|
|
37
38
|
]
|
|
38
39
|
|
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
from cyclopts import App, Group
|
|
6
6
|
|
|
7
7
|
from pymcap_cli.cmd import (
|
|
8
|
+
bag2mcap_cmd,
|
|
8
9
|
cat_cmd,
|
|
9
10
|
compress_cmd,
|
|
10
11
|
convert_cmd,
|
|
@@ -87,6 +88,27 @@ except ImportError:
|
|
|
87
88
|
return 1
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
try:
|
|
92
|
+
from pymcap_cli.cmd.rosdecompress_cmd import rosdecompress
|
|
93
|
+
except ImportError:
|
|
94
|
+
|
|
95
|
+
def rosdecompress() -> int:
|
|
96
|
+
"""ROS decompress command is unavailable because the 'av' package is not installed.
|
|
97
|
+
|
|
98
|
+
To enable rosdecompress functionality, please install pymcap-cli with the 'video' extra:
|
|
99
|
+
|
|
100
|
+
uv add --group video pymcap-cli
|
|
101
|
+
"""
|
|
102
|
+
print( # noqa: T201
|
|
103
|
+
"Error:\n"
|
|
104
|
+
"ROS decompress command is unavailable because the 'av' package is not installed.\n"
|
|
105
|
+
"To enable this functionality, please install pymcap-cli with the 'video' extra:\n\n"
|
|
106
|
+
" uv add --group video pymcap-cli\n",
|
|
107
|
+
file=sys.stderr,
|
|
108
|
+
)
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
|
|
90
112
|
app = App(
|
|
91
113
|
name="pymcap-cli",
|
|
92
114
|
help="CLI tool for slicing and dicing MCAP files.",
|
|
@@ -109,6 +131,7 @@ app.command(name="tftree", group=inspect_group)(tftree_cmd.tftree)
|
|
|
109
131
|
app.command(name="topic-chunks", group=inspect_group)(topic_chunks_cmd.topic_chunks)
|
|
110
132
|
|
|
111
133
|
# Transform commands — convert, filter, or produce new files
|
|
134
|
+
app.command(name="bag2mcap", group=transform_group)(bag2mcap_cmd.bag2mcap)
|
|
112
135
|
app.command(name="compress", group=transform_group)(compress_cmd.compress)
|
|
113
136
|
app.command(name="convert", group=transform_group)(convert_cmd.convert)
|
|
114
137
|
app.command(name="filter", group=transform_group)(filter_cmd.filter_cmd)
|
|
@@ -119,6 +142,7 @@ app.command(name="recover", group=transform_group)(recover_cmd.recover)
|
|
|
119
142
|
app.command(name="recover-inplace", group=transform_group)(recover_inplace_cmd.recover_inplace)
|
|
120
143
|
app.command(name="plot", group=inspect_group)(plot)
|
|
121
144
|
app.command(name="roscompress", group=transform_group)(roscompress)
|
|
145
|
+
app.command(name="rosdecompress", group=transform_group)(rosdecompress)
|
|
122
146
|
app.command(name="video", group=transform_group)(video)
|
|
123
147
|
|
|
124
148
|
|
|
@@ -4,8 +4,8 @@ import contextlib
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from pymcap_cli.input_handler import open_input
|
|
8
|
-
from pymcap_cli.mcap_processor import (
|
|
7
|
+
from pymcap_cli.core.input_handler import open_input
|
|
8
|
+
from pymcap_cli.core.mcap_processor import (
|
|
9
9
|
InputFile,
|
|
10
10
|
InputOptions,
|
|
11
11
|
McapProcessor,
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Convert ROS1 bag files to MCAP format (ros1 profile)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import BinaryIO
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
BarColumn,
|
|
14
|
+
Progress,
|
|
15
|
+
SpinnerColumn,
|
|
16
|
+
TaskProgressColumn,
|
|
17
|
+
TextColumn,
|
|
18
|
+
TimeRemainingColumn,
|
|
19
|
+
)
|
|
20
|
+
from small_mcap.writer import CompressionType, McapWriter
|
|
21
|
+
from small_mcap.writer import CompressionType as WriterCompressionType
|
|
22
|
+
|
|
23
|
+
from pymcap_cli.display.osc_utils import OSCProgressColumn
|
|
24
|
+
from pymcap_cli.types.types_manual import (
|
|
25
|
+
DEFAULT_CHUNK_SIZE,
|
|
26
|
+
DEFAULT_COMPRESSION,
|
|
27
|
+
ChunkSizeOption,
|
|
28
|
+
CompressionOption,
|
|
29
|
+
ForceOverwriteOption,
|
|
30
|
+
OutputPathOption,
|
|
31
|
+
)
|
|
32
|
+
from pymcap_cli.utils import confirm_output_overwrite
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Bag2McapOptions:
|
|
39
|
+
"""Options for bag to MCAP conversion."""
|
|
40
|
+
|
|
41
|
+
chunk_size: int = 1024 * 1024 * 8 # 8MB default
|
|
42
|
+
compression: CompressionType = CompressionType.ZSTD
|
|
43
|
+
enable_crcs: bool = True
|
|
44
|
+
use_chunking: bool = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Bag2McapStatistics:
|
|
49
|
+
"""Statistics from bag to MCAP conversion."""
|
|
50
|
+
|
|
51
|
+
topic_count: int
|
|
52
|
+
message_count: int
|
|
53
|
+
schema_count: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def convert_bag_to_mcap(
|
|
57
|
+
bag_path: Path,
|
|
58
|
+
output: BinaryIO,
|
|
59
|
+
options: Bag2McapOptions,
|
|
60
|
+
) -> Bag2McapStatistics:
|
|
61
|
+
"""Convert a ROS1 bag file to MCAP format with ros1 profile.
|
|
62
|
+
|
|
63
|
+
Messages are passed through as raw ROS1-serialized bytes.
|
|
64
|
+
Schema encoding is "ros1msg", message encoding is "ros1".
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
bag_path: Path to the input .bag file.
|
|
68
|
+
output: Output stream for MCAP data.
|
|
69
|
+
options: Conversion options.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Conversion statistics.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
from pymcap_cli.rosbag_reader import read_bag_info, read_bag_messages # noqa: PLC0415
|
|
76
|
+
|
|
77
|
+
with bag_path.open("rb") as bag_file:
|
|
78
|
+
info = read_bag_info(bag_file)
|
|
79
|
+
|
|
80
|
+
if not info.connections:
|
|
81
|
+
logger.warning("No connections found in bag file")
|
|
82
|
+
writer = McapWriter(
|
|
83
|
+
output,
|
|
84
|
+
chunk_size=options.chunk_size,
|
|
85
|
+
compression=options.compression,
|
|
86
|
+
enable_crcs=options.enable_crcs,
|
|
87
|
+
use_chunking=options.use_chunking,
|
|
88
|
+
)
|
|
89
|
+
writer.start(profile="ros1")
|
|
90
|
+
writer.finish()
|
|
91
|
+
return Bag2McapStatistics(topic_count=0, message_count=0, schema_count=0)
|
|
92
|
+
|
|
93
|
+
logger.info(f"Found {len(info.connections)} connections, {info.message_count} messages")
|
|
94
|
+
|
|
95
|
+
# Build schema map: deduplicate by msg_type
|
|
96
|
+
schema_map: dict[str, int] = {} # msg_type -> schema_id
|
|
97
|
+
schema_definitions: dict[int, tuple[str, str]] = {} # schema_id -> (msg_type, definition)
|
|
98
|
+
next_schema_id = 1
|
|
99
|
+
|
|
100
|
+
for conn in info.connections.values():
|
|
101
|
+
if conn.msg_type not in schema_map:
|
|
102
|
+
schema_id = next_schema_id
|
|
103
|
+
next_schema_id += 1
|
|
104
|
+
schema_map[conn.msg_type] = schema_id
|
|
105
|
+
schema_definitions[schema_id] = (conn.msg_type, conn.message_definition)
|
|
106
|
+
|
|
107
|
+
# Build channel map: deduplicate by (topic, msg_type)
|
|
108
|
+
# Multiple connections can exist for the same topic (multiple publishers)
|
|
109
|
+
channel_map: dict[tuple[str, str], int] = {} # (topic, msg_type) -> channel_id
|
|
110
|
+
conn_to_channel: dict[int, int] = {} # conn_id -> channel_id
|
|
111
|
+
next_channel_id = 1
|
|
112
|
+
|
|
113
|
+
for conn in info.connections.values():
|
|
114
|
+
key = (conn.topic, conn.msg_type)
|
|
115
|
+
if key not in channel_map:
|
|
116
|
+
channel_map[key] = next_channel_id
|
|
117
|
+
next_channel_id += 1
|
|
118
|
+
conn_to_channel[conn.conn_id] = channel_map[key]
|
|
119
|
+
|
|
120
|
+
# Create MCAP writer
|
|
121
|
+
writer = McapWriter(
|
|
122
|
+
output,
|
|
123
|
+
chunk_size=options.chunk_size,
|
|
124
|
+
compression=options.compression,
|
|
125
|
+
enable_crcs=options.enable_crcs,
|
|
126
|
+
use_chunking=options.use_chunking,
|
|
127
|
+
)
|
|
128
|
+
writer.start(profile="ros1")
|
|
129
|
+
|
|
130
|
+
# Write schemas
|
|
131
|
+
for schema_id, (msg_type, definition) in schema_definitions.items():
|
|
132
|
+
writer.add_schema(
|
|
133
|
+
schema_id=schema_id,
|
|
134
|
+
name=msg_type,
|
|
135
|
+
encoding="ros1msg",
|
|
136
|
+
data=definition.encode("utf-8"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Write channels
|
|
140
|
+
for (topic, msg_type), channel_id in channel_map.items():
|
|
141
|
+
schema_id = schema_map[msg_type]
|
|
142
|
+
|
|
143
|
+
# Collect metadata from connections for this channel
|
|
144
|
+
metadata: dict[str, str] = {}
|
|
145
|
+
for conn in info.connections.values():
|
|
146
|
+
if conn.topic == topic and conn.msg_type == msg_type:
|
|
147
|
+
metadata["md5sum"] = conn.md5sum
|
|
148
|
+
if conn.callerid:
|
|
149
|
+
metadata["callerid"] = conn.callerid
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
writer.add_channel(
|
|
153
|
+
channel_id=channel_id,
|
|
154
|
+
topic=topic,
|
|
155
|
+
message_encoding="ros1",
|
|
156
|
+
schema_id=schema_id,
|
|
157
|
+
metadata=metadata,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Read and write messages
|
|
161
|
+
logger.info("Converting messages...")
|
|
162
|
+
sequence_counters: defaultdict[int, int] = defaultdict(int)
|
|
163
|
+
message_count = 0
|
|
164
|
+
|
|
165
|
+
with (
|
|
166
|
+
bag_path.open("rb") as bag_file,
|
|
167
|
+
Progress(
|
|
168
|
+
SpinnerColumn(),
|
|
169
|
+
TextColumn("[bold blue]{task.description}"),
|
|
170
|
+
BarColumn(),
|
|
171
|
+
TaskProgressColumn(),
|
|
172
|
+
TimeRemainingColumn(),
|
|
173
|
+
OSCProgressColumn(title="Converting messages"),
|
|
174
|
+
transient=False,
|
|
175
|
+
) as progress,
|
|
176
|
+
):
|
|
177
|
+
task = progress.add_task(
|
|
178
|
+
"[cyan]Converting messages...",
|
|
179
|
+
total=info.message_count if info.message_count > 0 else None,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
for msg in read_bag_messages(bag_file, info):
|
|
183
|
+
channel_id = conn_to_channel[msg.conn_id]
|
|
184
|
+
|
|
185
|
+
writer.add_message(
|
|
186
|
+
channel_id=channel_id,
|
|
187
|
+
log_time=msg.time_ns,
|
|
188
|
+
data=msg.data,
|
|
189
|
+
publish_time=msg.time_ns,
|
|
190
|
+
sequence=sequence_counters[channel_id],
|
|
191
|
+
)
|
|
192
|
+
sequence_counters[channel_id] += 1
|
|
193
|
+
message_count += 1
|
|
194
|
+
progress.advance(task)
|
|
195
|
+
|
|
196
|
+
writer.finish()
|
|
197
|
+
|
|
198
|
+
logger.info(
|
|
199
|
+
f"Conversion complete: {len(channel_map)} topics, "
|
|
200
|
+
f"{message_count:,} messages, {len(schema_definitions)} schemas"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return Bag2McapStatistics(
|
|
204
|
+
topic_count=len(channel_map),
|
|
205
|
+
message_count=message_count,
|
|
206
|
+
schema_count=len(schema_definitions),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
console = Console()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def bag2mcap(
|
|
214
|
+
file: str,
|
|
215
|
+
output: OutputPathOption,
|
|
216
|
+
*,
|
|
217
|
+
chunk_size: ChunkSizeOption = DEFAULT_CHUNK_SIZE,
|
|
218
|
+
compression: CompressionOption = DEFAULT_COMPRESSION,
|
|
219
|
+
force: ForceOverwriteOption = False,
|
|
220
|
+
) -> int:
|
|
221
|
+
"""Convert ROS1 bag files to MCAP format.
|
|
222
|
+
|
|
223
|
+
Converts ROS1 bag files to MCAP with ros1 profile, preserving all
|
|
224
|
+
message data as raw ROS1-serialized bytes. Schemas use ros1msg encoding
|
|
225
|
+
with the full message definition from the bag file.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
file
|
|
230
|
+
Path to the ROS1 .bag file to convert.
|
|
231
|
+
output
|
|
232
|
+
Output MCAP filename.
|
|
233
|
+
chunk_size
|
|
234
|
+
Chunk size of output file in bytes.
|
|
235
|
+
compression
|
|
236
|
+
Compression algorithm for output file.
|
|
237
|
+
force
|
|
238
|
+
Force overwrite of output file without confirmation.
|
|
239
|
+
|
|
240
|
+
Examples
|
|
241
|
+
--------
|
|
242
|
+
```
|
|
243
|
+
# Basic conversion
|
|
244
|
+
pymcap-cli bag2mcap recording.bag -o recording.mcap
|
|
245
|
+
|
|
246
|
+
# With custom compression
|
|
247
|
+
pymcap-cli bag2mcap recording.bag -o recording.mcap --compression lz4
|
|
248
|
+
```
|
|
249
|
+
"""
|
|
250
|
+
input_path = Path(file)
|
|
251
|
+
if not input_path.exists():
|
|
252
|
+
console.print(f"[red]Error: Input file '{file}' does not exist[/red]")
|
|
253
|
+
return 1
|
|
254
|
+
|
|
255
|
+
confirm_output_overwrite(output, force)
|
|
256
|
+
|
|
257
|
+
compression_map = {
|
|
258
|
+
"zstd": WriterCompressionType.ZSTD,
|
|
259
|
+
"lz4": WriterCompressionType.LZ4,
|
|
260
|
+
"none": WriterCompressionType.NONE,
|
|
261
|
+
}
|
|
262
|
+
writer_compression = compression_map[compression.value]
|
|
263
|
+
|
|
264
|
+
options = Bag2McapOptions(
|
|
265
|
+
chunk_size=chunk_size,
|
|
266
|
+
compression=writer_compression,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
console.print(f"[blue]Converting '{file}' to '{output}'[/blue]")
|
|
270
|
+
|
|
271
|
+
with output.open("wb") as output_stream:
|
|
272
|
+
try:
|
|
273
|
+
stats = convert_bag_to_mcap(input_path, output_stream, options)
|
|
274
|
+
|
|
275
|
+
console.print("[green]Conversion completed successfully[/green]")
|
|
276
|
+
console.print(
|
|
277
|
+
f"Converted {stats.topic_count} topics, "
|
|
278
|
+
f"{stats.message_count:,} messages, "
|
|
279
|
+
f"{stats.schema_count} schemas"
|
|
280
|
+
)
|
|
281
|
+
except Exception as e: # noqa: BLE001
|
|
282
|
+
console.print(f"[red]Error during conversion: {e}[/red]")
|
|
283
|
+
return 1
|
|
284
|
+
|
|
285
|
+
return 0
|
|
@@ -25,7 +25,7 @@ from small_mcap import JSONDecoderFactory
|
|
|
25
25
|
from small_mcap.reader import read_message_decoded
|
|
26
26
|
from small_mcap.records import Channel
|
|
27
27
|
|
|
28
|
-
from pymcap_cli.input_handler import open_input
|
|
28
|
+
from pymcap_cli.core.input_handler import open_input
|
|
29
29
|
from pymcap_cli.utils import MAX_INT64, ProgressTrackingIO, file_progress, parse_timestamp_args
|
|
30
30
|
|
|
31
31
|
console_err = Console(stderr=True)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
|
|
5
5
|
from pymcap_cli.cmd._run_processor import run_processor
|
|
6
|
-
from pymcap_cli.mcap_processor import InputOptions, OutputOptions
|
|
7
|
-
from pymcap_cli.types_manual import (
|
|
6
|
+
from pymcap_cli.core.mcap_processor import InputOptions, OutputOptions
|
|
7
|
+
from pymcap_cli.types.types_manual import (
|
|
8
8
|
DEFAULT_CHUNK_SIZE,
|
|
9
9
|
DEFAULT_COMPRESSION,
|
|
10
10
|
ChunkSizeOption,
|
|
@@ -21,9 +21,9 @@ from rich.progress import (
|
|
|
21
21
|
from small_mcap.writer import CompressionType, McapWriter
|
|
22
22
|
from small_mcap.writer import CompressionType as WriterCompressionType
|
|
23
23
|
|
|
24
|
-
from pymcap_cli.msg_resolver import ROS2Distro, get_message_definition
|
|
25
|
-
from pymcap_cli.osc_utils import OSCProgressColumn
|
|
26
|
-
from pymcap_cli.types_manual import (
|
|
24
|
+
from pymcap_cli.core.msg_resolver import ROS2Distro, get_message_definition
|
|
25
|
+
from pymcap_cli.display.osc_utils import OSCProgressColumn
|
|
26
|
+
from pymcap_cli.types.types_manual import (
|
|
27
27
|
DEFAULT_CHUNK_SIZE,
|
|
28
28
|
DEFAULT_COMPRESSION,
|
|
29
29
|
ChunkSizeOption,
|