pymcap-cli 0.4.0__tar.gz → 0.6.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.
Files changed (64) hide show
  1. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/PKG-INFO +63 -1
  2. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/README.md +61 -0
  3. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/pyproject.toml +2 -1
  4. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cli.py +24 -0
  5. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/_run_processor.py +2 -2
  6. pymcap_cli-0.6.0/src/pymcap_cli/cmd/bag2mcap_cmd.py +285 -0
  7. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/cat_cmd.py +1 -1
  8. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/compress_cmd.py +2 -2
  9. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/convert_cmd.py +3 -3
  10. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/diag_cmd.py +99 -16
  11. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/du_cmd.py +3 -3
  12. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/filter_cmd.py +2 -2
  13. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/info_cmd.py +5 -5
  14. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/list_cmd.py +2 -2
  15. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/merge_cmd.py +2 -2
  16. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/plot_cmd.py +1 -1
  17. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/process_cmd.py +2 -2
  18. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/rechunk_cmd.py +2 -2
  19. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/records_cmd.py +1 -1
  20. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/recover_cmd.py +2 -2
  21. pymcap_cli-0.6.0/src/pymcap_cli/cmd/roscompress_cmd.py +806 -0
  22. pymcap_cli-0.6.0/src/pymcap_cli/cmd/rosdecompress_cmd.py +321 -0
  23. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/tftree_cmd.py +1 -1
  24. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +1 -1
  25. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/video_cmd.py +8 -6
  26. {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.6.0/src/pymcap_cli/core}/mcap_processor.py +28 -4
  27. pymcap_cli-0.6.0/src/pymcap_cli/core/mcap_transform.py +138 -0
  28. pymcap_cli-0.6.0/src/pymcap_cli/core/msg_resolver.py +447 -0
  29. pymcap_cli-0.6.0/src/pymcap_cli/display/__init__.py +0 -0
  30. {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.6.0/src/pymcap_cli/display}/display_utils.py +1 -1
  31. pymcap_cli-0.6.0/src/pymcap_cli/display/sparkline.py +75 -0
  32. pymcap_cli-0.6.0/src/pymcap_cli/encoding/__init__.py +0 -0
  33. pymcap_cli-0.6.0/src/pymcap_cli/encoding/decompress.py +213 -0
  34. pymcap_cli-0.6.0/src/pymcap_cli/encoding/encoder_common.py +275 -0
  35. pymcap_cli-0.6.0/src/pymcap_cli/encoding/pointcloud.py +212 -0
  36. pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_factory.py +47 -0
  37. pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_ffmpeg.py +793 -0
  38. pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_protocols.py +40 -0
  39. pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_pyav.py +372 -0
  40. pymcap_cli-0.6.0/src/pymcap_cli/py.typed +0 -0
  41. pymcap_cli-0.6.0/src/pymcap_cli/rosbag_reader/__init__.py +16 -0
  42. pymcap_cli-0.6.0/src/pymcap_cli/rosbag_reader/_reader.py +340 -0
  43. pymcap_cli-0.6.0/src/pymcap_cli/rosbag_reader/_types.py +48 -0
  44. pymcap_cli-0.6.0/src/pymcap_cli/rosbag_reader/py.typed +0 -0
  45. pymcap_cli-0.6.0/src/pymcap_cli/types/__init__.py +0 -0
  46. {pymcap_cli-0.4.0/src/pymcap_cli/cmd → pymcap_cli-0.6.0/src/pymcap_cli/types}/info_data.py +1 -1
  47. {pymcap_cli-0.4.0/src/pymcap_cli/cmd → pymcap_cli-0.6.0/src/pymcap_cli/types}/info_link.py +1 -1
  48. pymcap_cli-0.6.0/src/pymcap_cli/types/info_types.py +514 -0
  49. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/utils.py +1 -1
  50. pymcap_cli-0.4.0/src/pymcap_cli/cmd/roscompress_cmd.py +0 -618
  51. pymcap_cli-0.4.0/src/pymcap_cli/image_utils.py +0 -612
  52. pymcap_cli-0.4.0/src/pymcap_cli/msg_resolver.py +0 -284
  53. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/__init__.py +0 -0
  54. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/__init__.py +0 -0
  55. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
  56. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
  57. /pymcap_cli-0.4.0/src/pymcap_cli/py.typed → /pymcap_cli-0.6.0/src/pymcap_cli/core/__init__.py +0 -0
  58. {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.6.0/src/pymcap_cli/core}/input_handler.py +0 -0
  59. {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.6.0/src/pymcap_cli/core}/processors.py +0 -0
  60. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/debug_wrapper.py +0 -0
  61. {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.6.0/src/pymcap_cli/display}/osc_utils.py +0 -0
  62. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/http_utils.py +0 -0
  63. {pymcap_cli-0.4.0 → pymcap_cli-0.6.0}/src/pymcap_cli/info_types.py +0 -0
  64. {pymcap_cli-0.4.0/src/pymcap_cli → pymcap_cli-0.6.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.4.0
3
+ Version: 0.6.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.4.0"
3
+ version = "0.6.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,