diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..29152ba --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,66 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Export local", + "type": "debugpy", + "request": "launch", + "module": "capella_ros_tools", + "args": [ + "export", + "-m", + "tests/data/melody_model_60", + "-l", + "la", + "-o", + "tests/data/melody_msgs" + ] + }, + { + "name": "Export git", + "type": "debugpy", + "request": "launch", + "module": "capella_ros_tools", + "args": [ + "export", + "-m", + "git+https://github.com/DSD-DBS/coffee-machine", + "-l", + "oa", + "-o", + "tests/data/coffee_msgs" + ] + }, + { + "name": "Import local", + "type": "debugpy", + "request": "launch", + "module": "capella_ros_tools", + "args": [ + "import", + "-i", + "tests/data/data_model/example_msgs", + "-m", + "tests/data/empty_project_52", + "-l", + "la", + "--no-deps" + ] + }, + { + "name": "Import git", + "type": "debugpy", + "request": "launch", + "module": "capella_ros_tools", + "args": [ + "import", + "-i", + "git+https://github.com/DSD-DBS/dsd-ros-msg-definitions-oss", + "-m", + "tests/data/empty_project_52", + "-l", + "la" + ] + } + ] +} diff --git a/.vscode/launch.json.license b/.vscode/launch.json.license new file mode 100644 index 0000000..544def3 --- /dev/null +++ b/.vscode/launch.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Copyright DB InfraGO AG +SPDX-License-Identifier: CC0-1.0 diff --git a/capella_ros_tools/__main__.py b/capella_ros_tools/__main__.py index 2c124bb..74f5dd1 100644 --- a/capella_ros_tools/__main__.py +++ b/capella_ros_tools/__main__.py @@ -23,18 +23,20 @@ def cli(): @cli.command("import") -@click.argument( - "messages", +@click.option( + "-i", + "--input", type=str, required=True, ) -@click.argument( - "model", +@click.option( + "-m" "--model", type=cli_helpers.ModelCLI(), required=True, ) -@click.argument( - "layer", +@click.option( + "-l", + "--layer", type=click.Choice(["oa", "la", "sa", "pa"], case_sensitive=False), required=True, ) @@ -44,27 +46,24 @@ def cli(): is_flag=True, help="Don’t install message dependencies.", ) -@click.option("--port", type=int, help="Open model viewer on given port.") +@click.option( + "-p", "--port", type=int, help="Open model viewer on given port." +) def import_msgs( - messages: str, + input: str, model: capellambse.MelodyModel, layer: str, no_deps: bool, port: int, ) -> None: - """Import ROS messages into a Capella data package. - - MESSAGES: File path or git URL to ROS messages. - MODEL: Path to Capella model. - LAYER: Layer of Capella model to import elements to. - """ + """Import ROS messages into a Capella data package.""" from capella_ros_tools.scripts import import_msgs as importer root_uuid = getattr(model, layer).uuid types_uuid = model.sa.data_package.uuid - yml = importer.Importer(messages, no_deps)(root_uuid, types_uuid) + yml = importer.Importer(input, no_deps)(root_uuid, types_uuid) decl.apply(model, io.StringIO(yml)) if port: @@ -72,19 +71,23 @@ def import_msgs( @cli.command("export") -@click.argument("model", type=cli_helpers.ModelCLI, required=True) -@click.argument( - "layer", +@click.option("-m", "--model", type=cli_helpers.ModelCLI, required=True) +@click.option( + "-l", + "--layer", type=click.Choice(["oa", "la", "sa", "pa"], case_sensitive=False), required=True, ) -@click.argument( - "messages", type=click.Path(path_type=pathlib.Path), required=True +@click.option( + "-o", + "--output", + type=click.Path(path_type=pathlib.Path), + default=pathlib.Path.cwd() / "export", ) def export_capella( model: capellambse.MelodyModel, layer: str, - msg_path: pathlib.Path, + output: pathlib.Path, ): """Export Capella data package to ROS messages. @@ -94,7 +97,8 @@ def export_capella( """ from capella_ros_tools.scripts import export_capella as exporter - exporter.Exporter(model, layer, msg_path)() + current_pkg = getattr(model, layer).data_package + exporter.export(current_pkg, output) if __name__ == "__main__": diff --git a/capella_ros_tools/scripts/export_capella.py b/capella_ros_tools/scripts/export_capella.py index a63ba35..a10ecb4 100644 --- a/capella_ros_tools/scripts/export_capella.py +++ b/capella_ros_tools/scripts/export_capella.py @@ -5,7 +5,6 @@ import pathlib import re -import capellambse from capellambse.model.crosslayer import information from capella_ros_tools import data_model @@ -17,75 +16,64 @@ def _clean_name(name: str) -> str: return re.sub(r"\W", "", name) -class Exporter: - """Class for exporting a Capella data package as ROS messages.""" - - def __init__( - self, - model: capellambse.MelodyModel, - layer: str, - output_path: pathlib.Path, - ): - self._data_package = getattr(model, layer).data_package - output_path.mkdir(parents=True, exist_ok=True) - self._output_path = output_path - - def _handle_pkg( - self, - current_pkg: information.DataPkg, - current_path: pathlib.Path, - ): - for cls_obj in current_pkg.classes: - cls_def = data_model.MessageDef( - cls_obj.name, [], [], cls_obj.description +def export(current_pkg: information.DataPkg, current_path: pathlib.Path): + """Export a Capella data package to ROS messages.""" + for cls_obj in current_pkg.classes: + fields = [] + for prop_obj in cls_obj.owned_properties: + type_def = data_model.TypeDef( + name=prop_obj.type.name, + card=data_model.Range( + prop_obj.min_card.value, prop_obj.max_card.value + ), ) - for prop_obj in cls_obj.owned_properties: - type_def = data_model.TypeDef( - prop_obj.type.name, - data_model.Range( - prop_obj.min_card.value, prop_obj.max_card.value - ), - ) - prop_def = data_model.FieldDef( - type_def, prop_obj.name, prop_obj.description - ) - cls_def.fields.append(prop_def) - (current_path / f"{_clean_name(cls_obj.name)}.msg").write_text( - str(cls_def) + prop_def = data_model.FieldDef( + type=type_def, + name=prop_obj.name, + description=prop_obj.description, ) + fields.append(prop_def) + cls_def = data_model.MessageDef( + name=cls_obj.name, + fields=fields, + enums=[], + description=cls_obj.description, + ) + (current_path / f"{_clean_name(cls_obj.name)}.msg").write_text( + str(cls_def) + ) - for enum_obj in current_pkg.enumerations: - enum_def = data_model.EnumDef( - enum_obj.name, [], enum_obj.description + for enum_obj in current_pkg.enumerations: + literals = [] + for i, lit_obj in enumerate(enum_obj.owned_literals): + try: + type_name = lit_obj.value.type.name + except AttributeError: + type_name = "uint8" + try: + literal_value = lit_obj.value.value + except AttributeError: + literal_value = i + type_def = data_model.TypeDef( + type_name, data_model.Range("1", "1") ) - for i, lit_obj in enumerate(enum_obj.owned_literals): - try: - type_name = lit_obj.value.type.name - except AttributeError: - type_name = "uint8" - try: - literal_value = lit_obj.value.value - except AttributeError: - literal_value = i - type_def = data_model.TypeDef( - type_name, data_model.Range("1", "1") - ) - lit_def = data_model.ConstantDef( - type_def, - lit_obj.name, - literal_value, - lit_obj.description, - ) - enum_def.literals.append(lit_def) - (current_path / f"{_clean_name(enum_obj.name)}.msg").write_text( - str(enum_def) + lit_def = data_model.ConstantDef( + type=type_def, + name=lit_obj.name, + value=literal_value, + description=lit_obj.description, ) + literals.append(lit_def) + enum_def = data_model.EnumDef( + name=enum_obj.name, + literals=literals, + description=enum_obj.description, + ) + (current_path / f"{_clean_name(enum_obj.name)}.msg").write_text( + str(enum_def) + ) - for pkg_obj in current_pkg.packages: - pkg_path = current_path / _clean_name(pkg_obj.name) - pkg_path.mkdir(parents=True, exist_ok=True) - self._handle_pkg(pkg_obj, pkg_path) - - def __call__(self): - """Export the Capella data package as ROS messages.""" - self._handle_pkg(self._data_package, self._output_path) + for pkg_obj in current_pkg.packages: + pkg_path = current_path / _clean_name(pkg_obj.name) + pkg_path.mkdir(parents=True, exist_ok=True) + export(pkg_obj, pkg_path) diff --git a/docs/source/howtos.rst b/docs/source/howtos.rst index 86a8e3d..6645fdd 100644 --- a/docs/source/howtos.rst +++ b/docs/source/howtos.rst @@ -17,24 +17,35 @@ Import ROS2 Messages: --------------------- .. code-block:: bash - $ python -m capella_ros_tools import tests/data/example_msgs tests/data/empty_model la --exists-action=skip --port=5000 --no-deps + python -m capella_ros_tools import \ + -i tests/data/data_model/example_msgs \ + -m tests/data/empty_project_52 -l la \ + --port=5000 --no-deps Import ROS2 Messages from Git Repository: ----------------------------------------- .. code-block:: bash - $ python -m capella_ros_tools import git+https://github.com/DSD-DBS/dsd-ros-msg-definitions-oss tests/data/empty_model la --exists-action=skip --port=5000 + python -m capella_ros_tools import \ + -i git+https://github.com/DSD-DBS/dsd-ros-msg-definitions-oss \ + -m tests/data/empty_project_52 -l la \ + --port=5000 Export Capella data package: ------------------------------------ .. code-block:: bash - $ python -m capella_ros_tools export tests/data/melody_model_60 la tests/data/melody_msgs + python -m capella_ros_tools export \ + -m tests/data/melody_model_60 -l la \ + -o tests/data/melody_msgs Export Capella data package from Git Repository: -------------------------------------------------------- .. code-block:: bash - $ python -m capella_ros_tools export git+https://github.com/DSD-DBS/coffee-machine oa tests/data/coffee_msgs + python -m capella_ros_tools export \ + -m git+https://github.com/DSD-DBS/coffee-machine -l oa \ + -o tests/data/coffee_msgs -Note: When exporting Capella enumerations, if the enumeration values are not defined in the Capella model, the values will be set to 0, 1, 2, 3, etc. and the datatype will be set to unit8. +.. note:: + When exporting Capella enumerations, if the enumeration literal values are not defined in the Capella model, the values will be set to 0, 1, 2, 3, etc. and the value's type will be set to unit8. diff --git a/docs/source/index.rst b/docs/source/index.rst index f872e4a..d4c6365 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,41 +18,22 @@ Overview Capella ROS Tools is a command-line application written in Python, designed to facilitate the seamless integration of ROS2 and Capella MBSE tools. Key features include: -* generate ROS2 message files (.msg) from Capella models -* generate Capella elements and objects from ROS2 message files -* support input sources through both local file paths and Git repositories -* display snapshots of Capella model elements in a tree structure - -If you want a quickstart at how to use this tool, head right into the -:ref:`Usage section `. +* Export Capella model elements as ROS2 message (.msg) files. +* Import ROS2 message (.msg) files as Capella model elements. +* Works with local and remote message files/Capella projects. .. toctree:: :maxdepth: 2 :caption: Contents: -.. toctree:: - :maxdepth: 2 - :caption: Usage - usage - - -.. toctree:: - :maxdepth: 2 - :caption: Examples - howtos - -.. toctree:: - :maxdepth: 2 - :caption: ROS2 Messages Layout - messages .. toctree:: :maxdepth: 3 - :caption: API reference + :caption: API reference: code/modules diff --git a/docs/source/messages.rst b/docs/source/messages.rst index 6301af8..5046270 100644 --- a/docs/source/messages.rst +++ b/docs/source/messages.rst @@ -18,7 +18,7 @@ Class Definition * **Indented Comment Lines:** Comments on a line of their own but indented are added to the description of the last encountered property. * **Block Comments:** Comments on a line of their own and not indented are added to the description of the next properties until an empty line and the block comment has been used. -.. literalinclude:: ../../tests/data/example_msgs/msg/SampleClass.msg +.. literalinclude:: ../../tests/data/data_model/example_msgs/package1/msg/SampleClass.msg :language: python @@ -33,7 +33,7 @@ Enum definition * **Indented Comment Lines:** Comments on a line of their own but indented are added to the description of the last encountered enum literal. * **Block Comments:** Comments on a line of their own and not indented are added to the description of the next/current enum definition until an empty line and the block comment has been used. -.. literalinclude:: ../../tests/data/example_msgs/msg/SampleEnum.msg +.. literalinclude:: ../../tests/data/data_model/example_msgs/package1/msg/SampleEnum.msg :language: python Enum and Class Definition @@ -46,7 +46,7 @@ Enum and Class Definition * **Indented Comment Lines:** Comments on a line of their own but indented are added to the description of the last encountered property or enum literal. * **Block Comments:** Comments on a line of their own and not indented are added to the descriptions of the next properties or added to the descriptions of the next/current enum until an empty line and the block comment has been used. -.. literalinclude:: ../../tests/data/example_msgs/msg/SampleClassEnum.msg +.. literalinclude:: ../../tests/data/data_model/example_msgs/package2/msg/SampleClassEnum.msg :language: python Referencing enums @@ -61,7 +61,7 @@ In the Same File * Name matching takes precedence over type matching. -.. literalinclude:: ../../tests/data/example_msgs/msg/SampleClassEnum.msg +.. literalinclude:: ../../tests/data/data_model/example_msgs/package2/msg/SampleClassEnum.msg :language: python In another file @@ -72,8 +72,8 @@ In another file * **cf. :** The enum name was derived from the file name (excluding the extension). * **cf. , _XXX:** The enum name was derived from the longest common prefix of all enum literals in the definition. -.. literalinclude:: ../../tests/data/example_msgs/msg/SampleEnum.msg +.. literalinclude:: ../../tests/data/data_model/example_msgs/package1/msg/SampleEnum.msg :language: python -.. literalinclude:: ../../tests/data/example_msgs/msg/SampleClass.msg +.. literalinclude:: ../../tests/data/data_model/example_msgs/package1/msg/SampleClass.msg :language: python diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 26dd736..62739ad 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -14,33 +14,24 @@ Import ROS2 Messages: ---------------------- .. code-block:: bash - $ python -m capella_ros_tools import --port= --exists-action= --no-deps + python -m capella_ros_tools import -i -m -l -p --no-deps -* ****, import ROS2 messages from -* ****, export to Capella model -* ****, use Capella model layer -* **--port=**, start Capella model viewer at (optional) -* **--exists-action=**, action to take if a Capella element already exists (optional) +* **-i/--input**, import ROS2 messages from +* **-m/--model**, export to Capella model +* **-l/--layer**, use Capella model layer +* **-p/--port**, start Capella model explorer at (optional) +* **--no-deps**, do not import ROS2 dependencies (e.g. std_msgs) (flag) - * **skip**, skip elements - * **replace**, replace elements - * **abort**, abort import - * **ask**, ask the user (default) - -* **--no-deps**, do not import ROS2 dependencies (e.g. std_msgs) - -Tip: Use the `--port` option to start the Capella model viewer at a specific port. The Capella model viewer can then be downloaded to be viewed at a later time using the following command: -.. code-block:: bash - - $ wget http://localhost: -E -r +.. note:: + The `--port` option can be used to start the Capella model explorer on a specific port. The Capella model viewer can then be downloaded to be viewed at a later time using `wget` eg. `wget http://localhost: -E -r`. Export Capella Model (experimental): ------------------------------------ .. code-block:: bash - $ python -m capella_ros_tools export + python -m capella_ros_tools export -m -l -o -* ****, import Capella model from -* ****, use Capella model layer -* ****, export ROS2 messages to +* **-m/--model**, import Capella model from +* **-l/--layer**, use Capella model layer +* **-o/--output**, export ROS2 messages to diff --git a/pyproject.toml b/pyproject.toml index 3931f60..26b0c78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,9 +30,6 @@ classifiers = [ dependencies = [ "click", "capellambse@ git+https://github.com/DSD-DBS/py-capellambse.git@decl-sync", - "capellambse_context_diagrams", - "fastapi", - "uvicorn[standard]", ] [project.urls]