pymcap-cli 0.3.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.3.0 → pymcap_cli-0.5.0}/PKG-INFO +67 -1
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/README.md +61 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/pyproject.toml +8 -1
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cli.py +48 -0
- {pymcap_cli-0.3.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.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/cat_cmd.py +155 -97
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/compress_cmd.py +2 -2
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/convert_cmd.py +3 -3
- pymcap_cli-0.5.0/src/pymcap_cli/cmd/diag_cmd.py +609 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/du_cmd.py +3 -3
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/filter_cmd.py +2 -2
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/info_cmd.py +5 -5
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/list_cmd.py +2 -2
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/merge_cmd.py +2 -2
- pymcap_cli-0.5.0/src/pymcap_cli/cmd/plot_cmd.py +421 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/process_cmd.py +2 -2
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/rechunk_cmd.py +2 -2
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/records_cmd.py +1 -1
- {pymcap_cli-0.3.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.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/tftree_cmd.py +1 -1
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +1 -1
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/video_cmd.py +20 -11
- {pymcap_cli-0.3.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.3.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.3.0/src/pymcap_cli/cmd → pymcap_cli-0.5.0/src/pymcap_cli/types}/info_data.py +1 -1
- {pymcap_cli-0.3.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.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/utils.py +1 -1
- pymcap_cli-0.3.0/src/pymcap_cli/cmd/roscompress_cmd.py +0 -618
- pymcap_cli-0.3.0/src/pymcap_cli/image_utils.py +0 -587
- pymcap_cli-0.3.0/src/pymcap_cli/msg_resolver.py +0 -284
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
- /pymcap_cli-0.3.0/src/pymcap_cli/py.typed → /pymcap_cli-0.5.0/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.3.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/core}/input_handler.py +0 -0
- {pymcap_cli-0.3.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/core}/processors.py +0 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.3.0/src/pymcap_cli → pymcap_cli-0.5.0/src/pymcap_cli/display}/osc_utils.py +0 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.3.0 → pymcap_cli-0.5.0}/src/pymcap_cli/info_types.py +0 -0
- {pymcap_cli-0.3.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,7 +26,10 @@ 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
|
|
31
|
+
Requires-Dist: pymcap-cli[video,pointcloud,plot] ; extra == 'all'
|
|
32
|
+
Requires-Dist: plotly>=6.0.0 ; extra == 'plot'
|
|
30
33
|
Requires-Dist: pureini ; extra == 'pointcloud'
|
|
31
34
|
Requires-Dist: av>=12.0.0 ; extra == 'video'
|
|
32
35
|
Requires-Dist: numpy>=1.24.0 ; extra == 'video'
|
|
@@ -34,6 +37,8 @@ Requires-Python: >=3.10
|
|
|
34
37
|
Project-URL: Homepage, https://github.com/mrkbac/robotic-tools
|
|
35
38
|
Project-URL: Issues, https://github.com/mrkbac/robotic-tools/issues
|
|
36
39
|
Project-URL: Repository, https://github.com/mrkbac/robotic-tools
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Provides-Extra: plot
|
|
37
42
|
Provides-Extra: pointcloud
|
|
38
43
|
Provides-Extra: video
|
|
39
44
|
Description-Content-Type: text/markdown
|
|
@@ -129,6 +134,13 @@ pymcap-cli cat recording.mcap --query '/detections.objects[:]{confidence>0.8}'
|
|
|
129
134
|
|
|
130
135
|
# Pipe to file as JSONL
|
|
131
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
|
|
132
144
|
```
|
|
133
145
|
|
|
134
146
|
### `tftree` — TF Transform Tree
|
|
@@ -149,6 +161,45 @@ pymcap-cli tftree data.mcap
|
|
|
149
161
|
pymcap-cli tftree data.mcap --static-only
|
|
150
162
|
```
|
|
151
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
|
+
|
|
152
203
|
### `process` — Unified Processing
|
|
153
204
|
|
|
154
205
|
The most powerful command — combines recovery, filtering, and optimization in a single pass.
|
|
@@ -311,6 +362,21 @@ pymcap-cli roscompress data.mcap -o compressed.mcap
|
|
|
311
362
|
pymcap-cli roscompress data.mcap -o compressed.mcap --quality 28 --codec h265
|
|
312
363
|
```
|
|
313
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
|
+
|
|
314
380
|
### Shell Autocompletion
|
|
315
381
|
|
|
316
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
|
|
|
@@ -44,6 +45,12 @@ Issues = "https://github.com/mrkbac/robotic-tools/issues"
|
|
|
44
45
|
[project.optional-dependencies]
|
|
45
46
|
video = ["av>=12.0.0", "numpy>=1.24.0"]
|
|
46
47
|
pointcloud = ["pureini"]
|
|
48
|
+
plot = [
|
|
49
|
+
"plotly>=6.0.0",
|
|
50
|
+
]
|
|
51
|
+
all = [
|
|
52
|
+
"pymcap-cli[video,pointcloud,plot]",
|
|
53
|
+
]
|
|
47
54
|
|
|
48
55
|
[build-system]
|
|
49
56
|
requires = ["uv_build>=0.8.9,<0.9.0"]
|
|
@@ -5,9 +5,11 @@ 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,
|
|
12
|
+
diag_cmd,
|
|
11
13
|
du_cmd,
|
|
12
14
|
filter_cmd,
|
|
13
15
|
info_cmd,
|
|
@@ -44,6 +46,27 @@ except ImportError:
|
|
|
44
46
|
return 1
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
try:
|
|
50
|
+
from pymcap_cli.cmd.plot_cmd import plot
|
|
51
|
+
except ImportError:
|
|
52
|
+
|
|
53
|
+
def plot() -> int:
|
|
54
|
+
"""Plot command is unavailable because 'plotly' is not installed.
|
|
55
|
+
|
|
56
|
+
To enable plot functionality, please install pymcap-cli with the 'plot' extra:
|
|
57
|
+
|
|
58
|
+
uv add --group plot pymcap-cli
|
|
59
|
+
"""
|
|
60
|
+
print( # noqa: T201
|
|
61
|
+
"Error:\n"
|
|
62
|
+
"Plot command is unavailable because 'plotly' is not installed.\n"
|
|
63
|
+
"To enable plot functionality, please install pymcap-cli with the 'plot' extra:\n\n"
|
|
64
|
+
" uv add --group plot pymcap-cli\n",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
|
|
47
70
|
try:
|
|
48
71
|
from pymcap_cli.cmd.roscompress_cmd import roscompress
|
|
49
72
|
except ImportError:
|
|
@@ -65,6 +88,27 @@ except ImportError:
|
|
|
65
88
|
return 1
|
|
66
89
|
|
|
67
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
|
+
|
|
68
112
|
app = App(
|
|
69
113
|
name="pymcap-cli",
|
|
70
114
|
help="CLI tool for slicing and dicing MCAP files.",
|
|
@@ -76,6 +120,7 @@ transform_group = Group("Transform", sort_key=1)
|
|
|
76
120
|
|
|
77
121
|
# Inspect commands — read-only, extract information
|
|
78
122
|
app.command(name="cat", group=inspect_group)(cat_cmd.cat)
|
|
123
|
+
app.command(name="diag", group=inspect_group)(diag_cmd.diag)
|
|
79
124
|
app.command(name="du", group=inspect_group)(du_cmd.du)
|
|
80
125
|
app.command(name="info", group=inspect_group)(info_cmd.info)
|
|
81
126
|
app.command(name="info-json", group=inspect_group)(info_json_cmd.info_json)
|
|
@@ -86,6 +131,7 @@ app.command(name="tftree", group=inspect_group)(tftree_cmd.tftree)
|
|
|
86
131
|
app.command(name="topic-chunks", group=inspect_group)(topic_chunks_cmd.topic_chunks)
|
|
87
132
|
|
|
88
133
|
# Transform commands — convert, filter, or produce new files
|
|
134
|
+
app.command(name="bag2mcap", group=transform_group)(bag2mcap_cmd.bag2mcap)
|
|
89
135
|
app.command(name="compress", group=transform_group)(compress_cmd.compress)
|
|
90
136
|
app.command(name="convert", group=transform_group)(convert_cmd.convert)
|
|
91
137
|
app.command(name="filter", group=transform_group)(filter_cmd.filter_cmd)
|
|
@@ -94,7 +140,9 @@ app.command(name="process", group=transform_group)(process_cmd.process)
|
|
|
94
140
|
app.command(name="rechunk", group=transform_group)(rechunk_cmd.rechunk)
|
|
95
141
|
app.command(name="recover", group=transform_group)(recover_cmd.recover)
|
|
96
142
|
app.command(name="recover-inplace", group=transform_group)(recover_inplace_cmd.recover_inplace)
|
|
143
|
+
app.command(name="plot", group=inspect_group)(plot)
|
|
97
144
|
app.command(name="roscompress", group=transform_group)(roscompress)
|
|
145
|
+
app.command(name="rosdecompress", group=transform_group)(rosdecompress)
|
|
98
146
|
app.command(name="video", group=transform_group)(video)
|
|
99
147
|
|
|
100
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
|