6 from collections
import namedtuple
7 from sick_scan_xd_simu_report
import SickScanXdMsgStatus
9 Jitter = namedtuple(
"Jitter",
"angle_deg, angle_rad, range, velocity, acceleration, intensity")
10 jitter =
Jitter(angle_deg = 0.1, angle_rad = np.deg2rad(0.1), range = 0.001, velocity = 0.1, acceleration = 0.1, intensity = 0.1)
16 from sensor_msgs.msg
import PointCloud2, PointField, LaserScan, Imu
18 except ModuleNotFoundError
as exc:
23 from rclpy.node
import Node
24 from sensor_msgs.msg
import PointCloud2, PointField, LaserScan, Imu
26 except ModuleNotFoundError
as exc:
33 def ros_init(os_name = "linux", ros_version = "noetic", node_name = "sick_scan_xd_subscriber"):
35 ros initialization, delegates to ros initialization function depending on system and ros version
37 if ros1_found
and ros_version ==
"noetic":
38 rospy.init_node(node_name)
39 elif ros2_found
and (ros_version ==
"humble" or ros_version ==
"foxy"):
41 elif ros_version ==
"none":
44 print(f
"## ERROR sick_scan_xd_subscriber.ros_init(): ros version {ros_version} not found or not supported")
48 Extension of json encoder to support numpy types, thanks to https://stackoverflow.com/questions/26646362/numpy-array-is-not-json-serializable
49 and https://www.geeksforgeeks.org/fix-type-error-numpy-array-is-not-json-serializable/
52 if isinstance(obj, np.integer):
54 elif isinstance(obj, np.floating):
56 elif isinstance(obj, np.ndarray):
58 return json.JSONEncoder.default(self, obj)
62 Subset of a PointCloud2 message. Used to verify received PointCloud2 messages against reference (groundtruth) values
66 Initializing constructor
68 self.
frame_id = msg.header.frame_id
if msg
is not None else ""
69 self.
width = msg.width
if msg
is not None else 0
70 self.
height = msg.height
if msg
is not None else 0
71 self.
point_step = msg.point_step
if msg
is not None else 0
72 self.
row_step = msg.row_step
if msg
is not None else 0
73 self.
fields = msg.fields
if msg
is not None else []
74 self.
data = msg.data
if msg
is not None else []
77 Converts and returns all member variables to a serializable dictionary
81 fields.append({
"name": field.name,
"offset": field.offset,
"datatype": field.datatype,
"count": field.count})
82 hex_data_str =
"".join(
"{:02x}".format(x)
for x
in self.
data)
94 Returns True, if reference_msg and received_msg are identical except for the sequence counter and small floating point differences.
96 return (reference_msg[
"frame_id"] == received_msg[
"frame_id"])
and \
97 (reference_msg[
"width"] == received_msg[
"width"])
and \
98 (reference_msg[
"height"] == received_msg[
"height"])
and \
99 (reference_msg[
"point_step"] == received_msg[
"point_step"])
and \
100 (reference_msg[
"row_step"] == received_msg[
"row_step"])
and \
101 (self.
identical_fields(reference_msg[
"fields"], received_msg[
"fields"]))
and \
102 (self.
identical_pointclouds(reference_msg[
"data"], received_msg[
"data"], reference_msg[
"width"], reference_msg[
"height"], reference_msg[
"point_step"], reference_msg[
"row_step"], reference_msg[
"fields"]))
105 Returns True, if reference_fields and received_fields are identical
107 if (len(reference_fields) != len(received_fields)):
109 for field_idx
in range(len(reference_fields)):
110 if reference_fields[field_idx][
"name"] != received_fields[field_idx][
"name"]
or \
111 reference_fields[field_idx][
"offset"] != received_fields[field_idx][
"offset"]
or \
112 reference_fields[field_idx][
"datatype"] != received_fields[field_idx][
"datatype"]
or \
113 reference_fields[field_idx][
"count"] != received_fields[field_idx][
"count"]:
117 field_element_size = 0
118 field_element_dtype = 0
119 if field_datatype == 1:
120 field_element_size = 1
121 field_element_dtype = np.int8
122 elif field_datatype == 2:
123 field_element_size = 1
124 field_element_dtype = np.uint8
125 elif field_datatype == 3:
126 field_element_size = 2
127 field_element_dtype = np.int16
128 elif field_datatype == 4:
129 field_element_size = 2
130 field_element_dtype = np.uint16
131 elif field_datatype == 5:
132 field_element_size = 4
133 field_element_dtype = np.int32
134 elif field_datatype == 6:
135 field_element_size = 4
136 field_element_dtype = np.uint32
137 elif field_datatype == 7:
138 field_element_size = 4
139 field_element_dtype = np.float32
140 elif field_datatype == 8:
141 field_element_size = 8
142 field_element_dtype = np.float64
143 return field_element_dtype, field_element_size
144 def identical_pointclouds(self, reference_pointcloud_hex, received_pointcloud_hex, pointcloud_width, pointcloud_height, pointcloud_point_step, pointcloud_row_step, field_description):
145 if len(reference_pointcloud_hex) != len(received_pointcloud_hex):
148 fields_element_size = []
149 fields_element_dtype = []
150 fields_element_jitter = []
151 for field_idx
in range(len(field_description)):
153 field_element_jitter = jitter.range
154 if field_element_size <= 0:
155 print(f
"## ERROR RefPointcloudMsg.identical_pointclouds: unexpected field: count={field_description[field_idx]['count']}, datatype={field_description[field_idx]['datatype']}")
157 fields_element_size.append(field_element_size)
158 fields_element_dtype.append(field_element_dtype)
159 fields_element_jitter.append(field_element_jitter)
161 ref_pc_bytes = bytes.fromhex(reference_pointcloud_hex)
162 msg_pc_bytes = bytes.fromhex(received_pointcloud_hex)
163 for row
in range(pointcloud_height):
164 for col
in range(pointcloud_width):
165 for field_idx
in range(len(field_description)):
166 field_start_idx = row * pointcloud_row_step + col * pointcloud_point_step + field_description[field_idx][
"offset"]
167 field_end_idx = field_start_idx + fields_element_size[field_idx]
168 ref_field = np.frombuffer(ref_pc_bytes[field_start_idx:field_end_idx], dtype=fields_element_dtype[field_idx])
169 msg_field = np.frombuffer(msg_pc_bytes[field_start_idx:field_end_idx], dtype=fields_element_dtype[field_idx])
170 element_delta = np.abs(ref_field-msg_field)
171 if np.max(element_delta) > fields_element_jitter[field_idx]:
177 Subset of a LaserScan message. Used to verify received LaserScan messages against reference (groundtruth) values
181 Initializing constructor
183 self.
frame_id = msg.header.frame_id
if msg
is not None else ""
184 self.
angle_min = msg.angle_min
if msg
is not None else 0
185 self.
angle_max = msg.angle_max
if msg
is not None else 0
187 self.
ranges = msg.ranges
if msg
is not None else []
191 Converts and returns all member variables to a serializable dictionary
193 ranges_bytes = np.float32(self.
ranges).tobytes()
194 ranges_hex_str =
"".join(
"{:02x}".format(x)
for x
in ranges_bytes)
195 intensities_bytes = np.float32(self.
intensities).tobytes()
196 intensities_hex_str =
"".join(
"{:02x}".format(x)
for x
in intensities_bytes)
202 "ranges": ranges_hex_str,
203 "intensities": intensities_hex_str,
206 floats_ref_bytes = bytes.fromhex(floats_hex_ref)
207 floats_msg_bytes = bytes.fromhex(floats_hex_msg)
208 floats_ref = np.frombuffer(floats_ref_bytes, dtype=np.float32)
209 floats_msg = np.frombuffer(floats_msg_bytes, dtype=np.float32)
210 if len(floats_ref) != len(floats_msg):
212 floats_diff = np.max(np.abs(floats_ref - floats_msg))
213 return floats_diff <= epsilon
216 Returns True, if reference_msg and received_msg are identical except for the sequence counter and small floating point differences.
218 return (reference_msg[
"frame_id"] == received_msg[
"frame_id"])
and \
219 (np.abs(reference_msg[
"angle_min"] - received_msg[
"angle_min"]) < jitter.angle_rad)
and \
220 (np.abs(reference_msg[
"angle_max"] - received_msg[
"angle_max"]) < jitter.angle_rad)
and \
221 (np.abs(reference_msg[
"angle_increment"] - received_msg[
"angle_increment"]) < jitter.angle_rad)
and \
227 Subset of a IMU message. Used to verify received IMU messages against reference (groundtruth) values
231 Initializing constructor
233 self.
frame_id = msg.header.frame_id
if msg
is not None else ""
242 Converts and returns all member variables to a serializable dictionary
255 Returns True, if reference_msg and received_msg are identical except for the sequence counter and small floating point differences.
257 return (reference_msg[
"frame_id"] == received_msg[
"frame_id"])
and \
258 (np.linalg.norm(np.float32(reference_msg[
"orientation"]) - np.float32(received_msg[
"orientation"])) < jitter.angle_rad)
and \
259 (np.linalg.norm(np.float32(reference_msg[
"orientation_covariance"]) - np.float32(received_msg[
"orientation_covariance"])) < jitter.angle_rad**2)
and \
260 (np.linalg.norm(np.float32(reference_msg[
"angular_velocity"]) - np.float32(received_msg[
"angular_velocity"])) < jitter.velocity)
and \
261 (np.linalg.norm(np.float32(reference_msg[
"angular_velocity_covariance"]) - np.float32(received_msg[
"angular_velocity_covariance"])) < jitter.velocity**2)
and \
262 (np.linalg.norm(np.float32(reference_msg[
"linear_acceleration"]) - np.float32(received_msg[
"linear_acceleration"])) < jitter.acceleration)
and \
263 (np.linalg.norm(np.float32(reference_msg[
"linear_acceleration_covariance"]) - np.float32(received_msg[
"linear_acceleration_covariance"])) < jitter.acceleration**2)
267 def __init__(self, os_name = "linux", ros_version = "noetic", pointcloud_subscriber_topic = "", laserscan_subscriber_topic = "", imu_subscriber_topic = ""):
268 if ros2_found
and (ros_version ==
"humble" or ros_version ==
"foxy"):
269 subscriber_name =
"sick_scan_xd_{}_{}_{}".format(pointcloud_subscriber_topic, laserscan_subscriber_topic, imu_subscriber_topic).replace(
"/",
"_")
274 if len(pointcloud_subscriber_topic) > 0:
276 elif len(laserscan_subscriber_topic) > 0:
278 elif len(imu_subscriber_topic) > 0:
281 def create_subscriber(self, os_name, ros_version, topic, msg_type, fct_callback, msg_queue_size):
283 if ros1_found
and ros_version ==
"noetic":
285 elif ros2_found
and (ros_version ==
"humble" or ros_version ==
"foxy"):
286 self.
subscriber = self.create_subscription(msg_type, self.
subscriber_topic, callback=fct_callback, qos_profile=msg_queue_size)
288 print(f
"## ERROR SickScanXdSubscriber.create_subscriber(): ros version {ros_version} not found or not supported")
290 print(f
"## ERROR SickScanXdSubscriber: failed to create subscriber for messages on topic \"{self.subscriber_topic}\"")
292 print(f
"SickScanXdSubscriber: subscribed to {msg_type} messages on topic \"{self.subscriber_topic}\"")
328 ros_init(os_name = config.os_name, ros_version = config.ros_version, node_name =
"sick_scan_xd_simu")
329 for topic
in config.sick_scan_xd_pointcloud_topics:
331 for topic
in config.sick_scan_xd_laserscan_topics:
333 for topic
in config.sick_scan_xd_imu_topics:
335 if run_ros_init
and ros2_found
and (config.ros_version ==
"humble" or config.ros_version ==
"foxy"):
354 num_messages += len(subscriber.messages_received)
357 num_messages += len(subscriber.messages_received)
360 num_messages += len(subscriber.messages_received)
367 with open(jsonfile,
"w")
as file_stream:
368 json.dump(messages, file_stream, indent=2, cls=NumpyJsonEncoder)
369 print(f
"SickScanXdMonitor: {num_messages} messages exported to file \"{jsonfile}\"")
370 except Exception
as exc:
371 print(f
"## ERROR in SickScanXdMonitor.export_received_messages_to_jsonfile(\"{jsonfile}\"): exception {exc}")
373 print(f
"## ERROR SickScanXdMonitor.export_received_messages(): no messages received, file \"{jsonfile}\" not written")
379 with open(jsonfile,
"r")
as file_stream:
381 print(f
"SickScanXdMonitor: messages imported from file \"{jsonfile}\"")
382 except Exception
as exc:
383 print(f
"## ERROR in SickScanXdMonitor.import_received_messages_from_jsonfile(\"{jsonfile}\"): exception {exc}")
388 reference_messages = {}
390 reference_messages = json.load(file_stream)
392 num_messages_verified = 0
393 for msg_type
in reference_messages.keys():
394 if self.
ros_version ==
"none" and msg_type ==
"RefLaserscanMsg":
396 for topic
in reference_messages[msg_type].keys():
397 if self.
ros_version ==
"none" and topic ==
"/cloud_all_fields_fullframe":
401 for ref_msg
in reference_messages[msg_type][topic]:
402 if topic
not in received_messages[msg_type]
or not self.
find_message(ref_msg, received_messages[msg_type][topic], converter[msg_type]):
403 ref_msg_frame_id = ref_msg[
"frame_id"]
404 self.
print_message(report, SickScanXdMsgStatus.ERROR, f
"## ERROR in SickScanXdMonitor.verify_messages(): reference message not found in received messages (msg type: {msg_type}, topic: {topic}, frame_id: {ref_msg_frame_id}), test failed")
406 num_messages_verified = num_messages_verified + 1
407 self.
print_message(report, SickScanXdMsgStatus.INFO, f
"SickScanXdMonitor.verify_messages(): {num_messages_verified} reference messages verified.")
414 except Exception
as exc:
415 self.
print_message(report, SickScanXdMsgStatus.ERROR, f
"## ERROR in SickScanXdMonitor.verify_messages(): exception {exc}")
420 report.append_message(msg_status, message)
424 for received_message
in received_messages:
425 if converter.identical_messages(reference_msg, received_message):
429 if __name__ ==
'__main__':
431 sick_scan_xd_subscriber =
SickScanXdSubscriber(os_name =
"linux", ros_version =
"noetic", pointcloud_subscriber_topic =
"cloud_all_fields_fullframe")
433 while not rospy.is_shutdown():
436 print(
"## ERROR sick_scan_xd_subscriber: expected ros version not found or not supported")