Add rosbag conversion tools

This commit is contained in:
Marko Durkovic 2021-05-02 14:51:08 +02:00
parent 4de0c99274
commit 5d2d394110
10 changed files with 291 additions and 0 deletions

View File

@ -46,6 +46,14 @@ Read and deserialize rosbag2 messages:
print(msg.header.frame_id)
Convert rosbag1 to rosbag2::
# Convert "foo.bag", result will be "foo/"
rosbags-convert foo.bag
# Convert "foo.bag", save the result as "bar"
rosbags-convert foo.bag --dst /path/to/bar
Documentation
=============

View File

@ -0,0 +1,6 @@
rosbags.convert
===============
.. automodule:: rosbags.convert
:members:
:show-inheritance:

View File

@ -4,6 +4,7 @@ Rosbags namespace
.. toctree::
:maxdepth: 4
rosbags.convert
rosbags.rosbag1
rosbags.rosbag2
rosbags.serde

View File

@ -14,6 +14,7 @@
topics/serde
topics/rosbag2
topics/rosbag1
topics/convert
.. toctree::

29
docs/topics/convert.rst Normal file
View File

@ -0,0 +1,29 @@
Convert Rosbag1 to Rosbag2
==========================
The :py:mod:`rosbags.convert` package includes a CLI tool to convert legacy rosbag1 files to rosbag2.
Features
--------
- Reasonably fast, as it converts raw ROS1 messages to raw CDR messages without going though deserialization and serialization
- Tries to match ROS1 message type names to registered ROS2 types
- Automatically registers unknown message types present in the legacy rosbag file for the conversion
- Handles differences of ``std_msgs/msg/Header`` between both ROS versions
Limitations
-----------
- Refuses to convert unindexed rosbag files, please reindex files before conversion
- Currently does not handle split bags
Usage
-----
.. code-block:: console
# Convert "foo.bag", result will be "foo/"
$ rosbags-convert foo.bag
# Convert "foo.bag", save the result as "bar"
$ rosbags-convert foo.bag --dst /path/to/bar

View File

@ -54,6 +54,10 @@ install_requires =
ruamel.yaml
zstandard
[options.entry_points]
console_scripts =
rosbags-convert = rosbags.convert.__main__:main
[options.extras_require]
dev =
darglint

View File

@ -0,0 +1,16 @@
# Copyright 2020-2021 Ternaris.
# SPDX-License-Identifier: Apache-2.0
"""Rosbags file format conversion.
Conversion function transforms files from rosbag1 format to the latest rosbag2
format. It automatically matches ROS1 message types to their ROS2 counterparts
and adds custom types not present in the type system.
"""
from .converter import ConverterError, convert
__all__ = [
'ConverterError',
'convert',
]

View File

@ -0,0 +1,62 @@
# Copyright 2020-2021 Ternaris.
# SPDX-License-Identifier: Apache-2.0
"""CLI tool for rosbag conversion."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from .converter import ConverterError, convert
if TYPE_CHECKING:
from typing import Callable
def pathtype(exists: bool = True) -> Callable:
"""Path argument for argparse.
Args:
exists: Path should exists in filesystem.
Returns:
Argparse type function.
"""
def topath(pathname: str) -> Path:
path = Path(pathname)
if exists != path.exists():
raise argparse.ArgumentTypeError(
f'{path} should {"exist" if exists else "not exist"}.',
)
return path
return topath
def main() -> None:
"""Parse cli arguments and run conversion."""
parser = argparse.ArgumentParser(description='Convert rosbag1 to rosbag2.')
parser.add_argument(
'src',
type=pathtype(),
help='source path to read rosbag1 from',
)
parser.add_argument(
'--dst',
type=pathtype(exists=False),
help='destination path for rosbag2',
)
args = parser.parse_args()
try:
convert(args.src, args.dst)
except ConverterError as err:
print(f'ERROR: {err}') # noqa: T001
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,55 @@
# Copyright 2020-2021 Ternaris.
# SPDX-License-Identifier: Apache-2.0
"""Rosbag1 to Rosbag2 Converter."""
from __future__ import annotations
from typing import TYPE_CHECKING
from rosbags.rosbag1 import Reader, ReaderError
from rosbags.rosbag2 import Writer, WriterError
from rosbags.serde import ros1_to_cdr
from rosbags.typesys import get_types_from_msg, register_types
if TYPE_CHECKING:
from pathlib import Path
from typing import Any, Dict, Optional
class ConverterError(Exception):
"""Converter Error."""
def convert(src: Path, dst: Optional[Path]) -> None:
"""Convert Rosbag1 to Rosbag2.
Args:
src: Rosbag1 path.
dst: Rosbag2 path.
Raises:
ConverterError: An error occured during reading, writing, or
converting.
"""
dst = dst if dst else src.with_suffix('')
if dst.exists():
raise ConverterError(f'Output path {str(dst)!r} exists already.')
try:
with Reader(src) as reader, Writer(dst) as writer:
typs: Dict[str, Any] = {}
for name, topic in reader.topics.items():
writer.add_topic(name, topic.msgtype)
typs.update(get_types_from_msg(topic.msgdef, topic.msgtype))
register_types(typs)
for topic, msgtype, timestamp, data in reader.messages():
data = ros1_to_cdr(data, msgtype)
writer.write(topic, timestamp, data)
except ReaderError as err:
raise ConverterError(f'Reading source bag: {err}') from err
except WriterError as err:
raise ConverterError(f'Writing destination bag: {err}') from err
except Exception as err: # pylint: disable=broad-except
raise ConverterError(f'Converting rosbag: {err!r}') from err

109
tests/test_convert.py Normal file
View File

@ -0,0 +1,109 @@
# Copyright 2020-2021 Ternaris.
# SPDX-License-Identifier: Apache-2.0
"""Rosbag1to2 converter tests."""
import sys
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from rosbags.convert import ConverterError, convert
from rosbags.convert.__main__ import main
from rosbags.rosbag1 import ReaderError
from rosbags.rosbag2 import WriterError
def test_cliwrapper(tmp_path: Path):
"""Test cli wrapper."""
(tmp_path / 'subdir').mkdir()
(tmp_path / 'ros1.bag').write_text('')
with patch('rosbags.convert.__main__.convert') as cvrt, \
patch.object(sys, 'argv', ['cvt']), \
pytest.raises(SystemExit):
main()
assert not cvrt.called
with patch('rosbags.convert.__main__.convert') as cvrt, \
patch.object(sys, 'argv', ['cvt', str(tmp_path / 'no.bag')]), \
pytest.raises(SystemExit):
main()
assert not cvrt.called
with patch('rosbags.convert.__main__.convert') as cvrt, \
patch.object(sys, 'argv', ['cvt', str(tmp_path / 'ros1.bag')]):
main()
cvrt.assert_called_with(tmp_path / 'ros1.bag', None)
with patch('rosbags.convert.__main__.convert') as cvrt, \
patch.object(sys, 'argv', ['cvt',
str(tmp_path / 'ros1.bag'),
'--dst',
str(tmp_path / 'subdir')]), \
pytest.raises(SystemExit):
main()
assert not cvrt.called
with patch('rosbags.convert.__main__.convert') as cvrt, \
patch.object(sys, 'argv', ['cvt',
str(tmp_path / 'ros1.bag'),
'--dst',
str(tmp_path / 'target')]):
main()
cvrt.assert_called_with(tmp_path / 'ros1.bag', tmp_path / 'target')
with patch.object(sys, 'argv', ['cvt', str(tmp_path / 'ros1.bag')]), \
patch('builtins.print') as mock_print, \
patch('rosbags.convert.__main__.convert', side_effect=ConverterError('exc')), \
pytest.raises(SystemExit):
main()
mock_print.assert_called_with('ERROR: exc')
def test_convert(tmp_path: Path):
"""Test conversion function."""
(tmp_path / 'subdir').mkdir()
(tmp_path / 'foo.bag').write_text('')
with pytest.raises(ConverterError, match='exists already'):
convert(Path('foo.bag'), tmp_path / 'subdir')
with patch('rosbags.convert.converter.Reader') as reader, \
patch('rosbags.convert.converter.Writer') as writer, \
patch('rosbags.convert.converter.get_types_from_msg', return_value={'typ': 'def'}), \
patch('rosbags.convert.converter.register_types') as register_types, \
patch('rosbags.convert.converter.ros1_to_cdr') as ros1_to_cdr:
reader.return_value.__enter__.return_value.topics = {
'/topic': Mock(msgtype='typ', msgdef='def'),
}
reader.return_value.__enter__.return_value.messages.return_value = [
('/topic', 'typ', 42, b'\x42'),
]
ros1_to_cdr.return_value = b'666'
convert(Path('foo.bag'), None)
reader.assert_called_with(Path('foo.bag'))
reader.return_value.__enter__.return_value.messages.assert_called_with()
writer.assert_called_with(Path('foo'))
writer.return_value.__enter__.return_value.add_topic.assert_called_with('/topic', 'typ')
writer.return_value.__enter__.return_value.write.assert_called_with('/topic', 42, b'666')
register_types.assert_called_with({'typ': 'def'})
ros1_to_cdr.assert_called_with(b'\x42', 'typ')
ros1_to_cdr.side_effect = KeyError('exc')
with pytest.raises(ConverterError, match='Converting rosbag: '):
convert(Path('foo.bag'), None)
writer.side_effect = WriterError('exc')
with pytest.raises(ConverterError, match='Writing destination bag: '):
convert(Path('foo.bag'), None)
reader.side_effect = ReaderError('exc')
with pytest.raises(ConverterError, match='Reading source bag: '):
convert(Path('foo.bag'), None)