Skip to content

Commit 5193dbc

Browse files
committed
Initial commit
0 parents  commit 5193dbc

10 files changed

Lines changed: 820 additions & 0 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "open-simulation-interface"]
2+
path = open-simulation-interface
3+
url = https://github.com/OpenSimulationInterface/open-simulation-interface.git

LICENSE

Lines changed: 382 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Open Simulation Interface (OSI) Python Bindings
2+
===============================================
3+
4+
This python package provides python bindings and utility modules for the [ASAM Open Simulation Interface](https://github.com/OpenSimulationInterface/open-simulation-interface).
5+
6+
For more information on OSI see the [official documentation](https://opensimulationinterface.github.io/osi-antora-generator/asamosi/latest/specification/index.html) or the [class list](https://opensimulationinterface.github.io/osi-antora-generator/asamosi/latest/gen/annotated.html) for defined protobuf messages.
7+
8+
## Usage
9+
For usage examples, please refer to the official documentation:
10+
- [Trace file generation with python](https://opensimulationinterface.github.io/osi-antora-generator/asamosi/latest/interface/architecture/trace_file_example.html)
11+
12+
## Installation
13+
6.06 KB
Binary file not shown.

build-backend/protoc_backend.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import os
2+
import subprocess
3+
import sys
4+
import re
5+
import glob
6+
import pathlib
7+
from typing import Any
8+
9+
from poetry.core.masonry.api import get_requires_for_build_wheel, get_requires_for_build_sdist, prepare_metadata_for_build_wheel, build_wheel as build_wheel_orig, build_sdist as build_sdist_orig, build_editable, get_requires_for_build_editable, prepare_metadata_for_build_editable
10+
11+
import poetry_dynamic_versioning.patch as patch
12+
13+
from protoc import PROTOC_EXE
14+
15+
16+
# Activate Versioning Plugin
17+
patch.activate()
18+
19+
20+
def _generate_python_files(package_name):
21+
package_path = pathlib.Path(os.getcwd()) / package_name
22+
try:
23+
os.mkdir(package_path)
24+
except Exception:
25+
pass
26+
27+
# configure the version number
28+
VERSION_MAJOR = None
29+
VERSION_MINOR = None
30+
VERSION_PATCH = None
31+
VERSION_SUFFIX = None
32+
with open(pathlib.Path("open-simulation-interface")/"VERSION", "rt") as versionin:
33+
for line in versionin:
34+
if line.startswith("VERSION_MAJOR"):
35+
VERSION_MAJOR = int(line.split("=")[1].strip())
36+
if line.startswith("VERSION_MINOR"):
37+
VERSION_MINOR = int(line.split("=")[1].strip())
38+
if line.startswith("VERSION_PATCH"):
39+
VERSION_PATCH = int(line.split("=")[1].strip())
40+
if line.startswith("VERSION_SUFFIX"):
41+
VERSION_SUFFIX = line.split("=")[1].strip()
42+
43+
# Generate osi_version.proto
44+
with open("open-simulation-interface/osi_version.proto.in", "rt") as fin:
45+
with open("open-simulation-interface/osi_version.proto", "wt") as fout:
46+
for line in fin:
47+
lineConfigured = line.replace("@VERSION_MAJOR@", str(VERSION_MAJOR))
48+
lineConfigured = lineConfigured.replace(
49+
"@VERSION_MINOR@", str(VERSION_MINOR)
50+
)
51+
lineConfigured = lineConfigured.replace(
52+
"@VERSION_PATCH@", str(VERSION_PATCH)
53+
)
54+
fout.write(lineConfigured)
55+
56+
# Copy and adjust imports
57+
pattern = re.compile('^import "osi_')
58+
for source in glob.glob("open-simulation-interface/*.proto"):
59+
with open(source) as src_file:
60+
with open(package_path / pathlib.Path(source).name, "w") as dst_file:
61+
for line in src_file:
62+
dst_file.write(
63+
pattern.sub('import "' + package_name + "/osi_", line)
64+
)
65+
66+
# Run protoc
67+
subprocess.check_call(
68+
[PROTOC_EXE, "--python_out=.", "--pyi_out=.", package_name + "/*.proto"]
69+
)
70+
71+
# Write __init__.py
72+
with open(package_path / "__init__.py", "wt") as init_file:
73+
init_file.write(
74+
f"__version__ = '{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_PATCH}{VERSION_SUFFIX or ''}'\n"
75+
)
76+
77+
# Override build actions
78+
79+
def build_wheel(
80+
wheel_directory: str,
81+
config_settings: dict[str, Any] | None = None,
82+
metadata_directory: str | None = None,
83+
) -> str:
84+
"""Builds a wheel, places it in wheel_directory"""
85+
_generate_python_files("osi3")
86+
return build_wheel_orig(wheel_directory, config_settings, metadata_directory)
87+
88+
def build_sdist(
89+
sdist_directory: str, config_settings: dict[str, Any] | None = None
90+
) -> str:
91+
"""Builds an sdist, places it in sdist_directory"""
92+
_generate_python_files("osi3")
93+
return build_sdist_orig(sdist_directory, config_settings)
94+

open-simulation-interface

osi3trace/osi2read.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
This program converts serialized osi trace files into a human readable txth file.
3+
4+
Example usage:
5+
python3 osi2read.py -d trace.osi -o myreadableosifile
6+
"""
7+
8+
from osi3trace.osi_trace import OSITrace
9+
import argparse
10+
import pathlib
11+
12+
13+
def command_line_arguments():
14+
"""Define and handle command line interface"""
15+
16+
parser = argparse.ArgumentParser(
17+
description="Convert a serialized osi trace file to a readable txth output.",
18+
prog="osi2read",
19+
)
20+
parser.add_argument(
21+
"--data",
22+
"-d",
23+
help="Path to the file with serialized data.",
24+
type=str,
25+
required=True,
26+
)
27+
parser.add_argument(
28+
"--type",
29+
"-t",
30+
help="Name of the type used to serialize data.",
31+
choices=OSITrace.message_types(),
32+
default="SensorView",
33+
type=str,
34+
required=False,
35+
)
36+
parser.add_argument(
37+
"--output",
38+
"-o",
39+
help="Output name of the file.",
40+
type=str,
41+
required=False,
42+
)
43+
44+
return parser.parse_args()
45+
46+
47+
def main():
48+
# Handling of command line arguments
49+
args = command_line_arguments()
50+
51+
# Initialize the OSI trace class
52+
trace = OSITrace(args.data, args.type)
53+
54+
if not args.output:
55+
path = pathlib.Path(args.data).with_suffix(".txth")
56+
args.output = str(path)
57+
58+
with open(args.output, "wt") as f:
59+
for message in trace:
60+
f.write(str(message))
61+
62+
trace.close()
63+
64+
65+
if __name__ == "__main__":
66+
main()

osi3trace/osi_trace.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
Module to handle and manage OSI trace files.
3+
"""
4+
5+
import lzma
6+
import struct
7+
8+
from osi3.osi_sensorview_pb2 import SensorView
9+
from osi3.osi_sensorviewconfiguration_pb2 import SensorViewConfiguration
10+
from osi3.osi_groundtruth_pb2 import GroundTruth
11+
from osi3.osi_hostvehicledata_pb2 import HostVehicleData
12+
from osi3.osi_sensordata_pb2 import SensorData
13+
from osi3.osi_trafficcommand_pb2 import TrafficCommand
14+
from osi3.osi_trafficcommandupdate_pb2 import TrafficCommandUpdate
15+
from osi3.osi_trafficupdate_pb2 import TrafficUpdate
16+
from osi3.osi_motionrequest_pb2 import MotionRequest
17+
from osi3.osi_streamingupdate_pb2 import StreamingUpdate
18+
19+
20+
MESSAGES_TYPE = {
21+
"SensorView": SensorView,
22+
"SensorViewConfiguration": SensorViewConfiguration,
23+
"GroundTruth": GroundTruth,
24+
"HostVehicleData": HostVehicleData,
25+
"SensorData": SensorData,
26+
"TrafficCommand": TrafficCommand,
27+
"TrafficCommandUpdate": TrafficCommandUpdate,
28+
"TrafficUpdate": TrafficUpdate,
29+
"MotionRequest": MotionRequest,
30+
"StreamingUpdate": StreamingUpdate,
31+
}
32+
33+
34+
class OSITrace:
35+
"""This class can import and decode OSI trace files."""
36+
37+
@staticmethod
38+
def map_message_type(type_name):
39+
"""Map the type name to the protobuf message type."""
40+
return MESSAGES_TYPE[type_name]
41+
42+
@staticmethod
43+
def message_types():
44+
"""Message types that OSITrace supports."""
45+
return list(MESSAGES_TYPE.keys())
46+
47+
def __init__(self, path=None, type_name="SensorView", cache_messages=False):
48+
self.type = self.map_message_type(type_name)
49+
self.file = None
50+
self.current_index = None
51+
self.message_offsets = None
52+
self.read_complete = False
53+
self.message_cache = {} if cache_messages else None
54+
self._header_length = 4
55+
if path:
56+
self.from_file(path, type_name, cache_messages)
57+
58+
def from_file(self, path, type_name="SensorView", cache_messages=False):
59+
"""Import a trace from a file"""
60+
self.type = self.map_message_type(type_name)
61+
62+
if path.lower().endswith((".lzma", ".xz")):
63+
self.file = lzma.open(path, "rb")
64+
else:
65+
self.file = open(path, "rb")
66+
67+
self.read_complete = False
68+
self.current_index = 0
69+
self.message_offsets = [0]
70+
self.message_cache = {} if cache_messages else None
71+
72+
def retrieve_offsets(self, limit=None):
73+
"""Retrieve the offsets of the messages from the file."""
74+
if not self.read_complete:
75+
self.current_index = len(self.message_offsets) - 1
76+
self.file.seek(self.message_offsets[-1], 0)
77+
while not self.read_complete and (
78+
not limit or len(self.message_offsets) <= limit
79+
):
80+
self.retrieve_message(skip=True)
81+
return self.message_offsets
82+
83+
def retrieve_message(self, index=None, skip=False):
84+
"""Retrieve the next message from the file at the current position or given index, or skip it if skip is true."""
85+
if index is not None:
86+
self.current_index = index
87+
self.file.seek(self.message_offsets[index], 0)
88+
if self.message_cache is not None and self.current_index in self.message_cache:
89+
message = self.message_cache[self.current_index]
90+
self.current_index += 1
91+
if self.current_index == len(self.message_offsets):
92+
self.file.seek(0, 2)
93+
else:
94+
self.file.seek(self.message_offsets[self.current_index], 0)
95+
if skip:
96+
return self.message_offsets[self.current_index]
97+
else:
98+
return message
99+
start = self.file.tell()
100+
header = self.file.read(self._header_length)
101+
if len(header) < self._header_length:
102+
if start == self.message_offsets[-1]:
103+
self.message_offsets.pop()
104+
self.read_complete = True
105+
self.file.seek(start, 0)
106+
return None
107+
message_length = struct.unpack("<L", header)[0]
108+
if skip:
109+
new_pos = self.file.seek(message_length, 1)
110+
if new_pos - start < message_length + self._header_length:
111+
if start == self.message_offsets[-1]:
112+
self.message_offsets.pop()
113+
self.read_complete = True
114+
self.file.seek(start, 0)
115+
return None
116+
self.current_index += 1
117+
if start == self.message_offsets[-1]:
118+
self.message_offsets.append(new_pos)
119+
return new_pos
120+
message_data = self.file.read(message_length)
121+
if len(message_data) < message_length:
122+
if start == self.message_offsets[-1]:
123+
self.message_offsets.pop()
124+
self.read_complete = True
125+
self.file.seek(start, 0)
126+
return None
127+
self.current_index += 1
128+
message = self.type()
129+
message.ParseFromString(message_data)
130+
if start == self.message_offsets[-1]:
131+
if self.message_cache is not None:
132+
self.message_cache[len(self.message_offsets) - 1] = message
133+
self.message_offsets.append(self.file.tell())
134+
return message
135+
136+
def restart(self, index=None):
137+
"""Restart the reading of the file from the beginning or from a given index."""
138+
self.current_index = index if index else 0
139+
self.file.seek(self.message_offsets[self.current_index], 0)
140+
141+
def __iter__(self):
142+
while message := self.retrieve_message():
143+
yield message
144+
145+
def get_message_by_index(self, index):
146+
"""
147+
Get a message by its index.
148+
"""
149+
if index >= len(self.message_offsets):
150+
self.retrieve_offsets(index)
151+
if self.message_cache is not None and index in self.message_cache:
152+
return self.message_cache[index]
153+
return self.retrieve_message(index=index)
154+
155+
def get_messages(self):
156+
"""
157+
Yield an iterator over all messages in the file.
158+
"""
159+
return self.get_messages_in_index_range(0, None)
160+
161+
def get_messages_in_index_range(self, begin, end):
162+
"""
163+
Yield an iterator over messages of indexes between begin and end included.
164+
"""
165+
if begin >= len(self.message_offsets):
166+
self.retrieve_offsets(begin)
167+
self.restart(begin)
168+
current = begin
169+
while end is None or current < end:
170+
if self.message_cache is not None and current in self.message_cache:
171+
yield self.message_cache[current]
172+
else:
173+
message = self.retrieve_message()
174+
if message is None:
175+
break
176+
yield message
177+
current += 1
178+
179+
def close(self):
180+
if self.file:
181+
self.file.close()
182+
self.file = None
183+
self.current_index = None
184+
self.message_cache = None
185+
self.message_offsets = None
186+
self.read_complete = False
187+
self.read_limit = None
188+
self.type = None

poetry.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)