7. Introduction to Code Generation

This notebook exemplifies how to generate automatically code in terms of interfaces. For this three examples are provided. The first one creates a ROS message, the second a standard python class and the last a protobuf interface.

[1]:
import capellambse
[2]:
path_to_model = "../../../tests/data/melodymodel/5_0/Melody Model Test.aird"
model = capellambse.MelodyModel(path_to_model)

In particular, we want to create code from our class:

[3]:
model.diagrams.by_name("[CDB] CodeGeneration")
Unknown global filter 'hide.technical.interfaces.filter'
Unknown global filter 'ModelExtensionFilter'
[3]:

In order to access the classes, we can simply access the data_package of the operational layer, and from there access the attribute classes.

[4]:
data_pkg = model.oa.data_package
data_pkg.classes
[4]:
  1. Class "Twist" (8164ae8b-36d5-4502-a184-5ec064db4ec3)
  2. Class "Trajectory" (c5ea0585-7657-4764-9eb2-3a6584980ce6)
  3. Class "Waypoint" (2a923851-a4ca-4fd2-a4b3-302edb8ac178)
  4. Class "Example" (a7ecc231-c55e-4ab9-ae14-9558e3ec2a34)

7.1. ROS2 IDL Message

Let’s have a brief look into the structure of ROS2 Message descriptions. They are stored in .msg files and comprised of a type and name, separated by whitespace, i.e.:

fieldtype1 fieldname1
fieldtype2[] fieldname2
[5]:
def class_to_ros2_idl(cls):
    filename = f"{cls.name}.msg"
    lines = []
    for prop in cls.properties:
        multiplicity = ("", "[]")[prop.max_card.value > 1]
        lines.append(f"{prop.type.name}{multiplicity} {prop.name}")
    text = "\n".join(lines)
    with open(filename, "w") as file:
        file.write(text)
    print(f"# file: {filename} \n{text}\n")

In our example, files would be generated with the following content:

[6]:
data_pkg = model.oa.data_package
for cls in data_pkg.classes:
    class_to_ros2_idl(cls)
# file: Twist.msg


# file: Trajectory.msg
Waypoint[] waypoints

# file: Waypoint.msg
float lat
float lon
float alt
Example[] examples

# file: Example.msg
str test

7.2. Interface for python classes

A python class has the following structure:

class class_name:
    name1: type
    name2: [type]

A python interface can be generated as follows:

[7]:
def class_to_python(cls, current_classes=None):
    lines = [f"class {cls.name}:"]
    current_classes = [cls]
    if not cls.properties:
        lines.append(4*" " + "pass")
    for prop in cls.properties:
        if (
            isinstance(
                prop.type, capellambse.model.crosslayer.information.Class
            )
            and prop.type not in current_classes
        ):
            nested_text = class_to_python(prop.type, current_classes)
            lines = [nested_text] + ["\n"] + lines
        multiplicity = (f"{prop.type.name}", f"list[{prop.type.name}]")[
            prop.max_card.value > 1
        ]
        lines.append(4*" " + f"{prop.name}: {multiplicity}")
    text = "\n".join(lines)

    return text
[8]:
trajectory = data_pkg.classes.by_name("Trajectory")
text = class_to_python(trajectory)
filename = f"{trajectory.name.lower()}.py"
with open(filename, "w") as file:
    file.write(text)
print(f"# file: {filename} \n{text}\n")
# file: trajectory.py
class Example:
    test: str


class Waypoint:
    lat: float
    lon: float
    alt: float
    examples: list[Example]


class Trajectory:
    waypoints: list[Waypoint]

7.3. Interface for Protocol Buffers (Protobuf)

Protobuf Message descriptions are stored in .proto files where a class definition starts with message and each property of the class is defined by at least three parts: the data type, name and its order number. Classes can also be nested in other classes. An example is shown in the following:

syntax = "proto3";

message class1 {
    datatype class1_name1 = 1;
    datatype class1_name2 = 2;
    message class2 {
        datatype class2_name1 = 1;
   }
   repeated class2 class_name = 3;
}
[9]:
def class_to_proto(cls, current_classes=None, indent=""):
    if current_classes is None:
        current_classes = [cls]
        lines = ['syntax = "proto3";\n']
        indent += " "*4
        lines.append(f"{indent[:-4]}message  {cls.name} {{")
    else:
        lines = [f"{indent[:-4]}message  {cls.name} {{"]

    for counter, prop in enumerate(cls.properties, start=1):
        multiplicity = ("", "[]")[prop.max_card.value > 1]
        if (
            isinstance(
                prop.type, capellambse.model.crosslayer.information.Class
            )
            and prop.type not in current_classes
        ):
            current_classes.append(prop.type)
            nested_text = class_to_proto(
                prop.type, current_classes, indent + " "*4
            )
            lines.append(nested_text)
            lines.append(
                f"{indent}repeated {prop.type.name}{multiplicity} {prop.name} = {counter};"
            )
        else:
            lines.append(
                f"{indent}{prop.type.name}{multiplicity} {prop.name} = {counter};"
            )
    lines.append(f"{indent[:-4]}}}")
    text = "\n".join(lines)
    return text

The protobuf interface of class Trajectory would look as follows:

[10]:
trajectory = data_pkg.classes.by_name("Trajectory")
text = class_to_proto(trajectory)
filename = f"{trajectory.name}.proto"
with open(filename, "w") as file:
    file.write(text)
print(f"# file: {filename} \n{text}\n")
# file: Trajectory.proto
syntax = "proto3";

message  Trajectory {
    message  Waypoint {
        float lat = 1;
        float lon = 2;
        float alt = 3;
        message  Example {
            str test = 1;
        }
        repeated Example[] examples = 4;
    }
    repeated Waypoint[] waypoints = 1;
}