mcap-bag-parser 0.1.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.
@@ -0,0 +1 @@
1
+ *.mcap filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,3 @@
1
+ .idea
2
+ *.pickle
3
+ __pycache__
@@ -0,0 +1,22 @@
1
+ stages:
2
+ - linting
3
+ - testing
4
+
5
+ flake8:
6
+ stage: linting
7
+ image: python:3.10
8
+
9
+ script:
10
+ - pip install flake8 pep8-naming
11
+ - flake8 . --count --max-complexity=20 --max-line-length=127 --statistics
12
+ only:
13
+ - merge_requests
14
+
15
+ pytest:
16
+ stage: testing
17
+ image: python:3.10
18
+ script:
19
+ - pip install -r requirements.txt
20
+ - python -m pytest
21
+ only:
22
+ - merge_requests
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Neal Tanner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.3
2
+ Name: mcap_bag_parser
3
+ Version: 0.1.0
4
+ Summary: Parse mcap bagfiles into pandas dataframes
5
+ Project-URL: Homepage, https://gitlab.com/nealtanner/mcap-bag-parser
6
+ Project-URL: Issues, https://gitlab.com/nealtanner/mcap-bag-parser/-/issues
7
+ Author: Neal Tanner, Teresa Gadda
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # MCAP Bag Parser
16
+
17
+ Parse MCAP rosbags into pandas dataframes
@@ -0,0 +1,3 @@
1
+ # MCAP Bag Parser
2
+
3
+ Parse MCAP rosbags into pandas dataframes
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcap_bag_parser"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Neal Tanner"},
10
+ { name="Teresa Gadda"},
11
+ ]
12
+ description = "Parse mcap bagfiles into pandas dataframes"
13
+ readme = "README.md"
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://gitlab.com/nealtanner/mcap-bag-parser"
23
+ Issues = "https://gitlab.com/nealtanner/mcap-bag-parser/-/issues"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/mcap_bag_parser"]
@@ -0,0 +1,5 @@
1
+ mcap
2
+ mcap-ros2-support
3
+ pandas
4
+ numpy
5
+ pytest
File without changes
File without changes
@@ -0,0 +1,325 @@
1
+ import functools
2
+ import pathlib
3
+ from collections import defaultdict
4
+ import pickle
5
+ from typing import Iterable, Optional
6
+
7
+ import pandas as pd
8
+ from mcap.reader import make_reader
9
+ from mcap_ros2.decoder import DecoderFactory
10
+
11
+ MAX_ARRAY_SIZE_TO_SPLIT = 10
12
+
13
+
14
+ def read_messages(bag_file: str,
15
+ topics: Optional[Iterable[str]] = None,
16
+ start_time: Optional[int] = None,
17
+ end_time: Optional[int] = None,
18
+ log_time_order: bool = True,
19
+ reverse: bool = False):
20
+ """ iterates through the messages in an MCAP.
21
+
22
+ Parameters:
23
+ bag_file: path/name of bag file
24
+ topics: if not None, only messages from these topics will be returned.
25
+ start_time: an integer nanosecond timestamp. messages logged before this timestamp are not included.
26
+ end_time: an integer nanosecond timestamp. messages logged after this timestamp are not included.
27
+ log_time_order: if True, messages will be yielded in ascending log time order.
28
+ if False, messages will be yielded in the order they appear in the MCAP file.
29
+ reverse: if both `log_time_order` and `reverse` are True, messages will be
30
+ yielded in descending log time order.
31
+ """
32
+ with open(bag_file, "rb") as fobj:
33
+ reader = make_reader(fobj, decoder_factories=[DecoderFactory()])
34
+ for schema, channel, message, ros_msg in reader.iter_decoded_messages(topics=topics,
35
+ start_time=start_time,
36
+ end_time=end_time,
37
+ log_time_order=log_time_order,
38
+ reverse=reverse):
39
+ yield channel.topic, ros_msg, message.log_time
40
+
41
+
42
+ def members(msg):
43
+ """ Return the public members of a message """
44
+ return [attr for attr in dir(msg) if
45
+ not callable(getattr(msg, attr)) and
46
+ not attr.startswith("_") and
47
+ attr not in ['real', 'imag', 'numerator', 'denominator']]
48
+
49
+
50
+ # https://stackoverflow.com/a/31174427
51
+ def rgetattr(obj, attr, *args):
52
+ def _getattr(obj, attr):
53
+ return getattr(obj, attr, *args)
54
+ return functools.reduce(_getattr, [obj] + attr.split('.'))
55
+
56
+
57
+ # https://docs.python.org/3/library/operator.html#operator.attrgetter
58
+ def resolve_attr(obj, attr):
59
+ for name in attr.split("."):
60
+ if name in [f'{idx}' for idx in range(100)]:
61
+ # interpret field names of simple numbers as array indices
62
+ try:
63
+ idx = int(name)
64
+ obj = obj[idx]
65
+ except IndexError:
66
+ obj = None
67
+ else:
68
+ obj = getattr(obj, name)
69
+ return obj
70
+
71
+
72
+ class MessageSchema:
73
+ """ Describes how we decompose a message into a DataFrame entry """
74
+
75
+ def __init__(self, topic, msg, prefix_with_topic=False):
76
+ """ Generate the schema from an example message """
77
+ self._topic = topic
78
+ self._name_indexed_fields = []
79
+ self._names = None
80
+ self._field_map = {} # alternative approach
81
+
82
+ if 'name' in members(msg) and isinstance(getattr(msg, 'name'), Iterable):
83
+ # Lots of ROS messages use parallel arrays that are indexed by a `name` array
84
+ # We want to unwrap these for the user
85
+ # TODO: figure out how to handle multiple messages on same topic with different name lists
86
+ # TODO: Push parsing of name-indexed arrays down into recursive approach below so that
87
+ # it works at more than just the top level
88
+ self._names = getattr(msg, 'name')
89
+ for member_name in members(msg):
90
+ member_var = getattr(msg, member_name)
91
+ if member_name == 'name':
92
+ pass
93
+ elif isinstance(member_var, Iterable) \
94
+ and len(member_var) == len(self._names) \
95
+ and not isinstance(member_var, str):
96
+ # This field is the same length as the name field, so we will assume they are paired
97
+ self._name_indexed_fields.append(member_name)
98
+ for idx, name in enumerate(self._names):
99
+ self._parse_field(key_name=f'{member_name}.{name}',
100
+ data_name=f'{member_name}.{idx}',
101
+ var=member_var[idx])
102
+ else:
103
+ # not name-index, so we'll deal with it separately
104
+ pass
105
+
106
+ for member_name in members(msg):
107
+ member_var = getattr(msg, member_name)
108
+ if member_name in self._name_indexed_fields or member_name == 'name':
109
+ # already dealt with these
110
+ pass
111
+ else:
112
+ self._parse_field(key_name=member_name, data_name=member_name, var=member_var)
113
+
114
+ if prefix_with_topic:
115
+ prefixed_field_map = {}
116
+ for column, member_name in self._field_map.items():
117
+ if column in ['msg_timestamp', 'header', 'stamp']:
118
+ # there are a few special fields that we do NOT want to be unique between topics
119
+ prefixed_field_map[column] = member_name
120
+ else:
121
+ prefixed_field_map[f'{self._topic}.{column}'] = member_name
122
+ self._field_map = prefixed_field_map
123
+
124
+ # print(f'Header for {self._topic} is {self.header}')
125
+
126
+ def _parse_field(self, key_name, data_name, var):
127
+ """ Parse an individual field in the data structure, likely to recurse! """
128
+ # print(f'parse_field({key_name}, {data_name}, {var}')
129
+ # print(f'members = {members(var)}')
130
+ # If this looks like a struct, recurse down into each named member
131
+ if len(members(var)):
132
+ for member in members(var):
133
+ member_var = getattr(var, member)
134
+ self._parse_field(key_name=f'{key_name}.{member}',
135
+ data_name=f'{data_name}.{member}',
136
+ var=member_var)
137
+
138
+ # If this looks like an array, recurse down into each numbered element
139
+ elif isinstance(var, Iterable) \
140
+ and len(var) <= MAX_ARRAY_SIZE_TO_SPLIT \
141
+ and not isinstance(var, str) \
142
+ and key_name not in ['arg', 'changed_parameters', 'deleted_parameters']:
143
+ # TODO: figure out how to handle arrays (such as `arg` field) with variable lengths message to message
144
+ for idx in range(len(var)):
145
+ # recurse down into each sub_member
146
+ self._parse_field(key_name=f'{key_name}.{idx}',
147
+ data_name=f'{data_name}.{idx}',
148
+ var=var[idx])
149
+ else: # The thing in the array looks like a scalar, so go ahead and add it to the field map
150
+ self._field_map.update(
151
+ {f'{key_name}.{idx}': f'{data_name}.{idx}' for idx in range(len(var))})
152
+
153
+ # Otherwise, presume that we have reached the end of the tree and add this field to the map
154
+ elif key_name != 'name':
155
+ # No special situations found, so just use each member as its own column
156
+ self._field_map[f'{key_name}'] = f'{data_name}'
157
+
158
+ @property
159
+ def header(self):
160
+ return tuple(['msg_timestamp'] + list(self._field_map.keys()))
161
+
162
+ def generate_row(self, timestamp, msg):
163
+ """ Use the pre-determined schema to generate one row in the dataframe """
164
+ row_data = [timestamp]
165
+
166
+ for header, member_name in self._field_map.items():
167
+ row_data.append(resolve_attr(msg, member_name))
168
+
169
+ return row_data
170
+
171
+
172
+ class BagFileParser:
173
+ """ Class-based parser to mimic existing sql-based parser interface """
174
+
175
+ def __init__(self, bag_file, use_pickle_if_exists=False):
176
+ self._bag_file: pathlib.Path = bag_file
177
+ self._pickle_file = pathlib.Path(str(self._bag_file).replace('.mcap', '.pickle'))
178
+
179
+ # Write to/read from a locally stored dataframe for faster post-processing
180
+ self._use_pickle_if_exists: bool = use_pickle_if_exists
181
+
182
+ def _get_summary(self):
183
+ with open(self._bag_file, "rb") as fobj:
184
+ reader = make_reader(fobj, decoder_factories=[DecoderFactory()])
185
+ summary = reader.get_summary()
186
+ return summary
187
+
188
+ def set_pickle_path(self, path):
189
+ self._pickle_file = path
190
+
191
+ @property
192
+ def message_start_time(self):
193
+ return self._get_summary().statistics.message_start_time
194
+
195
+ @property
196
+ def message_end_time(self):
197
+ return self._get_summary().statistics.message_end_time
198
+
199
+ @property
200
+ def topics(self):
201
+ summary = self._get_summary()
202
+ topics = [channel.topic for idx, channel in summary.channels.items()]
203
+ return tuple(topics)
204
+
205
+ @property
206
+ def message_counts(self):
207
+ summary = self._get_summary()
208
+ message_counts = {}
209
+ for idx, count in summary.statistics.channel_message_counts.items():
210
+ message_counts[summary.channels[idx].topic] = count
211
+ return message_counts
212
+
213
+ def read_messages(self, *args, **kwargs):
214
+ return read_messages(self._bag_file, *args, **kwargs)
215
+
216
+ def topic_to_dataframe(self, topic: str,
217
+ start_time: Optional[int] = None, # [ns]
218
+ end_time: Optional[int] = None, # [ns]
219
+ rel_start_time: Optional[float] = None, # [s]
220
+ rel_end_time: Optional[float] = None,): # [s]
221
+ """ Generate a pandas.DataFrame from a single topic """
222
+ if rel_start_time is not None:
223
+ start_time = self.message_start_time + int(1E9*rel_start_time)
224
+ if rel_end_time is not None:
225
+ end_time = self.message_start_time + int(1E9*rel_end_time)
226
+
227
+ # TODO: fix message counts for loading a partial bagfile
228
+ print(f'Expect {self.message_counts[topic]} messages for {topic}')
229
+ schema = None # have to wait until we get the first message
230
+ data = []
231
+ msg_count = 0
232
+ for topic, msg, time in self.read_messages(topics=[topic], start_time=start_time, end_time=end_time):
233
+ if schema is None:
234
+ # figure out how we are going to represent this type of message in the dataframe
235
+ schema = MessageSchema(topic=topic, msg=msg)
236
+ data.append(schema.generate_row(timestamp=time, msg=msg))
237
+ msg_count += 1
238
+ if msg_count % 10000 == 0:
239
+ print(f'Processed {msg_count} messages')
240
+ print('Generating data frame')
241
+ df = pd.DataFrame(data, columns=schema.header)
242
+ df.set_index('msg_timestamp', inplace=True)
243
+ return df
244
+
245
+ def has_messages_for_topic(self, topic: str) -> bool:
246
+ return topic in self.message_counts
247
+
248
+ def check_for_existing_data(self) -> bool:
249
+ if pathlib.Path.exists(self._pickle_file):
250
+ return True
251
+ return False
252
+
253
+ def to_dataframe(self, topics: Optional[Iterable[str]] = None,
254
+ start_time: Optional[int] = None, # [ns]
255
+ end_time: Optional[int] = None, # [ns]
256
+ rel_start_time: Optional[float] = None, # [s]
257
+ rel_end_time: Optional[float] = None, # [s]
258
+ fillna=False,
259
+ generate_relative_timestamps=True):
260
+ """ Generate a pandas.DataFrame from a list of topics
261
+
262
+ DataFrame field names will be prepended with the topic name
263
+ """
264
+ if rel_start_time is not None:
265
+ start_time = self.message_start_time + int(1E9*rel_start_time)
266
+ if rel_end_time is not None:
267
+ end_time = self.message_start_time + int(1E9*rel_end_time)
268
+
269
+ if self._use_pickle_if_exists:
270
+ # Check if we already have the dataframe downloaded. If so, just use that, instead of re-parsing the MCAP.
271
+ if self.check_for_existing_data():
272
+ with open(self._pickle_file, 'rb') as f:
273
+ # The protocol version used is detected automatically, so we do not
274
+ # have to specify it.
275
+ mega_data_frame = pickle.load(f)
276
+ return mega_data_frame
277
+ else:
278
+ print(f'Data not found at {self._pickle_file}. Proceeding to MCAP parsing...')
279
+
280
+ if topics is None:
281
+ print(f'Expected message counts are {self.message_counts}')
282
+ else:
283
+ for topic in topics:
284
+ print(f'Expect {self.message_counts[topic]} messages for {topic}')
285
+
286
+ schemas = {}
287
+ data_tables = defaultdict(list)
288
+ msg_count = 0
289
+ for topic, msg, time in self.read_messages(topics=topics, start_time=start_time, end_time=end_time):
290
+ if topic not in schemas:
291
+ print(f'Trying to infer schema for {topic}')
292
+ # figure out how we are going to represent this type of message in the dataframe
293
+ schemas[topic] = MessageSchema(topic=topic, msg=msg, prefix_with_topic=True)
294
+ data_tables[topic].append(schemas[topic].generate_row(timestamp=time, msg=msg))
295
+ msg_count += 1
296
+ if msg_count % 10000 == 0:
297
+ print(f'Processed {msg_count} messages')
298
+
299
+ # create a dataframe for each topic, and then concatenate
300
+ print('Generating data frames')
301
+ dataframes = []
302
+ for topic in schemas:
303
+ dataframes.append(pd.DataFrame(data_tables[topic], columns=schemas[topic].header))
304
+ dataframes[-1].set_index('msg_timestamp', drop=False, inplace=True)
305
+ print('Concatenating data frames')
306
+ mega_data_frame = pd.concat(dataframes)
307
+ print('Sorting resultant data frame')
308
+ mega_data_frame.sort_index(inplace=True)
309
+
310
+ if generate_relative_timestamps:
311
+ print('Creating relative timestamps')
312
+ mega_data_frame['rel_timestamp'] = (mega_data_frame.index - self.message_start_time) / 1E9
313
+ mega_data_frame.set_index('rel_timestamp', drop=False, inplace=True)
314
+ if fillna:
315
+ print('Filling NaNs')
316
+ mega_data_frame.fillna(method='ffill', inplace=True)
317
+
318
+ if self._use_pickle_if_exists:
319
+ # If we've gotten here, it's because we didn't find an existing pickle, so let's create one for this data
320
+ with open(self._pickle_file, 'wb') as f:
321
+ pickle.dump(mega_data_frame, f, pickle.HIGHEST_PROTOCOL)
322
+ print(f'Stored data frame at: {self._pickle_file}')
323
+
324
+ print('Done!')
325
+ return mega_data_frame
@@ -0,0 +1,272 @@
1
+ rosbag2_bagfile_information:
2
+ version: 5
3
+ storage_identifier: mcap
4
+ duration:
5
+ nanoseconds: 42730368608
6
+ starting_time:
7
+ nanoseconds_since_epoch: 1694016529830946819
8
+ message_count: 25387
9
+ topics_with_message_count:
10
+ - topic_metadata:
11
+ name: /visualizer/secondary_camera_pose
12
+ type: geometry_msgs/msg/Pose
13
+ serialization_format: cdr
14
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
15
+ message_count: 13
16
+ - topic_metadata:
17
+ name: /visualizer/primary_camera_pose
18
+ type: geometry_msgs/msg/Pose
19
+ serialization_format: cdr
20
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
21
+ message_count: 13
22
+ - topic_metadata:
23
+ name: /robot/sensor_status
24
+ type: capstan/msg/RobotSensorStatus
25
+ serialization_format: cdr
26
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
27
+ message_count: 76
28
+ - topic_metadata:
29
+ name: /device/state
30
+ type: capstan/msg/DeviceState
31
+ serialization_format: cdr
32
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
33
+ message_count: 3999
34
+ - topic_metadata:
35
+ name: /operator_interface/input_event
36
+ type: capstan/msg/StateMachineEvent
37
+ serialization_format: cdr
38
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
39
+ message_count: 0
40
+ - topic_metadata:
41
+ name: /reference_hardstops/_action/status
42
+ type: action_msgs/msg/GoalStatusArray
43
+ serialization_format: cdr
44
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
45
+ message_count: 0
46
+ - topic_metadata:
47
+ name: /robot/simulate_device_installed
48
+ type: std_msgs/msg/Bool
49
+ serialization_format: cdr
50
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
51
+ message_count: 0
52
+ - topic_metadata:
53
+ name: /system/state
54
+ type: std_msgs/msg/String
55
+ serialization_format: cdr
56
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
57
+ message_count: 189
58
+ - topic_metadata:
59
+ name: /fault/action
60
+ type: capstan/msg/FaultAction
61
+ serialization_format: cdr
62
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
63
+ message_count: 0
64
+ - topic_metadata:
65
+ name: /robot/event
66
+ type: std_msgs/msg/String
67
+ serialization_format: cdr
68
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
69
+ message_count: 0
70
+ - topic_metadata:
71
+ name: /reference_hardstops/_action/feedback
72
+ type: capstan/action/ReferenceHardstops_FeedbackMessage
73
+ serialization_format: cdr
74
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
75
+ message_count: 0
76
+ - topic_metadata:
77
+ name: /device/commanded_pose
78
+ type: capstan/msg/DevicePose
79
+ serialization_format: cdr
80
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
81
+ message_count: 3998
82
+ - topic_metadata:
83
+ name: /device/command
84
+ type: capstan/msg/StateMachineEvent
85
+ serialization_format: cdr
86
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
87
+ message_count: 6
88
+ - topic_metadata:
89
+ name: /device/status
90
+ type: capstan/msg/DeviceStatus
91
+ serialization_format: cdr
92
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
93
+ message_count: 3999
94
+ - topic_metadata:
95
+ name: /robot/joint_command
96
+ type: capstan/msg/RobotJointCommand
97
+ serialization_format: cdr
98
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
99
+ message_count: 3998
100
+ - topic_metadata:
101
+ name: /user_input/implant
102
+ type: capstan/msg/Implant
103
+ serialization_format: cdr
104
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
105
+ message_count: 0
106
+ - topic_metadata:
107
+ name: /presence/state
108
+ type: std_msgs/msg/String
109
+ serialization_format: cdr
110
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
111
+ message_count: 176
112
+ - topic_metadata:
113
+ name: /device/event
114
+ type: capstan/msg/StateMachineEvent
115
+ serialization_format: cdr
116
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
117
+ message_count: 2
118
+ - topic_metadata:
119
+ name: /user_input/implant_twist
120
+ type: geometry_msgs/msg/Twist
121
+ serialization_format: cdr
122
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
123
+ message_count: 0
124
+ - topic_metadata:
125
+ name: /rosout
126
+ type: rcl_interfaces/msg/Log
127
+ serialization_format: cdr
128
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 10\n nsec: 0\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 10\n nsec: 0\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 10\n nsec: 0\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 10\n nsec: 0\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 10\n nsec: 0\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
129
+ message_count: 166
130
+ - topic_metadata:
131
+ name: /user_input/pose_absolute
132
+ type: capstan/msg/DevicePose
133
+ serialization_format: cdr
134
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
135
+ message_count: 0
136
+ - topic_metadata:
137
+ name: /robot/joint_override
138
+ type: capstan/msg/RobotJointState
139
+ serialization_format: cdr
140
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
141
+ message_count: 0
142
+ - topic_metadata:
143
+ name: /user_input/lever
144
+ type: capstan/msg/Lever
145
+ serialization_format: cdr
146
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
147
+ message_count: 0
148
+ - topic_metadata:
149
+ name: /robot/joint_state
150
+ type: capstan/msg/RobotJointState
151
+ serialization_format: cdr
152
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
153
+ message_count: 4217
154
+ - topic_metadata:
155
+ name: /device/estimated_pose
156
+ type: capstan/msg/DevicePose
157
+ serialization_format: cdr
158
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
159
+ message_count: 3998
160
+ - topic_metadata:
161
+ name: /fault/detail
162
+ type: capstan/msg/FaultDetail
163
+ serialization_format: cdr
164
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
165
+ message_count: 85
166
+ - topic_metadata:
167
+ name: /joy
168
+ type: sensor_msgs/msg/Joy
169
+ serialization_format: cdr
170
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
171
+ message_count: 0
172
+ - topic_metadata:
173
+ name: /carriage_clutch/state
174
+ type: std_msgs/msg/String
175
+ serialization_format: cdr
176
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
177
+ message_count: 171
178
+ - topic_metadata:
179
+ name: /fault/config
180
+ type: capstan/msg/FaultConfig
181
+ serialization_format: cdr
182
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
183
+ message_count: 0
184
+ - topic_metadata:
185
+ name: /imu/camera_orientation
186
+ type: capstan/msg/CameraOrientation
187
+ serialization_format: cdr
188
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
189
+ message_count: 0
190
+ - topic_metadata:
191
+ name: /fault/status
192
+ type: capstan/msg/FaultStatus
193
+ serialization_format: cdr
194
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
195
+ message_count: 85
196
+ - topic_metadata:
197
+ name: /motionless_self_test/_action/feedback
198
+ type: capstan/action/MotionlessSelfTest_FeedbackMessage
199
+ serialization_format: cdr
200
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
201
+ message_count: 0
202
+ - topic_metadata:
203
+ name: /user/event
204
+ type: capstan/msg/StateMachineEvent
205
+ serialization_format: cdr
206
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
207
+ message_count: 1
208
+ - topic_metadata:
209
+ name: /joy/set_feebback
210
+ type: sensor_msgs/msg/JoyFeedback
211
+ serialization_format: cdr
212
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
213
+ message_count: 0
214
+ - topic_metadata:
215
+ name: /events/write_split
216
+ type: rosbag2_interfaces/msg/WriteSplitEvent
217
+ serialization_format: cdr
218
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
219
+ message_count: 0
220
+ - topic_metadata:
221
+ name: /user_input/angle_plane
222
+ type: capstan/msg/AnglePlane
223
+ serialization_format: cdr
224
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
225
+ message_count: 0
226
+ - topic_metadata:
227
+ name: /motionless_self_test/_action/status
228
+ type: action_msgs/msg/GoalStatusArray
229
+ serialization_format: cdr
230
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 1\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
231
+ message_count: 2
232
+ - topic_metadata:
233
+ name: /visualizer/device_pose
234
+ type: geometry_msgs/msg/Pose
235
+ serialization_format: cdr
236
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
237
+ message_count: 12
238
+ - topic_metadata:
239
+ name: /user_input/auto_relax
240
+ type: capstan/msg/AutoRelax
241
+ serialization_format: cdr
242
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
243
+ message_count: 0
244
+ - topic_metadata:
245
+ name: /operator_interface/state
246
+ type: std_msgs/msg/String
247
+ serialization_format: cdr
248
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
249
+ message_count: 171
250
+ - topic_metadata:
251
+ name: /parameter_events
252
+ type: rcl_interfaces/msg/ParameterEvent
253
+ serialization_format: cdr
254
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false\n- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
255
+ message_count: 9
256
+ - topic_metadata:
257
+ name: /operator_interface/event
258
+ type: capstan/msg/StateMachineEvent
259
+ serialization_format: cdr
260
+ offered_qos_profiles: "- history: 3\n depth: 0\n reliability: 1\n durability: 2\n deadline:\n sec: 9223372036\n nsec: 854775807\n lifespan:\n sec: 9223372036\n nsec: 854775807\n liveliness: 1\n liveliness_lease_duration:\n sec: 9223372036\n nsec: 854775807\n avoid_ros_namespace_conventions: false"
261
+ message_count: 1
262
+ compression_format: ""
263
+ compression_mode: ""
264
+ relative_file_paths:
265
+ - bagfile_0.mcap
266
+ files:
267
+ - path: bagfile_0.mcap
268
+ starting_time:
269
+ nanoseconds_since_epoch: 1694016529830946819
270
+ duration:
271
+ nanoseconds: 42730368608
272
+ message_count: 25387
@@ -0,0 +1,113 @@
1
+ import pytest
2
+ import pandas as pd
3
+ from collections import defaultdict
4
+ import sys
5
+ import os
6
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
7
+ import mcap_bag_parser # noqa: E402
8
+
9
+
10
+ SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
11
+
12
+
13
+ def test_always_passes():
14
+ assert True
15
+
16
+
17
+ @pytest.fixture
18
+ def parser():
19
+ return mcap_bag_parser.BagFileParser(os.path.join(SCRIPT_DIR, 'bagfile', 'bagfile_0.mcap'))
20
+
21
+
22
+ def test_read_messages():
23
+ num_msgs = defaultdict(lambda: 0)
24
+ for topic, msg, timestamp in mcap_bag_parser.read_messages(
25
+ os.path.join(SCRIPT_DIR, 'bagfile', 'bagfile_0.mcap')):
26
+ num_msgs[topic] = num_msgs[topic] + 1
27
+ # if topic in '/device/status':
28
+ # print(f"{topic} [{timestamp}]: '{msg}'")
29
+
30
+ print(f'Found {num_msgs}')
31
+ assert num_msgs['/rosout'] == 166
32
+ assert num_msgs['/parameter_events'] == 9
33
+ assert num_msgs['/robot/joint_command'] == 3998
34
+ assert num_msgs['/device/estimated_pose'] == 3998
35
+ assert num_msgs['/device/status'] == 3999
36
+ assert num_msgs['/device/state'] == 3999
37
+ assert num_msgs['/device/command'] == 6
38
+
39
+
40
+ def test_read_messages_through_class(parser):
41
+ num_msgs = defaultdict(lambda: 0)
42
+ for topic, msg, timestamp in parser.read_messages():
43
+ num_msgs[topic] = num_msgs[topic] + 1
44
+ # if topic in '/device/status':
45
+ # print(f"{topic} [{timestamp}]: '{msg}'")
46
+
47
+ print(f'Found {num_msgs}')
48
+ assert num_msgs['/rosout'] == 166
49
+ assert num_msgs['/parameter_events'] == 9
50
+ assert num_msgs['/robot/joint_command'] == 3998
51
+ assert num_msgs['/device/estimated_pose'] == 3998
52
+ assert num_msgs['/device/status'] == 3999
53
+ assert num_msgs['/device/state'] == 3999
54
+ assert num_msgs['/device/command'] == 6
55
+
56
+
57
+ def test_topic_to_dataframe(parser):
58
+ df = parser.topic_to_dataframe(topic='/device/command')
59
+ print(f'\n/device/command = \n{df}')
60
+ assert isinstance(df, pd.DataFrame)
61
+ assert len(df) == 6
62
+ assert list(df.columns) == ['arg', 'event']
63
+
64
+ df = parser.topic_to_dataframe(topic='/device/status')
65
+ print(f'\n/device/status = \n{df}')
66
+ assert isinstance(df, pd.DataFrame)
67
+ assert len(df) == 3999
68
+ assert 'active_driving_view' in df.columns
69
+ assert 'state' in df.columns
70
+
71
+ df = parser.topic_to_dataframe(topic='/robot/joint_command')
72
+ print(f'\n/robot/joint_command = \n{df}')
73
+ assert isinstance(df, pd.DataFrame)
74
+ assert len(df) == 3998
75
+ print(df.columns)
76
+ assert 'enable.C1_E' in df.columns
77
+ assert 'position.C3_ROLL' in df.columns
78
+
79
+ df = parser.topic_to_dataframe(topic='/device/estimated_pose')
80
+ print(f'\n/device/estimated_pose = \n{df}')
81
+ assert isinstance(df, pd.DataFrame)
82
+ assert len(df) == 3998
83
+ assert 'section.DISTAL.insertion' in df.columns
84
+ assert 'section.PROXIMAL.roll' in df.columns
85
+
86
+
87
+ def test_to_dataframe(parser):
88
+ df = parser.to_dataframe(topics=['/device/command', '/device/status', '/robot/joint_command'])
89
+ print(f'\ncombined dataframe = \n{df}')
90
+ assert '/device/command.event' in df.columns
91
+ assert '/device/status.state' in df.columns
92
+ assert '/robot/joint_command.enable.C1_E' in df.columns
93
+
94
+
95
+ def test_topics(parser):
96
+ topics = parser.topics
97
+ print(topics)
98
+ assert '/rosout' in topics
99
+ assert '/parameter_events' in topics
100
+ assert '/robot/joint_command' in topics
101
+ assert '/device/estimated_pose' in topics
102
+ assert '/device/status' in topics
103
+ assert '/device/state' in topics
104
+ assert '/device/estimated_pose' in topics
105
+ assert '/device/command' in topics
106
+
107
+
108
+ def test_message_counts(parser):
109
+ message_counts = parser.message_counts
110
+ print(message_counts)
111
+ assert message_counts['/device/command'] == 6
112
+ assert message_counts['/device/estimated_pose'] == 3998
113
+ assert message_counts['/device/status'] == 3999
@@ -0,0 +1,2 @@
1
+ [pytest]
2
+ qt_api=pyqt5