34
34
import functools
35
35
import math
36
36
import threading
37
+ import time
37
38
38
39
import rclpy
39
40
40
41
from rclpy .clock import Clock
41
42
from rclpy .clock import ClockType
43
+ from rclpy .executors import ExternalShutdownException
42
44
from rclpy .qos import qos_profile_sensor_data
43
45
from ros2cli .node .direct import add_arguments as add_direct_node_arguments
44
46
from ros2cli .node .direct import DirectNode
@@ -56,7 +58,8 @@ class HzVerb(VerbExtension):
56
58
def add_arguments (self , parser , cli_name ):
57
59
arg = parser .add_argument (
58
60
'topic_name' ,
59
- help = "Name of the ROS topic to listen to (e.g. '/chatter')" )
61
+ nargs = '+' ,
62
+ help = "Names of the ROS topic to listen to (e.g. '/chatter')" )
60
63
arg .completer = TopicNameCompleter (
61
64
include_hidden_topics_key = 'include_hidden_topics' )
62
65
parser .add_argument (
@@ -80,7 +83,7 @@ def main(self, *, args):
80
83
81
84
82
85
def main (args ):
83
- topic = args .topic_name
86
+ topics = args .topic_name
84
87
if args .filter_expr :
85
88
def expr_eval (expr ):
86
89
def eval_fn (m ):
@@ -91,7 +94,7 @@ def eval_fn(m):
91
94
filter_expr = None
92
95
93
96
with DirectNode (args ) as node :
94
- _rostopic_hz (node .node , topic , window_size = args .window_size , filter_expr = filter_expr ,
97
+ _rostopic_hz (node .node , topics , window_size = args .window_size , filter_expr = filter_expr ,
95
98
use_wtime = args .use_wtime )
96
99
97
100
@@ -202,13 +205,10 @@ def get_hz(self, topic=None):
202
205
"""
203
206
if not self .get_times (topic = topic ):
204
207
return
205
- elif self .get_last_printed_tn (topic = topic ) == 0 :
206
- self .set_last_printed_tn (self .get_msg_tn (topic = topic ), topic = topic )
207
- return
208
- elif self .get_msg_tn (topic = topic ) < self .get_last_printed_tn (topic = topic ) + 1e9 :
208
+ elif self .get_msg_tn (topic = topic ) == self .get_last_printed_tn (topic = topic ):
209
209
return
210
210
with self .lock :
211
- # Get frequency every one minute
211
+ # Get frequency every one second
212
212
times = self .get_times (topic = topic )
213
213
n = len (times )
214
214
mean = sum (times ) / n
@@ -225,43 +225,108 @@ def get_hz(self, topic=None):
225
225
226
226
return rate , min_delta , max_delta , std_dev , n
227
227
228
- def print_hz (self , topic = None ):
228
+ def print_hz (self , topics = None ):
229
229
"""Print the average publishing rate to screen."""
230
- ret = self .get_hz (topic )
231
- if ret is None :
232
- return
233
- rate , min_delta , max_delta , std_dev , window = ret
234
- print ('average rate: %.3f\n \t min: %.3fs max: %.3fs std dev: %.5fs window: %s'
235
- % (rate * 1e9 , min_delta * 1e-9 , max_delta * 1e-9 , std_dev * 1e-9 , window ))
236
- return
237
230
231
+ def get_format_hz (stat ):
232
+ return stat [0 ] * 1e9 , stat [1 ] * 1e-9 , stat [2 ] * 1e-9 , stat [3 ] * 1e-9 , stat [4 ]
233
+
234
+ if len (topics ) == 1 :
235
+ ret = self .get_hz (topics [0 ])
236
+ if ret is None :
237
+ return
238
+ rate , min_delta , max_delta , std_dev , window = get_format_hz (ret )
239
+ print ('average rate: %.3f\n \t min: %.3fs max: %.3fs std dev: %.5fs window: %s'
240
+ % (rate , min_delta , max_delta , std_dev , window ))
241
+ return
238
242
239
- def _rostopic_hz (node , topic , window_size = DEFAULT_WINDOW_SIZE , filter_expr = None , use_wtime = False ):
243
+ # monitoring multiple topics' hz
244
+ header = ['topic' , 'rate' , 'min_delta' , 'max_delta' , 'std_dev' , 'window' ]
245
+ stats = {h : [] for h in header }
246
+ for topic in topics :
247
+ hz_stat = self .get_hz (topic )
248
+ if hz_stat is None :
249
+ continue
250
+ rate , min_delta , max_delta , std_dev , window = get_format_hz (hz_stat )
251
+ stats ['topic' ].append (topic )
252
+ stats ['rate' ].append ('{:.3f}' .format (rate ))
253
+ stats ['min_delta' ].append ('{:.3f}' .format (min_delta ))
254
+ stats ['max_delta' ].append ('{:.3f}' .format (max_delta ))
255
+ stats ['std_dev' ].append ('{:.5f}' .format (std_dev ))
256
+ stats ['window' ].append (str (window ))
257
+ if not stats ['topic' ]:
258
+ return
259
+ print (_get_ascii_table (header , stats ))
260
+
261
+
262
+ def _get_ascii_table (header , cols ):
263
+ # compose table with left alignment
264
+ header_aligned = []
265
+ col_widths = []
266
+ for h in header :
267
+ col_width = max (len (h ), max (len (el ) for el in cols [h ]))
268
+ col_widths .append (col_width )
269
+ header_aligned .append (h .center (col_width ))
270
+ for i , el in enumerate (cols [h ]):
271
+ cols [h ][i ] = str (cols [h ][i ]).ljust (col_width )
272
+ # sum of col and each 3 spaces width
273
+ table_width = sum (col_widths ) + 3 * (len (header ) - 1 )
274
+ n_rows = len (cols [header [0 ]])
275
+ body = '\n ' .join (' ' .join (cols [h ][i ] for h in header ) for i in range (n_rows ))
276
+ table = '{header}\n {hline}\n {body}\n ' .format (
277
+ header = ' ' .join (header_aligned ), hline = '=' * table_width , body = body )
278
+ return table
279
+
280
+
281
+ def _rostopic_hz (node , topics , window_size = DEFAULT_WINDOW_SIZE , filter_expr = None , use_wtime = False ):
240
282
"""
241
283
Periodically print the publishing rate of a topic to console until shutdown.
242
284
243
- :param topic: topic name , ``list`` of ``str``
285
+ :param topics: list of topic names , ``list`` of ``str``
244
286
:param window_size: number of messages to average over, -1 for infinite, ``int``
245
287
:param filter_expr: Python filter expression that is called with m, the message instance
246
288
"""
247
289
# pause hz until topic is published
248
- msg_class = get_msg_class (
249
- node , topic , blocking = True , include_hidden_topics = True )
250
-
251
- if msg_class is None :
290
+ rt = ROSTopicHz (node , window_size , filter_expr = filter_expr , use_wtime = use_wtime )
291
+ topics_len = len (topics )
292
+ topics_to_be_removed = []
293
+ for topic in topics :
294
+ msg_class = get_msg_class (
295
+ node , topic , blocking = True , include_hidden_topics = True )
296
+
297
+ if msg_class is None :
298
+ topics_to_be_removed .append (topic )
299
+ print ('WARNING: failed to find message type for topic [%s]' % topic )
300
+ continue
301
+
302
+ node .create_subscription (
303
+ msg_class ,
304
+ topic ,
305
+ functools .partial (rt .callback_hz , topic = topic ),
306
+ qos_profile_sensor_data )
307
+ if topics_len > 1 :
308
+ print ('Subscribed to [%s]' % topic )
309
+
310
+ # remove the topics from the list if failed to find message type
311
+ while (topic in topics_to_be_removed ):
312
+ topics .remove (topic )
313
+ if len (topics ) == 0 :
252
314
node .destroy_node ()
315
+ rclpy .try_shutdown ()
253
316
return
254
317
255
- rt = ROSTopicHz (node , window_size , filter_expr = filter_expr , use_wtime = use_wtime )
256
- node .create_subscription (
257
- msg_class ,
258
- topic ,
259
- functools .partial (rt .callback_hz , topic = topic ),
260
- qos_profile_sensor_data )
261
-
262
- while rclpy .ok ():
263
- rclpy .spin_once (node )
264
- rt .print_hz (topic )
265
-
266
- node .destroy_node ()
267
- rclpy .shutdown ()
318
+ try :
319
+ def thread_func ():
320
+ while rclpy .ok ():
321
+ rt .print_hz (topics )
322
+ time .sleep (1.0 )
323
+
324
+ print_thread = threading .Thread (target = thread_func )
325
+ print_thread .start ()
326
+ rclpy .spin (node )
327
+ except (KeyboardInterrupt , ExternalShutdownException ):
328
+ pass
329
+ finally :
330
+ node .destroy_node ()
331
+ rclpy .try_shutdown ()
332
+ print_thread .join ()
0 commit comments