From d62207a4d645d17d1a9f225bdbd8e40b2eff917c Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 29 Oct 2018 13:18:53 +0900 Subject: [PATCH 01/54] [jsk_tools] Add speak warnings node --- jsk_tools/src/audible_warning.py | 159 +++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100755 jsk_tools/src/audible_warning.py diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py new file mode 100755 index 000000000..d9e951fce --- /dev/null +++ b/jsk_tools/src/audible_warning.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from collections import defaultdict +import actionlib +import rospy +from threading import Event +from threading import Thread +from threading import Lock +from sound_play.msg import SoundRequest +from sound_play.msg import SoundRequestAction +from sound_play.msg import SoundRequestGoal +from diagnostic_msgs.msg import DiagnosticArray +from diagnostic_msgs.msg import DiagnosticStatus + +cached_paths = {} +cached_result = {} + + +def is_leaf(original_path): + if hasattr(cached_result, original_path): + return cached_result[original_path] + + path = original_path[1:].split('/') + + def _is_leaf(cached, path): + if not isinstance(path, list): + raise ValueError + if len(path) == 0: + raise ValueError + if len(path) == 1: + if path[0] in cached: + return False + else: + return True + + parent = path[0] + child = path[1:] + if parent not in cached: + cached[parent] = {} + return _is_leaf(cached[parent], child) + + result = _is_leaf(cached_paths, path) + if result is False: + cached_result[original_path] = result + return result + + +class SpeakThread(Thread): + + def __init__(self, rate=1.0, wait=True, blacklist=None): + super(SpeakThread, self).__init__() + self.memo = {} + self.event = Event() + self.rate = rate + self.wait = wait + self.lock = Lock() + self.stale = [] + self.error = [] + self.history = defaultdict(lambda: 10) + self.blacklist = blacklist + + self.talk = actionlib.SimpleActionClient( + "/robotsound", SoundRequestAction) + self.talk.wait_for_server() + + self.language = 'en' + + def stop(self): + self.event.set() + + def sort(self, error): + ret = {} + for s in error: + ns = s.name.split()[0] + if is_leaf(ns) is False: + continue + if any(filter(lambda n: n in ns, self.blacklist)): + continue + ret[ns] = s + return ret.values() + + def add(self, stale, error): + with self.lock: + self.stale = self.sort(stale) + self.error = self.sort(error) + + def pop(self): + with self.lock: + for e in self.stale + self.error: + if e.name not in self.history.keys(): + self.history[e.name] += 1 + return e + return None + + def sweep(self): + for k in self.history.keys(): + self.history[k] -= 1 + if self.history[k] == 0: + del self.history[k] + + def run(self): + while not self.event.wait(self.rate): + e = self.pop() + if e: + # rospy.loginfo("audible warning talking: %s" % e) + sentence = e.name + ' ' + e.message + sentence = sentence.replace('/', ' ') + sentence = sentence.replace('_', ' ') + + goal = SoundRequestGoal() + goal.sound_request.sound = SoundRequest.SAY + goal.sound_request.command = SoundRequest.PLAY_ONCE + goal.sound_request.arg = sentence + goal.sound_request.volume = 1.0 + + self.talk.send_goal(goal) + if self.wait: + self.talk.wait_for_result(rospy.Duration(10.0)) + self.sweep() + + +class AudibleWarning(object): + def __init__(self): + self.history = [] + self.error = [] + self.stale = [] + + speak_rate = rospy.get_param("~speak_rate", 1.0) + speak_wait = rospy.get_param("~speak_wait", True) + blacklist = rospy.get_param("~blacklist", []) + self.speak_thread = SpeakThread(speak_rate, speak_wait, blacklist) + + # run-stop + self.run_stop = False + + # diag + self.sub_diag = rospy.Subscriber("/diagnostics_agg", DiagnosticArray, + self.diag_cb, queue_size=1) + self.speak_thread.start() + + def on_shutdown(self): + self.speak_thread.stop() + self.speak_thread.join() + + def diag_cb(self, msg): + if self.run_stop: + return + + error = filter(lambda s: s.level == DiagnosticStatus.ERROR, msg.status) + stale = filter(lambda s: s.level == DiagnosticStatus.STALE, msg.status) + self.speak_thread.add(stale, error) + + +if __name__ == '__main__': + rospy.init_node("audible_warning") + aw = AudibleWarning() # NOQA + rospy.on_shutdown(aw.on_shutdown) + rospy.spin() From 83644d4343881fef9ae6f989acbc0edb87d7686a Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 12 May 2022 22:42:54 +0900 Subject: [PATCH 02/54] Add warn_stale option --- jsk_tools/src/audible_warning.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index d9e951fce..3c2406e80 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -128,6 +128,7 @@ def __init__(self): speak_rate = rospy.get_param("~speak_rate", 1.0) speak_wait = rospy.get_param("~speak_wait", True) + self.warn_stale = rospy.get_param('~warn_stale', False) blacklist = rospy.get_param("~blacklist", []) self.speak_thread = SpeakThread(speak_rate, speak_wait, blacklist) @@ -148,7 +149,11 @@ def diag_cb(self, msg): return error = filter(lambda s: s.level == DiagnosticStatus.ERROR, msg.status) - stale = filter(lambda s: s.level == DiagnosticStatus.STALE, msg.status) + if self.warn_stale: + stale = filter(lambda s: s.level == DiagnosticStatus.STALE, + msg.status) + else: + stale = [] self.speak_thread.add(stale, error) From 37505772fb604872a321db9d19efbb34cd825f55 Mon Sep 17 00:00:00 2001 From: iory Date: Sun, 15 May 2022 16:16:23 +0900 Subject: [PATCH 03/54] Add is_leaf and filter function to jsk_tools'library --- jsk_tools/src/audible_warning.py | 33 +---------- jsk_tools/src/jsk_tools/diagnostics_utils.py | 58 ++++++++++++++++++++ 2 files changed, 60 insertions(+), 31 deletions(-) create mode 100644 jsk_tools/src/jsk_tools/diagnostics_utils.py diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 3c2406e80..d8ae4f516 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -13,37 +13,8 @@ from diagnostic_msgs.msg import DiagnosticArray from diagnostic_msgs.msg import DiagnosticStatus -cached_paths = {} -cached_result = {} - - -def is_leaf(original_path): - if hasattr(cached_result, original_path): - return cached_result[original_path] - - path = original_path[1:].split('/') - - def _is_leaf(cached, path): - if not isinstance(path, list): - raise ValueError - if len(path) == 0: - raise ValueError - if len(path) == 1: - if path[0] in cached: - return False - else: - return True - - parent = path[0] - child = path[1:] - if parent not in cached: - cached[parent] = {} - return _is_leaf(cached[parent], child) - - result = _is_leaf(cached_paths, path) - if result is False: - cached_result[original_path] = result - return result +from jsk_tools.diagnostics_utils import is_leaf +from jsk_tools.diagnostics_utils import filter_diagnostics_status_list class SpeakThread(Thread): diff --git a/jsk_tools/src/jsk_tools/diagnostics_utils.py b/jsk_tools/src/jsk_tools/diagnostics_utils.py new file mode 100644 index 000000000..887f9e5f2 --- /dev/null +++ b/jsk_tools/src/jsk_tools/diagnostics_utils.py @@ -0,0 +1,58 @@ +cached_paths = {} +cached_result = {} + + +def is_leaf(original_path): + if hasattr(cached_result, original_path): + return cached_result[original_path] + + path = original_path[1:].split('/') + + def _is_leaf(cached, path): + if not isinstance(path, list): + raise ValueError + if len(path) == 0: + raise ValueError + if len(path) == 1: + if path[0] in cached: + return False + else: + return True + + parent = path[0] + child = path[1:] + if parent not in cached: + cached[parent] = {} + return _is_leaf(cached[parent], child) + + result = _is_leaf(cached_paths, path) + if result is False: + cached_result[original_path] = result + return result + + +def filter_diagnostics_status_list(status_list, blacklist): + """Filter list of DiagnosticStatus. + + Parameters + ---------- + status_list : diagnostic_msgs.msg._DiagnosticStatus.DiagnosticStatus + List of DiagnosticStatus. + black_list : List[str] + List of blacklist. + The path of the name contained in this list is ignored. + + Returns + ------- + ret.values() : Dict[str, DiagnosticStatus] + Key is namespace and value is status. + """ + ret = {} + for s in status_list: + ns = s.name.split()[0] + if is_leaf(ns) is False: + continue + if any(filter(lambda n: n in ns, blacklist)): + continue + ret[ns] = s + return ret.values() From 2ff889ac823f72642dab2d08599c862d548937d4 Mon Sep 17 00:00:00 2001 From: iory Date: Sun, 15 May 2022 16:17:11 +0900 Subject: [PATCH 04/54] Refactor audible_warning --- jsk_tools/src/audible_warning.py | 54 +++++++++++++------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index d8ae4f516..94470ad57 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -2,70 +2,59 @@ # -*- coding: utf-8 -*- from collections import defaultdict -import actionlib -import rospy from threading import Event -from threading import Thread from threading import Lock +from threading import Thread + +import actionlib +from diagnostic_msgs.msg import DiagnosticArray +from diagnostic_msgs.msg import DiagnosticStatus +import rospy from sound_play.msg import SoundRequest from sound_play.msg import SoundRequestAction from sound_play.msg import SoundRequestGoal -from diagnostic_msgs.msg import DiagnosticArray -from diagnostic_msgs.msg import DiagnosticStatus -from jsk_tools.diagnostics_utils import is_leaf from jsk_tools.diagnostics_utils import filter_diagnostics_status_list class SpeakThread(Thread): - def __init__(self, rate=1.0, wait=True, blacklist=None): + def __init__(self, rate=1.0, wait=True, blacklist=None, + language='en', wait_speak_duration_time=10): super(SpeakThread, self).__init__() - self.memo = {} + self.wait_speak_duration_time = wait_speak_duration_time self.event = Event() self.rate = rate self.wait = wait self.lock = Lock() - self.stale = [] - self.error = [] + self.status_list = [] self.history = defaultdict(lambda: 10) self.blacklist = blacklist + self.language = language self.talk = actionlib.SimpleActionClient( "/robotsound", SoundRequestAction) self.talk.wait_for_server() - self.language = 'en' - def stop(self): self.event.set() - def sort(self, error): - ret = {} - for s in error: - ns = s.name.split()[0] - if is_leaf(ns) is False: - continue - if any(filter(lambda n: n in ns, self.blacklist)): - continue - ret[ns] = s - return ret.values() - - def add(self, stale, error): + def add(self, status_list): with self.lock: - self.stale = self.sort(stale) - self.error = self.sort(error) + self.status_list = filter_diagnostics_status_list( + status_list, + self.blacklist) def pop(self): with self.lock: - for e in self.stale + self.error: - if e.name not in self.history.keys(): - self.history[e.name] += 1 - return e + for status in self.status_list: + if status.name not in self.history.keys(): + self.history[status.name] += 1 + return status return None def sweep(self): - for k in self.history.keys(): + for k in list(self.history.keys()): self.history[k] -= 1 if self.history[k] == 0: del self.history[k] @@ -87,7 +76,8 @@ def run(self): self.talk.send_goal(goal) if self.wait: - self.talk.wait_for_result(rospy.Duration(10.0)) + self.talk.wait_for_result( + rospy.Duration(self.wait_speak_duration_time)) self.sweep() From dd7ddf6a729448292109611b89a38633f77bea27 Mon Sep 17 00:00:00 2001 From: iory Date: Sun, 15 May 2022 16:24:08 +0900 Subject: [PATCH 05/54] Add prefix to speaking --- jsk_tools/src/audible_warning.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 94470ad57..ee41dde05 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -63,10 +63,18 @@ def run(self): while not self.event.wait(self.rate): e = self.pop() if e: - # rospy.loginfo("audible warning talking: %s" % e) - sentence = e.name + ' ' + e.message - sentence = sentence.replace('/', ' ') - sentence = sentence.replace('_', ' ') + if e.level == DiagnosticStatus.WARN: + prefix = 'warning.' + elif e.level == DiagnosticStatus.ERROR: + prefix = 'error.' + elif e.level == DiagnosticStatus.STALE: + prefix = 'stale.' + else: + prefix = 'ok.' + sentence = prefix + e.name + ' ' + e.message + sentence = sentence.replace('/', ' ') + sentence = sentence.replace('_', ' ') + rospy.loginfo("audible warning talking: %s" % sentence) goal = SoundRequestGoal() goal.sound_request.sound = SoundRequest.SAY From e1896776bfd774f2f5f696d055a930ffb0f4331f Mon Sep 17 00:00:00 2001 From: iory Date: Sun, 15 May 2022 16:24:30 +0900 Subject: [PATCH 06/54] Refactor AudibleWarning node --- jsk_tools/src/audible_warning.py | 37 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index ee41dde05..61ed23881 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -90,23 +90,31 @@ def run(self): class AudibleWarning(object): - def __init__(self): - self.history = [] - self.error = [] - self.stale = [] + def __init__(self): speak_rate = rospy.get_param("~speak_rate", 1.0) - speak_wait = rospy.get_param("~speak_wait", True) - self.warn_stale = rospy.get_param('~warn_stale', False) + wait_speak = rospy.get_param("~wait_speak", True) + language = rospy.get_param('~language', 'en') + + self.diagnostics_list = [] + if rospy.get_param("~speak_warn", True): + self.diagnostics_list.append(DiagnosticStatus.WARN) + if rospy.get_param("~speak_error", True): + self.diagnostics_list.append(DiagnosticStatus.ERROR) + if rospy.get_param("~speak_stale", True): + self.diagnostics_list.append(DiagnosticStatus.STALE) + blacklist = rospy.get_param("~blacklist", []) - self.speak_thread = SpeakThread(speak_rate, speak_wait, blacklist) + self.speak_thread = SpeakThread(speak_rate, wait_speak, blacklist, + language) # run-stop self.run_stop = False # diag - self.sub_diag = rospy.Subscriber("/diagnostics_agg", DiagnosticArray, - self.diag_cb, queue_size=1) + self.sub_diag = rospy.Subscriber( + "/diagnostics_agg", DiagnosticArray, + self.diag_cb, queue_size=1) self.speak_thread.start() def on_shutdown(self): @@ -116,14 +124,9 @@ def on_shutdown(self): def diag_cb(self, msg): if self.run_stop: return - - error = filter(lambda s: s.level == DiagnosticStatus.ERROR, msg.status) - if self.warn_stale: - stale = filter(lambda s: s.level == DiagnosticStatus.STALE, - msg.status) - else: - stale = [] - self.speak_thread.add(stale, error) + target_status_list = filter(lambda n: n.level in self.diagnostics_list, + msg.status) + self.speak_thread.add(target_status_list) if __name__ == '__main__': From e93e36355ef9d1d5233b8ba831a240259467f1f6 Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 16 May 2022 22:18:40 +0900 Subject: [PATCH 07/54] [jsk_tools/audible_warning] Use priority queue instead of history --- jsk_tools/src/audible_warning.py | 45 +++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 61ed23881..de07f7b60 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from collections import defaultdict +import heapq from threading import Event from threading import Lock from threading import Thread @@ -20,15 +21,21 @@ class SpeakThread(Thread): def __init__(self, rate=1.0, wait=True, blacklist=None, - language='en', wait_speak_duration_time=10): + language='en', + volume=1.0, + speak_interval=0, + wait_speak_duration_time=10): super(SpeakThread, self).__init__() self.wait_speak_duration_time = wait_speak_duration_time self.event = Event() self.rate = rate self.wait = wait + self.volume = volume self.lock = Lock() self.status_list = [] - self.history = defaultdict(lambda: 10) + self.speak_interval = speak_interval + tm = rospy.Time.now().to_sec() - speak_interval + self.previous_spoken_time = defaultdict(lambda tm=tm: tm) self.blacklist = blacklist self.language = language @@ -41,24 +48,24 @@ def stop(self): def add(self, status_list): with self.lock: - self.status_list = filter_diagnostics_status_list( - status_list, - self.blacklist) + status_list = filter_diagnostics_status_list( + status_list, self.blacklist) + for status in status_list: + heapq.heappush( + self.status_list, + (rospy.Time.now().to_sec(), status)) def pop(self): with self.lock: - for status in self.status_list: - if status.name not in self.history.keys(): - self.history[status.name] += 1 - return status + while len(self.status_list) > 0: + _, status = heapq.heappop(self.status_list) + if rospy.Time.now().to_sec() \ + - self.previous_spoken_time[status.name] \ + < self.speak_interval: + continue + return status return None - def sweep(self): - for k in list(self.history.keys()): - self.history[k] -= 1 - if self.history[k] == 0: - del self.history[k] - def run(self): while not self.event.wait(self.rate): e = self.pop() @@ -80,20 +87,22 @@ def run(self): goal.sound_request.sound = SoundRequest.SAY goal.sound_request.command = SoundRequest.PLAY_ONCE goal.sound_request.arg = sentence - goal.sound_request.volume = 1.0 + goal.sound_request.volume = self.volume + self.previous_spoken_time[e.name] = rospy.Time.now().to_sec() self.talk.send_goal(goal) if self.wait: self.talk.wait_for_result( rospy.Duration(self.wait_speak_duration_time)) - self.sweep() class AudibleWarning(object): def __init__(self): speak_rate = rospy.get_param("~speak_rate", 1.0) + speak_interval = rospy.get_param("~speak_interval", 120.0) wait_speak = rospy.get_param("~wait_speak", True) + volume = rospy.get_param("~volume", 1.0) language = rospy.get_param('~language', 'en') self.diagnostics_list = [] @@ -106,7 +115,7 @@ def __init__(self): blacklist = rospy.get_param("~blacklist", []) self.speak_thread = SpeakThread(speak_rate, wait_speak, blacklist, - language) + language, volume, speak_interval) # run-stop self.run_stop = False From 3e45382f4e30cee31d25aa1a3cd39ecd1b18f970 Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 16 May 2022 22:19:34 +0900 Subject: [PATCH 08/54] [jsk_tools/audible_warning] Enable run_stop to suppress audible warning --- jsk_tools/src/audible_warning.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index de07f7b60..f3d3096a7 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -3,6 +3,7 @@ from collections import defaultdict import heapq +from importlib import import_module from threading import Event from threading import Lock from threading import Thread @@ -18,6 +19,12 @@ from jsk_tools.diagnostics_utils import filter_diagnostics_status_list +def expr_eval(expr): + def eval_fn(topic, m, t): + return eval(expr) + return eval_fn + + class SpeakThread(Thread): def __init__(self, rate=1.0, wait=True, blacklist=None, @@ -119,6 +126,16 @@ def __init__(self): # run-stop self.run_stop = False + self.run_stop_topic = rospy.get_param('~run_stop_topic', None) + if self.run_stop_topic: + run_stop_condition = rospy.get_param( + '~run_stop_condition', 'm.data == True') + self.run_stop_condition = expr_eval(run_stop_condition) + self.run_stop_sub = rospy.Subscriber( + self.run_stop_topic, + rospy.AnyMsg, + callback=self.run_stop_callback, + queue_size=1) # diag self.sub_diag = rospy.Subscriber( @@ -126,12 +143,26 @@ def __init__(self): self.diag_cb, queue_size=1) self.speak_thread.start() + def run_stop_callback(self, msg): + if isinstance(msg, rospy.msg.AnyMsg): + package, msg_type = msg._connection_header['type'].split('/') + ros_pkg = package + '.msg' + msg_class = getattr(import_module(ros_pkg), msg_type) + self.run_stop_sub.unregister() + deserialized_sub = rospy.Subscriber( + self.run_stop_topic, msg_class, self.run_stop_callback) + self.run_stop_sub = deserialized_sub + return + self.run_stop = self.run_stop_condition( + self.run_stop_topic, msg, rospy.Time.now()) + def on_shutdown(self): self.speak_thread.stop() self.speak_thread.join() def diag_cb(self, msg): if self.run_stop: + rospy.logdebug('RUN STOP is pressed. Do not speak warning.') return target_status_list = filter(lambda n: n.level in self.diagnostics_list, msg.status) From 91ecd310f2aaba987ad14aa46b8ab468467b267e Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 16 May 2022 22:30:33 +0900 Subject: [PATCH 09/54] [jsk_tools/audible_warning] Add seconds_to_start_speaking option for initialization --- jsk_tools/src/audible_warning.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index f3d3096a7..7a0cc2a83 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -31,6 +31,7 @@ def __init__(self, rate=1.0, wait=True, blacklist=None, language='en', volume=1.0, speak_interval=0, + seconds_to_start_speaking=0, wait_speak_duration_time=10): super(SpeakThread, self).__init__() self.wait_speak_duration_time = wait_speak_duration_time @@ -41,7 +42,9 @@ def __init__(self, rate=1.0, wait=True, blacklist=None, self.lock = Lock() self.status_list = [] self.speak_interval = speak_interval - tm = rospy.Time.now().to_sec() - speak_interval + tm = rospy.Time.now().to_sec() \ + - speak_interval \ + + seconds_to_start_speaking self.previous_spoken_time = defaultdict(lambda tm=tm: tm) self.blacklist = blacklist self.language = language @@ -111,6 +114,8 @@ def __init__(self): wait_speak = rospy.get_param("~wait_speak", True) volume = rospy.get_param("~volume", 1.0) language = rospy.get_param('~language', 'en') + seconds_to_start_speaking = rospy.get_param( + '~seconds_to_start_speaking', 0) self.diagnostics_list = [] if rospy.get_param("~speak_warn", True): @@ -122,7 +127,8 @@ def __init__(self): blacklist = rospy.get_param("~blacklist", []) self.speak_thread = SpeakThread(speak_rate, wait_speak, blacklist, - language, volume, speak_interval) + language, volume, speak_interval, + seconds_to_start_speaking) # run-stop self.run_stop = False From 2eb51a226dc56a5641ee69c1389196a7bb39d282 Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 16 May 2022 22:30:46 +0900 Subject: [PATCH 10/54] [jsk_tools/audible_warning] Enable language specification --- jsk_tools/src/audible_warning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 7a0cc2a83..05db4aeff 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -97,6 +97,7 @@ def run(self): goal.sound_request.sound = SoundRequest.SAY goal.sound_request.command = SoundRequest.PLAY_ONCE goal.sound_request.arg = sentence + goal.sound_request.language = self.language goal.sound_request.volume = self.volume self.previous_spoken_time[e.name] = rospy.Time.now().to_sec() From aab38ff6f4f10e84669750f09cade9d08ecdc78f Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 16 May 2022 22:31:00 +0900 Subject: [PATCH 11/54] [jsk_tools/audible_warning] Fixed namespace of diagnostics --- jsk_tools/src/jsk_tools/diagnostics_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsk_tools/src/jsk_tools/diagnostics_utils.py b/jsk_tools/src/jsk_tools/diagnostics_utils.py index 887f9e5f2..5504a3d33 100644 --- a/jsk_tools/src/jsk_tools/diagnostics_utils.py +++ b/jsk_tools/src/jsk_tools/diagnostics_utils.py @@ -49,7 +49,7 @@ def filter_diagnostics_status_list(status_list, blacklist): """ ret = {} for s in status_list: - ns = s.name.split()[0] + ns = s.name if is_leaf(ns) is False: continue if any(filter(lambda n: n in ns, blacklist)): From 003b586c65edb6f677b7056c8176768edc2dba9e Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 16 May 2022 22:34:10 +0900 Subject: [PATCH 12/54] [jsk_tools/audible_warning] Fixed typo language to arg2 and set default value to "" --- jsk_tools/src/audible_warning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 05db4aeff..6f9cb5aae 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -28,7 +28,7 @@ def eval_fn(topic, m, t): class SpeakThread(Thread): def __init__(self, rate=1.0, wait=True, blacklist=None, - language='en', + language='', volume=1.0, speak_interval=0, seconds_to_start_speaking=0, @@ -97,7 +97,7 @@ def run(self): goal.sound_request.sound = SoundRequest.SAY goal.sound_request.command = SoundRequest.PLAY_ONCE goal.sound_request.arg = sentence - goal.sound_request.language = self.language + goal.sound_request.arg2 = self.language goal.sound_request.volume = self.volume self.previous_spoken_time[e.name] = rospy.Time.now().to_sec() @@ -114,7 +114,7 @@ def __init__(self): speak_interval = rospy.get_param("~speak_interval", 120.0) wait_speak = rospy.get_param("~wait_speak", True) volume = rospy.get_param("~volume", 1.0) - language = rospy.get_param('~language', 'en') + language = rospy.get_param('~language', '') seconds_to_start_speaking = rospy.get_param( '~seconds_to_start_speaking', 0) From 03e2c6a1c6926f12ba4a1194208963701e06d7cf Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 14:21:04 +0900 Subject: [PATCH 13/54] [jsk_tools/audible_warning] Wait until seconds_to_start_speaking the time has passed. --- jsk_tools/src/audible_warning.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 6f9cb5aae..1d5c0ec6e 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -31,7 +31,6 @@ def __init__(self, rate=1.0, wait=True, blacklist=None, language='', volume=1.0, speak_interval=0, - seconds_to_start_speaking=0, wait_speak_duration_time=10): super(SpeakThread, self).__init__() self.wait_speak_duration_time = wait_speak_duration_time @@ -43,8 +42,7 @@ def __init__(self, rate=1.0, wait=True, blacklist=None, self.status_list = [] self.speak_interval = speak_interval tm = rospy.Time.now().to_sec() \ - - speak_interval \ - + seconds_to_start_speaking + - speak_interval self.previous_spoken_time = defaultdict(lambda tm=tm: tm) self.blacklist = blacklist self.language = language @@ -118,6 +116,14 @@ def __init__(self): seconds_to_start_speaking = rospy.get_param( '~seconds_to_start_speaking', 0) + # Wait until seconds_to_start_speaking the time has passed. + rate = rospy.Rate(10) + start_time = rospy.Time.now() + while not rospy.is_shutdown() \ + and (rospy.Time.now() - start_time).to_sec() \ + < seconds_to_start_speaking: + rate.sleep() + self.diagnostics_list = [] if rospy.get_param("~speak_warn", True): self.diagnostics_list.append(DiagnosticStatus.WARN) From 2fe97c2b50d56ea713c27ee80f60949637af2e9c Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 14:30:07 +0900 Subject: [PATCH 14/54] [jsk_tools/audible_warning] Output error name --- jsk_tools/src/audible_warning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 1d5c0ec6e..b20bf6724 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -89,6 +89,7 @@ def run(self): sentence = prefix + e.name + ' ' + e.message sentence = sentence.replace('/', ' ') sentence = sentence.replace('_', ' ') + rospy.loginfo('audible warning error name "{}"'.format(e.name)) rospy.loginfo("audible warning talking: %s" % sentence) goal = SoundRequestGoal() From d65bacc1d90b3a399b1ce3546d94ce6a78202cce Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 14:44:03 +0900 Subject: [PATCH 15/54] [jsk_tools/audible_warning] Check status is leaf node --- jsk_tools/src/audible_warning.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index b20bf6724..a51e6d483 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -17,6 +17,7 @@ from sound_play.msg import SoundRequestGoal from jsk_tools.diagnostics_utils import filter_diagnostics_status_list +from jsk_tools.diagnostics_utils import is_leaf def expr_eval(expr): @@ -67,6 +68,8 @@ def pop(self): with self.lock: while len(self.status_list) > 0: _, status = heapq.heappop(self.status_list) + if is_leaf(status.name) is False: + continue if rospy.Time.now().to_sec() \ - self.previous_spoken_time[status.name] \ < self.speak_interval: From 793179be0f44b0f9add8b12a0605b8d3ad08576b Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 15:09:13 +0900 Subject: [PATCH 16/54] [jsk_tools/audible_warning] Enable regex for blacklist --- jsk_tools/src/audible_warning.py | 2 ++ jsk_tools/src/jsk_tools/diagnostics_utils.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index a51e6d483..d374d4230 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -4,6 +4,7 @@ from collections import defaultdict import heapq from importlib import import_module +import re from threading import Event from threading import Lock from threading import Thread @@ -137,6 +138,7 @@ def __init__(self): self.diagnostics_list.append(DiagnosticStatus.STALE) blacklist = rospy.get_param("~blacklist", []) + blacklist = list(map(re.compile, blacklist)) self.speak_thread = SpeakThread(speak_rate, wait_speak, blacklist, language, volume, speak_interval, seconds_to_start_speaking) diff --git a/jsk_tools/src/jsk_tools/diagnostics_utils.py b/jsk_tools/src/jsk_tools/diagnostics_utils.py index 5504a3d33..c561a76ea 100644 --- a/jsk_tools/src/jsk_tools/diagnostics_utils.py +++ b/jsk_tools/src/jsk_tools/diagnostics_utils.py @@ -1,3 +1,6 @@ +import re + + cached_paths = {} cached_result = {} @@ -52,7 +55,7 @@ def filter_diagnostics_status_list(status_list, blacklist): ns = s.name if is_leaf(ns) is False: continue - if any(filter(lambda n: n in ns, blacklist)): + if any(filter(lambda n: re.match(n, ns), blacklist)): continue ret[ns] = s return ret.values() From e9550ac87a7dfed9615954091f9d952c6dc88f2f Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 15:33:42 +0900 Subject: [PATCH 17/54] [jsk_tools/audible_warning] Add run_stop blacklist --- jsk_tools/src/audible_warning.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index d374d4230..6c8dc5817 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -144,15 +144,21 @@ def __init__(self): seconds_to_start_speaking) # run-stop + self.speak_when_runstopped = rospy.get_param( + '~speak_when_runstopped', True) self.run_stop = False self.run_stop_topic = rospy.get_param('~run_stop_topic', None) if self.run_stop_topic: run_stop_condition = rospy.get_param( '~run_stop_condition', 'm.data == True') + run_stop_blacklist = rospy.get_param( + '~run_stop_blacklist', []) + self.run_stop_blacklist = list(map(re.compile, run_stop_blacklist)) self.run_stop_condition = expr_eval(run_stop_condition) self.run_stop_sub = rospy.Subscriber( self.run_stop_topic, rospy.AnyMsg, + callback=self.run_stop_callback, queue_size=1) @@ -181,8 +187,17 @@ def on_shutdown(self): def diag_cb(self, msg): if self.run_stop: - rospy.logdebug('RUN STOP is pressed. Do not speak warning.') - return + if self.speak_when_runstopped is False: + rospy.logdebug('RUN STOP is pressed. Do not speak warning.') + return + + filtered_status = [] + for status in msg.status: + for bn in self.run_stop_blacklist: + if re.match(status.name, bn): + filtered_status.append(status) + break + msg.status = filtered_status target_status_list = filter(lambda n: n.level in self.diagnostics_list, msg.status) self.speak_thread.add(target_status_list) From 917f8531baec305f46a78949d17f28d2885a1ce0 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 17:13:28 +0900 Subject: [PATCH 18/54] [jsk_tools/audible_warning] Enable blacklist message option --- jsk_tools/src/jsk_tools/diagnostics_utils.py | 31 ++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/jsk_tools/src/jsk_tools/diagnostics_utils.py b/jsk_tools/src/jsk_tools/diagnostics_utils.py index c561a76ea..42b8142aa 100644 --- a/jsk_tools/src/jsk_tools/diagnostics_utils.py +++ b/jsk_tools/src/jsk_tools/diagnostics_utils.py @@ -1,3 +1,4 @@ +import itertools import re @@ -34,28 +35,40 @@ def _is_leaf(cached, path): return result -def filter_diagnostics_status_list(status_list, blacklist): +def filter_diagnostics_status_list(status_list, blacklist, + blacklist_messages=None): """Filter list of DiagnosticStatus. Parameters ---------- - status_list : diagnostic_msgs.msg._DiagnosticStatus.DiagnosticStatus + status_list : List[DiagnosticStatus] List of DiagnosticStatus. - black_list : List[str] + blacklist : List[str] or List[re.Pattern] List of blacklist. The path of the name contained in this list is ignored. + blacklist_messages : List[str] or List[re.Pattern] + List of blacklist for message. + This has a one-to-one correspondence with blacklist. Returns ------- - ret.values() : Dict[str, DiagnosticStatus] - Key is namespace and value is status. + filtered_status : List[DiagnosticStatus] + List of filtered diagnostics status. """ - ret = {} + blacklist_messages = blacklist_messages or [] + filtered_status = [] for s in status_list: ns = s.name if is_leaf(ns) is False: continue - if any(filter(lambda n: re.match(n, ns), blacklist)): + matched = False + for bn, message in itertools.zip_longest( + blacklist, blacklist_messages): + if re.match(bn, ns): + if message is None or re.match(message, s.message): + matched = True + break + if matched is True: continue - ret[ns] = s - return ret.values() + filtered_status.append(s) + return filtered_status From 3f9d7abb6f654445d80f40f298c65d9c03058a57 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 17:13:56 +0900 Subject: [PATCH 19/54] [jsk_tools/audible_warning] Add blacklist messsage option --- jsk_tools/src/audible_warning.py | 54 ++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 6c8dc5817..f0b49171f 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -29,7 +29,7 @@ def eval_fn(topic, m, t): class SpeakThread(Thread): - def __init__(self, rate=1.0, wait=True, blacklist=None, + def __init__(self, rate=1.0, wait=True, language='', volume=1.0, speak_interval=0, @@ -46,7 +46,6 @@ def __init__(self, rate=1.0, wait=True, blacklist=None, tm = rospy.Time.now().to_sec() \ - speak_interval self.previous_spoken_time = defaultdict(lambda tm=tm: tm) - self.blacklist = blacklist self.language = language self.talk = actionlib.SimpleActionClient( @@ -58,8 +57,6 @@ def stop(self): def add(self, status_list): with self.lock: - status_list = filter_diagnostics_status_list( - status_list, self.blacklist) for status in status_list: heapq.heappush( self.status_list, @@ -138,8 +135,20 @@ def __init__(self): self.diagnostics_list.append(DiagnosticStatus.STALE) blacklist = rospy.get_param("~blacklist", []) - blacklist = list(map(re.compile, blacklist)) - self.speak_thread = SpeakThread(speak_rate, wait_speak, blacklist, + self.blacklist_names = [] + self.blacklist_messages = [] + for bl in blacklist: + if 'name' not in bl: + name = re.compile(r'.') + else: + name = re.compile(bl['name']) + self.blacklist_names.append(name) + if 'message' not in bl: + message = re.compile(r'.') + else: + message = re.compile(bl['message']) + self.blacklist_messages.append(message) + self.speak_thread = SpeakThread(speak_rate, wait_speak, language, volume, speak_interval, seconds_to_start_speaking) @@ -153,7 +162,19 @@ def __init__(self): '~run_stop_condition', 'm.data == True') run_stop_blacklist = rospy.get_param( '~run_stop_blacklist', []) - self.run_stop_blacklist = list(map(re.compile, run_stop_blacklist)) + self.run_stop_blacklist_names = [] + self.run_stop_blacklist_messages = [] + for bl in run_stop_blacklist: + if 'name' not in bl: + name = re.compile(r'.') + else: + name = re.compile(bl['name']) + self.run_stop_blacklist_names.append(name) + if 'message' not in bl: + message = re.compile(r'.') + else: + message = re.compile(bl['message']) + self.run_stop_blacklist_messages.append(message) self.run_stop_condition = expr_eval(run_stop_condition) self.run_stop_sub = rospy.Subscriber( self.run_stop_topic, @@ -186,20 +207,21 @@ def on_shutdown(self): self.speak_thread.join() def diag_cb(self, msg): + target_status_list = filter( + lambda n: n.level in self.diagnostics_list, + msg.status) if self.run_stop: if self.speak_when_runstopped is False: rospy.logdebug('RUN STOP is pressed. Do not speak warning.') return - filtered_status = [] - for status in msg.status: - for bn in self.run_stop_blacklist: - if re.match(status.name, bn): - filtered_status.append(status) - break - msg.status = filtered_status - target_status_list = filter(lambda n: n.level in self.diagnostics_list, - msg.status) + target_status_list = filter_diagnostics_status_list( + target_status_list, + self.run_stop_blacklist_names, + self.run_stop_blacklist_messages) + + target_status_list = filter_diagnostics_status_list( + target_status_list, self.blacklist_names, self.blacklist_messages) self.speak_thread.add(target_status_list) From 1f1e299ee627ac3672c6a3b675dd8c6030c9fa28 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 18:19:39 +0900 Subject: [PATCH 20/54] [jsk_tools/audible_warning] Add sample --- doc/jsk_tools/scripts/audible_warning.md | 136 ++++++++++++++++++ .../scripts/images/audible_warning.jpg | Bin 0 -> 92036 bytes jsk_tools/sample/pseudo_diagnostics.py | 56 ++++++++ .../sample/sample_audible_warning.launch | 39 +++++ 4 files changed, 231 insertions(+) create mode 100644 doc/jsk_tools/scripts/audible_warning.md create mode 100644 doc/jsk_tools/scripts/images/audible_warning.jpg create mode 100755 jsk_tools/sample/pseudo_diagnostics.py create mode 100644 jsk_tools/sample/sample_audible_warning.launch diff --git a/doc/jsk_tools/scripts/audible_warning.md b/doc/jsk_tools/scripts/audible_warning.md new file mode 100644 index 000000000..61aadee5c --- /dev/null +++ b/doc/jsk_tools/scripts/audible_warning.md @@ -0,0 +1,136 @@ +audible\_warning.py +======================= + +## What is this + +This is a general node that subscribes to `/diagnostics_agg` to speak the error content. +Robots using diagnostics can use this node. + +## Target Action + +* `/robotsound` (`sound_play/SoundRequestAction`) + + Target action name. + + If it is a different server name, please remap it like ``. + +## Subscribing Topics + +* `/diagnostics_agg` (`diagnostic_msgs/DiagnosticArray`) + + Aggregated diagnostics. + + +## Parameter + +* `~speak_rate` (`Float`, default: `1.0`) + + Rate of speak loop. + +* `~speak_interval` (`Float`, default: `120.0`) + + The same error will not be spoken until this number of seconds has passed. + +* `~volume` (`Float`, default: `1.0`) + + Volume of speaking. + +* `~language` (`String`, default: `""`) + + Language parameters for `arg2` in `sound_play/SoundRequestAction`. + +* `~wait_speak` (`Bool`, default: `True`) + + If `True`, wait until finish saying one error. + +* `~seconds_to_start_speaking` (`Float`, default: `0.0`) + + It is the time to wait for the node to speak after it has started. + + This is useful for ignoring errors that occur when the robot starts. + +* `~speak_warn` (`Bool`, default: `True`) + + If `True`, speak warning level diagnostics. + +* `~speak_error` (`Bool`, default: `True`) + + If `True`, speak error level diagnostics. + +* `~speak_stale` (`Bool`, default: `True`) + + If `True`, speak stale level diagnostics. + +* `~speak_when_runstopped` (`Bool`, default: `True`) + + If `True`, speak an error even if runstop is `True`. + +* `~run_stop_topic` (`String`, default: `None`) + + Subscribe this topic if this value is specified. + +* `~run_stop_condition` (`String`, default: `m.data == True`) + + Returning bool value condition using the given Python expression. + The Python expression can access any of the Python builtins plus: + ``topic`` (the topic of the message), ``m`` (the message) and ``t`` (time of message). + + For example, ``~run_stop_topic`` is ``robot_state (fetch_driver_msgs/RobotState)`` and if you want to check whether a runstop is a pressed, you can do the following. + + ```bash + run_stop_condition: "m.runstopped is True" + ``` + +- `~blacklist` (`Yaml`, required) + + User must always specify `name`. You can specify `message` as an option. + + These values are matched using python regular expressions. + + It is something like below: + + ``` + + + + + run_stop_topic: robot_state + run_stop_condition: "m.runstopped is True" + seconds_to_start_speaking: 30 + blacklist: + - "/CPU/CPU Usage/my_machine CPU Usage" + - "/SoundPlay/sound_play: Node State" + - "/Other/jsk_joy_node: Joystick Driver Status" + - "/Other/Combined Gyro" + - "/Other/l515_head l515_head_realsense2_camera_color: Frequency Status" + - "/Other/l515_head l515_head_realsense2_camera_confidence: Frequency Status" + - "/Other/l515_head l515_head_realsense2_camera_depth: Frequency Status" + - "/Other/l515_head l515_head_realsense2_camera_infra: Frequency Status" + - "/Other/ukf_se: Filter diagnostic updater" + - "/Peripherals/PS3 Controller" + - "/Sensors/IMU/IMU 1 Gyro" + - "/Sensors/IMU/IMU 2 Gyro" + run_stop_blacklist: + - "\\w*_(mcb|breaker)" + - "/Motor Control Boards/.*" + - "/Breakers/.*" + + + ``` + +- `~run_stop_blacklist` (`Yaml`, optional) + + This is valid when run_stop is `True`. Blacklist at run_stop. + + +## Usage + +Listen warnings from diagnostics. + +```bash +roslaunch jsk_tools sample_audible_warning.launch +``` + +![audible_warning](./images/audible_warning.jpg) diff --git a/doc/jsk_tools/scripts/images/audible_warning.jpg b/doc/jsk_tools/scripts/images/audible_warning.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5d3ed1f7d0c479ff73ed7870be7e7c047f25c957 GIT binary patch literal 92036 zcmeFZ2T+?yvnYz!>vdjR#$+&Su)$=@k_ZC9Z<`#KAVkKNu!IBx10tv0wT+45$CxD2 z5@ke?2?B!&Ya_sb0E3VKkqj7tz$BCNW2?@&_3G9+uimYD-n(_{{wHN71 z7tWtMfBwRyE0-=@{Q2Vf^Opgaf4*|{D&XpcOV{|XUFAQyzWO(jQ-9Yy{o}clidQe5 zzj%WEAB5w-_yFflKRTUt`V@fg2f(S*fK$hGK8ce~p8orN(${~3Ge4dE@rTprPMtp~ zcDl-U>ZIR4ow>pH4{VUhzJ7U?AHa}@e^gbko)vgy{Vad#w(8ozF^BKcN!^pG06rbQ9j~aL9H%@9 zyfe&bLg`c-oN{8!<>nD8%2_;2bE0nHDTqOHR|wTLRDSl-W)Qc(0P$wrfX9}q2` zv*-KE*D10LM`uUdyQ2l|IP6gBZ5bQ-e^dMa{Nfa*=}9fHby)fe_x1#PSW&&$IK3RM zsq5wNf*erTa;gFW1aucnNnv+u{v6zsudN&D9Ksha)E6o%5RGJ4)d%6dr3P>I_tpIy?(kiqbx9e z*R*3#)nmT*MZN5R>>NO6pf^zh1|1J-XRGYY2L>6A~;C9L;fTbWHc zLvUaFYtYgrmmZTF1>&1S$q(iS5^T)QHNY*;vy*Y0k!>VqmbZ4ycbNUp=0fZYkd4&# z4v_6?#6G`Uo+&o5w0f^I8s{O0Px&XSO#zo}CMzUQ6Q1>jNPLd{YNjKKHGh?RPinBJ zfFIX3En6Wg+*i25r0|$o3rN4~b&3o^?|L28oRS3Q<8!>%CtrEwgVg(|;r((v=>2WY zh16`?)z*9&igZ>*$`VdWA#?p;Hf5k`(Sxl@Z29_PQ^6>f6PR5voBREF4)&{zw_D-s zc*emMd$%XP^)*bWA^8u46xO!Z&S^^_2}owM@s{dGR za#3v;G!?sVc}YNRGy&Eif@Ze0?-qK9&bjM3pR3SwK3~Xco$mvbi2A569syGE;qFr# z<)vOpA^~WZvt`|;k&)f|F9?tn)eF1sx8J;=Bz@v4zwomRAw+UfQ=0|RuR@U_8$RT2q%}Z(dek77VqQLxh|3I|0XbNq~ zFnduww+^5a!IV(7@778gOy~5c?}LC_?P~*p@bme}t%9QQ@aUy<+^eeCSE1!jHa>lh z_a0jC7luG2q=)t$VjJKQZc6CcIsL;esjVMqI$%PmQ5}d>dj|I!aVE zv_5Kif7!ynZcI8S$Kflwxy-2O26tg&5tmPx-`U82l3@3h*;H@?9qc!mik4j-^>(SO z*5z5iHm3c+v788iCu(If=v!YL{6q(S=zDo@RCu~kHpZuk$R7FjP9GZVwW(k!rv5x* z;ZqH;*8m3p)WV4?FfjkUbOY+oYI(0Zn^?y@Fw-eyefL;F;Q%dt8@Xx13o}?=NmAdFZ zv6qT{z9(p{y^co+%c`fXrA5#_1zvL)g^=XwA8beO+%DS0$w$c0cs(ScUbYPsl%Mu-BXZ zrY~w1f20PK+QogQ9NuX;1+I7wxA`0!)^UD1LaEiy2O5&vh6eS3fEi0b*ip}9@brz- zyBZy4>PpU{dNe6?VHWfd0lJNL-7xw22j+QEu9=4xyFmu-vjC19~}H7JhW*!$y=K-JM_FO zmS6s#9>~9yX}{|dr?>5z{PhelW7=HbhHdIgET}WGd;QdO+gdvzc77-UmHdE$-D!Xl z!Z2Z0XhM+l`EBn$!9>^K-k&P94ERf6SYy*L*<8u5dx6sKm5#i7z2X%`RYu7vc2bCG z2-#T82)6ks$<6qA;3iu)8Ijj?ezj)f=As3vFjsBpm~R~hY}OWjY*s&aX684dNZr?Rwj5rykbZr5nIu;NSRH_3J`IMVcd*53 z8rE?;CS@e>hQJP$7A|4tI%1x!v&;4XtSw5SN53&Y>_f-&z{h+ZT>N21+1wN{mp8t4 z{al=udlTW9?|`&(CuW*39Z?fWircR`Nj>i3?cl}7E3@h=7uWn@g0_7h&2~Z*8HwO|XLPvefNH?7l`R%guJgeVvVmJX z>6TYc_N^U}LZ2};*(D7XZuq+_zW^>T9v?r`u&gZ;qzyXeD>~-8va~|nneFwPq%Ie# zTU*44rD;_3UajKT_GwG!2RO86-WYr9vxf=OvS=N&i-06gMF!+1TnYh__sV=EY3rEG z{j%*2PmH8ECJe&yR%2&h840!=B4>2hYR+X+TZ>1GxerBpv?9A>o;CH3bs)baWFpzK zE;8B*(X68u$#x%-fAh+O=Cn2@xqP>8>EP4%HD1o}Xqs?etNIo^C{R}C%GZd#)UdI` zU2gBeM6M*aReDOA&?|obgST?&TL@d8K_P#rrFiNNqYG__vzHRk1^tslY=c5o%Tlut4G)tbI-p=RAsmdG~?e0jXqKmZp6hsDg`|y0*$YgqLxBlaa66M!M z&M!c=$(J1F{Hs#>qL>OZMsnb?$l)d^V!t8s{j}BzqxT{i80$zi+l`iWTa8NotuUB2 zdl{;g+dG{Z90G2N5Ud61gcRLJKD&ZRJNMq!X!M&+a9Jn*`;@S}y(!Dh3i8VKTY2uD zk<=p3873jOj+Of|hdAc*jGo$SFP}bYc5*;giX0A4$W9RtN`t5h$9zxA)^2>`=?*b- zJjD9i1JaNAGHb1B1XAA{oC}@ZdX^-TSZ=QZcaFVX+xFu>A4W@0kIl{F6vJ@tUbWBr z3-oida+=Et>fToezmP?JaNjNZ)cd@0BmA0w9bnehQeX1ck}`X^N)o$1TJ;?fEmH|^ zr$5v|<1yKdf_}z_0^sz_>vk$XHzM>b#Ffj!?i#aUOGs(|8jZ&s@`-e+I=d zneny+@KRzz0Z1ou54ebPbZjM1OEl8iG{4ojA;5Nod{ZxvG(>7#3lG6}Gs48JdF-5?7c^+7}I_dOuQ(K+CeSd-X# z2Y);BxzWBGIS&3JB(&Mf`I#GDtZ@~M7LrH3z-Us8aMJLYpuoKf$A zP6n=AU6TZLvJd9Aj@*KSHu^mA-SL>ItWfy_&E(vd-*qtE+FlU{>zD{{i^TQE>&JW- zvO3z;y>EYisi253oL_Ce`Iq4P7hQiq)xkD4v`=4?+y-X=gvthj26&#wEHXJT;e{a_ z%@5_eAv>mR1;>1v3aiI_`Kx;iLld-3B!(@8T1)%%SGe9WpF!5(>)j7AWNLv!gZrx9 z$hvi5oTS*vqv3kUnh=U#59A7F-`ZSq*l`u|Xh+I55*V1`(3@mON4mOU;dIz1`X>GR zXqa~F$FxgDbYYZ~a(d{`4tNb%H@t#cH?~k%=3jSh?OKr*9ArAH_rhhCHISg^tY@R^ zdlOym@6nD2s(#0RwJ>Bb%<>~iQnF%mKu-)nei2f4zNA}t&v>_TXoSZv*{$8D_7S!F z&z;SkhMYJB%#_RlOb~5AA}CjDCt$@iJ>?X@rR>}_`NRVPmBIgU-}@xX_^qgy8*gVQo)b za!CpcWNM|iRc2Rz>)4s|EFzbhOPf4rO}AkBN(2`Zih=yA6A=ffFfAo83of$zfvY(2 zN2juP8G?)~wo=XLo4$75HVJ2Sg|`+xI6%^V>Ao9-HO~&pks6bIFAD78_0{_190oTp z55wuZxfup?5@DS0d*3k;+12r3+FD)yqt~6+EiQb`ahnOWMoi#rWQ7;B*NePz4vKHI1?v3396&|^MTyC6e$IibK{ z>tLOWS(?-sO{>rr?yLGx*^HWYs{dk z29*M)Sw;@!T~I#?H^cud=}1m-G!5Vq?fd%X#>p?fZ_LFGeO@XB*dN&!?j8Q~Lsj?K z>?bvgtJC-D`n75qFJXiKoJtA7?Be5;6frY*slNEle?bmq` zqr6B=okeKbT*Wvrk?1?Pa|Ygn>)82zsGFqP+@H1`vL}+uf6Go@+Aif4L_C&c8Oh{5 z2-7d$&{BzEF|!;re00Fzjm7e5apja5Nai$ZD#lY>a;+H8HMg0pzNn}y7?(^1qQ1n0N-xZ5Kd0`IT8=+F+9VP`vqV`~Ffh%QMIdx6&B zCb$TnJ{^Z5u&63z1L>tC`~D-!TPARDt@#P@mOWdw z=MZA!F=U=obpvySJK72?*l*b89x+BG_VGF@l`XE_L>*)Q2J`rteY^PZ-D5t6PUg;n zmW3ds#ULRCaaDEJx{Fegz`^Zw_%s0z&g_Itj;=-!pEatbd>#6<4HVwjKJ1ldtjqX^ zFCX*mp1ZgC%YU-?c3Rymm6gX|Uq?0u$1e>Tc&u%(4{n<{T1bAh@hTi}%JbW(gCk7w zpD85+F=ctq^t+E2JeqXXt$04~IHM(Wo3T40dX};u^a2`zJs|r$&L`5XfY-V%FPv@8 zec{7qL>$NlbusLJU#b5c{gK5Q{-eXc@YSN3t0Jh?%*-zqo!`$i=hjQuUzqoW2nY~V z^nVli?LZdO#gP5|oBi+THprm*AG`jguUKZT*Y7^GHuH=Xq_D03F{j~daBm)`0RVuh z=D&gN@`XDmZ`NKM|LF#4HZ*m}JNC{99By%~Q}H5y#5G_!ZwT%cbzoG~1Gh0xUbEnz z(otQ*n`z51?YJ4eAoUD}X;>ud(Cmx$O|8JM%fz}fhJ!U$3f&-pct5~CW+QqL#)ax~ z#bn&MDbFB^&|c&B5PwpEnS>~%TVOzz0%0<^`BJv-x`rYB-k^ADbJ(dCyCr-~IqgcI@F*}c$yEFn-o0VyoyieKy;1?`lQY77qY|j;j`M&1%}M@YF@d@tkt9X)1C_^gBV8)Ib`mRC92Jh% zXcYy4PpX8%EM(Hw4RZ1GOJ{p*|TQ*+@p8pX1o6S z_b&X$->1}~d{Xw$oAQ(d43j=Jxc#BOsK2=f*M4%$XJcdCy{7Zil$JxKxLzPJSSjYol#c6qAX>7#EVDo!xBYQS7XZ zg-mkWREyMs=7WRnOjn6wl;qUaJ)-7G16m$&1KCPd`^eu^AKX~l6o2+#*Z<E(vOW%CSnf1|GHOqgy5*mMR)R>EmWlT^F;Ia@G5j~~KVLulH@ zHT%4&8HS%F%25H}dXex8S)KXh9*%f{qg!ujMJr4Ag#v^b=jmgkccG(ntu*C{9&^q8 zA<5MQS}D1@t9=v^7FI9IU8eRUjG6oE?)w_Z>Lac0Xn_Mu;#=;OcaImHw~@KrTN)hB|Ipe>s3PTawy3rbd845}&o#uuBb*iA8HqCXFJGp!wf9YJb=sZN z$^64_ZABYqE?Ky|wvAH~XKk_+#3tz<_ytxXN&!wwm-U=4>%n)EY^VqalEB7S#3Bd0Rx3qp}2}wcd zC&12Rxh{Yp)2iv|iOyUf;dZ~Qb|B03;@+}W!g}sY^ZH(3yt!XI(l`6^1uhZj9n zzJ^jejzyLiWD9c(-x||HCG$QJ&6IqVU+CH}2VRgkYEzp^MJ2*CG%{kC1LxtGuI3Q% z91xfuC{XrbTG9BM4jf(NCEnz$f&9WblR9qts_KP6VxZ)&l&{lMW@d0-tTP4zCd7t@ zB$1gwEMtQMir?!q-s|+nn0pWL1Bm;*syhbu8wnFxS5DZCw9AfOEw@Rze!0A?uI!cW z{1CC|O&GsSX_C+bBmRD&Io*Nm3kpkErY+Q^(tTe^Bym;g8*ah9ugPdOw-&fl_8%{b z{wJ_!0tLnw-pCXe{XrY_3=ao{JAaB)95X zk*rm}@n)(*RFLfxRi)C}0f*{K_uEA8!P2uT_0lXX4 z^Bv_`H$f3g-X-)6_@Hm?D>WOl2P1D-@i7(p^^0aDHMo3AY$cz zalt`JQ}=r>Xh?*2%$MHldY-)N(Y`R?&m-HHc>{YyI!tG%M;PJo^U1pf2Bde0)C|Md z{u@=_SQ~Ga+=$NSzS>fIJ0O;p2=a^`PfuH@EldsZF(~sXr>ViBETnnmW0D4mI(g)&gJ>bG zf$WHgHF+ser9f-``FYyR{7$F4?i#-xbI~iizNI<~1FOv1g0vvG>pFqd2-hW4vF4oc z@=%2KYjyv~n`J@GqzQx^K6;w|pB16()URxs~aCrlYLwc?IMG z_SCxtcQ$y*@M}(`$W}2F1A@si0jxCZ2SYjTHFI?zBpm%(w@u#9{@E1=xt=nRZy;~x zQq=*@7G;>_Ni!=N9LCLDW56nXwk`a5B|VV-GR%zkG2co2YS@^Br~n0@_-k5qruWpY zSd~Wg>zR&DEER`Ku$VVq;E}&bQywqg{J!eow-w8b>LOEH8-dUp3ho z^%0~BxyKMy{Z*I~bau0=?Dus)%#A+P;M!KTGoot1DL&* z<^4LmSDf!%t1K=Er#d{YVWH?xq*vH@EN8;+6-cf_3#{$oP0DNod70PXHJ!|(@OG5= zScCfP=AKK0X+Rudc0vjzVZ#fq{_u3H{_8rV;n2xCIa8-Qey+uNGPT{h<5^Wk)-R6! zXcMIleg(0JnStxi5|8=582JtDINA2J`-BC4V74t1=H;nN*kitTGrivxhJ@FH3v#Ep z!9hE7xni?VntEIrk(jxFlReRQ3-f~EcW)%C$^_&8 zJ;!MN&(@VXGqGE~rm-akwJ@Go?y4oF=VIxf+{9|?N5{nNISUt%*Ly~9yrWJFIJ!NM zmp#tfDs4mjzU`N%-DgT?_RMxHuQaRnscJU=HYMQthy7m#f-Q=>kaQRFw2;i`N_b zcIH-oU96H8?+GpavZ*Z@xi6IWCRfy&8#`43?rw~5T>wK zaHre37pqziQ?1%O9Z8*?3Xus6x5fzSUo$&Vb+IP_UM?<7nl(cWy z^~xhGxLgt@jnkp5+O2lQ%uHLz!op&_qL_A~4NCdo-Ig>!#Cr_YEmi!Z&&;UpKOTMS ztzx-6rF95SSD4gI3u+t5D^@b$+M_l1T%RuiZW)h9zM#Z22dv3tyji$P?+moR)pW9W zXs$ngX@9JGDH1%|5K>o>6V(4xdtO4wQ}l<74{dq#dY`k;YH3<=I0rJth1>}XA$|}$7++${ z4Q1F?itK2f*kb)GmuzW3j;IeuhM5OSG-WVm*iQh@OQi*iZ;BpMo2Cip=rWzAa%&*6MoE?WDZ5zY`2(l@^+F?O2UyWg) z8ah?An=3f2R6;!Q4HWeZpb6_n@mt*ehFPVTZe4~x1y8=@DP%rEpK8$*Z}^@0ttaCb z^hcWU(R1pI`Vus z^^`F~rrr%%0FT_!YlHI7F;BguAM*{;2O1PT3Z3W+^>f&XChjsKGjV-mfS|If5pRJiBdhMqXOxt> z0uIawjYywmmx@Ni*cLUhB7#ivqR#CDlZR1jcoS4=9Ro4djMR42mi2GY zst=c?-cP5zEhG)p@o)`roJwCC&phGm>}1M8xcY?7{Pt{o_zGJ`>fL4Yv#nfzY4P>+ z#FrLpUUOZ|O|O~zl*m!dE$Wu_QRllMoKIL$ud+(?4)o-B1B>=5TBT^!-sS9V?QSZBEe~fM0AcEMl3O zUSU6#EHHNc>nD_ooh)-+Qq4~ssm-seXvY(<$MssB5q+K zNQ66AV}>E$Ld(cZB3?o1Gs5%$)auC$5*r&4`t5t@`0i1iGHeADhSK0pzF!Edu69{n zcAE0kiJ{bHHdS@&8Mvy})`?h(U-BffcQ@*V$a9dOsB*|ole4o*03MrIg|^qyG2hct z&dHwP7l`gC9@k!uh8~=LwY^uOysh}QY78eoL=3lihe{xHEob|Od~D!Ez@Q+)f40Q4Cp+5uYQr-=LYusHwXR$rTVWy z&Aj8dE0>`Z^8DV>;HVMlf=~hL2lIY#agz1?uhP4js68)7b}9XI1F_R1k4#FvpiA&S zpYNs7oASg=6s*w)N5~KpF8m3%zEwq&4|x6Yt~7x+@@}`*)nI74CbGEVnfK95+xOrI zx0$6!e_KuENKjWr#3yHD>>Vk$tGb_N65g}}6LdWsvd=}wtlf-h;{H7@5&AJAKTzQgtNNvXA}iV~`#9#SPadO9IbwZPc}AiC+JF%yArd4kO@e^<++VeT98~ zmc-bfy2=7xALX~l47{;)uV?{~KXS$@HE6}M{N08Ox7tYRxuZm&e_&<@H%gK= zv0`Op^(pZ{0MMd+kLKyN3Q7KesaNi{Cj`2riHsDio5h85D^p zl(-T0Lg1FjWaElt!PKT8*GsHv+IG1=q#<;oM-GP3B+!wXE6touFrMxcqNN?hrh)f1 zCK0w!e6zF&V`~(@IHLA(^d3k-@X^TJhw|LZDC{k>oVfF@a&wgr=5udrt&icPjOB|p z;bp#UC(#^$0~Lo=!@8Tg5d53w3rAs6N&tRvTU}PWk0u&bh!*jum3Bi(FsXxgiy-l_ z#*u^q{uolPkH{oYIeO>8T=hMb>zHxVpv&xq5E4mDsU(#{C&{52zL|xac3YLFPzFO& z%G^APtjRMV_qI*&JL6uB&4cWHTZm6~L$5}GScj22dzLw^PU@&H;Lfqb8W`<|((02~ zhc|vy;9c8W?eD~(RP}2_O4|#mHroTe`$ljxFHe$Obrtj5va;kJCgTA&6hedRgv z5lSns*Yvi1Kt6u-ik(uL$CJH&WWz9Iri5b0swl1qyb;TaJL*rgg;D2iY^?m5wPjeB zOovAO1X)e`x=XT1G@b4rXB*NHW|Go`4Yds{ukkMz<{&+cc$)Ohm>6QRgy^ywrK2{? zAlXJEI$Z^Tyhv0ojUrCz;mY%)e%FvG`4;8_trQso-z!wWY8e+ai7$pK@cuH}3?ye1)P2NMMAUc#VKAhnt+{}D{#4ZJ>BTa3dgW#7 zm4O+O;g^)2TBbcJh1Pvqy$7wqI&c1Ib8Mqu#aMIZW;q>emE4A<_r|*iGZ!;r+dOhk zqIg>7<8oecyu1>%98t7ZCp$rY&(yT1zNIK;w5fCy^OMoj5=LWZyPfvMiYsozk)q5V zXzk1Mu2{)O<~Py5W2mJuatgv5-h$LPOUM|@>pjA#Dobl%yPuVdqxFUufK7FWQYVIj z({pFjN$ro*tX*W1%8PC=IMNcBR6l@c`=~icB0r?}cK*z_&9bh8o<_UM6j-yk(v7o} zHdRV{tIAz zjF-DC(fa4uQ;Ynu3udL*jr6ws2mqLyeN?zIl5CW--PrUVDb8fXpMGL$iSOLk{^eaQ zrz^eqRdEO>wd&==*`)Fqs;$gxWQ%bV+=(Qb2`Swz^9g}!GCbUQdu`>oo?tQn6rGdg zyc;kPh?!av_vUiyIJFf`Gi*8gqI^w#bdz5l7~d`8Z?A5OY3iDc%C;ib6Yvt+Io@CtRjHDM4lzRqWSvA>=|yu>84m@uN{UUwuYGw_wdP}k zF#CB%($6@6t#sq$EZSkuw`2su(U@w%2#hb^3Gty7Zx7TdXN8CPvN1DD3s_qqv3lF! zk3-D5HokGp@0d?TO;y{)ma6JE(4^M8l8SIbM07Vo!a8gSHtA6_xZ=`!W`Fz6mp}3i zIdkppO})O-aQ_ge($BaV0=9KcsK1b_px#&3&eU=1vv^smbG}^5Ji)&h*wYxE+~l{` z4qEGCpxys#=D7ck%Bddj%xj}}H3d6h*iryIG1_Oe72(@kf7lF)P#AC+&baq%*WA_Q)9@oMh@7#G} zH*O&Kd1qX4FSf@^!%7C^==ea**nNOfV60V&bAe7M*zf3}F@P-wy^pFCf(j_yeO)A5 zgjd~1qtYUsbbEDZI{qf&tEg^9Ac0=z(6BgOGE5oqt{ZxY%efNh#9x zc0M-?$JIShxK7N*n1qqSIqtlGn=j=!cmM zLONqJ;QdV{0QXi#l2=E1^Q8ULKZqA+FT>1K+rmcd$`Uqxn$Id%Rl+N#vqd$a0_Tyr zMn+|3Dylg~+eo}!*-VQeu^N~jv+#V~jBr@OE!5R(2|+5U#&0cxnxz7>9xrx>{Z=~* z@5&?vClbY8-ccXS`$&UqtUHv59_su6yNef1V8c-o_h=3L58NNvH05VCB=@Mt();Y9 z+to%v(d`(}ONfF{<3we5My9EOQATgTQpU+xL?k{8{rFx6SEb>V*88BeG_`V5JWSI; z+jzFz#=yscq|W-Bd)l0;pRIE)L7Ny9oVattRi&e=Lpfr?t{1ra5wU0DF%dZvoHcpn zo{Umrm8|F7Oj_ODcUoT*dz)NVf{Hz=zqUO7^tMO$SDRWHgCG4(ti}5&YR=WFa6JPL zg@_Abl8L4Y=7ue|7-m#**mfLtE)_L*a`(~6Xhq|uPFL9@*o`Juf?;E-G?`oZXz%M_+B63 zlZ>V~sk<`4_h0_m-b2-&>Mjd3eo~MvWnUvzz$=2|7C*?l8wryjN&H1%0bckOeslTe z&5#iDHFrkfEX~7xgNCX_VoJ0zS0Y5iM-OJO@${{XGEVhs%^_9MI?3*$=(MRpbfq55 zIn>8oB<}o}yQG3u#-J(5-qIlfXE*{W4*Y1a;qLWZ&rHEJ<%VrV+A*KiU~_0&iEtTx ztHWiV9v-Jqf^#)L$@Y0E6upt&q5`%kLsm+<^|~+ag>?99 z7GR)#GR_k9hguVXpit;HR>>g$gXL51Qe1&S4!m zs#8?{jL-4E+#)(I z2>^&b{pWvYTmNf*s=r;9s6{;9f8y^nBq#f(Aj;mkD@Cqs8z_KyV3Y?%aDOl$*_zY3 zLs&t-X%x9FI@OgUqEG)1qMkaCFv4V1XgTm z-IQ^KJW2!fOnB*cL@>Qqb6PE9U%Dkvy9d2K0Bpg}0oJn%*vU8SKS^g()%85qG#={tWpoUdxUZb@{!4Z?^yZes?Zc1{w?*z@|m!Glr z&>^Ko_%z?YQ2*k@|GqMDDixizneKPh`tyiaoqUj_ey7uTGlmz^G+$cO3*RB-`~%p0 zT|F3>qx5CzWt!4kuBMUgx5k&z9sv%b363a)$=7Mw&*lAPxlCvI=xj+*>jHBxD6^YC zL2Pqrg9J<;rFxB;u5j?|I+fEzZ2SUmW91ueN$1De}C4ZsW7p`|!H2&Og)7|IW54esf#4V`w9u*|AU9l5vM^Qe^c>LRS#Wnb}I zHtXWAn`DFml`ixAca;93a)QJee!70z6}k_*0+~ibHdTr`1S}3|u`?zx%<~Iai;TQh z+u=+m2|ua*q;w0Z0Fcm@*B9+&tf0?z=Ug2MYb&gG0G+<(xa2P^mr6og_gf)s$^f-F zdF32u%b!#07p2FBaGyvCpGgqj{I0n9%d@3g>!nUT4M=7Yv_2l+DLgv~is|*GMz?4u z^3$;fitas?62bWmNo1fmx6ZCzN)`|kmGWWzDjKcXg1Y1#>!&@1z}6wVwykbSvxgS%MLm*Ai0g7^NbiwmuzQB2pg-}}-8N5Yb=csmkY+7e&%N|m#jWhLR zD!1m$YqRENyoTosf1UlwhC6p*Y(DMY+uM|rm`UxkOI1-JZ;tt>`iVsuuJkUJ0i9Bg z_xoo4d6BLv-WFn!po?p4DYLxS)M6}{%JwVVQgp`F3 zGHMQ{7L|mW>Y~TeNNgWX1F#w1Q@2+NBgWMzC6hml#*$E}Y0NTR#&qpqIhwWEh5-UV zH4NR}n}IJOJH}t)1x(_rWyE@#`=ZuZ0Gbe+A9alI7x@TzOb&J3xL{hDLAq*B3l?} zO*|gh$?u*KzY)I~)RP&sC^&5@8^?XuKlnIv*$>+B@)2poNZtTf^k?ad?`_i4_M$vo z2{*);IHW(XUoSeVSK4}5?jqK+O#{D6Fj@lIR}c!kaypdALz#jM-3MWwZ65R?i*aW5 zg+2NtgPftA98)s@lgWrTU%Ub5-oVW`-3*!%3T8l4hv#RTjm&;`#}lwWSoa%g0o#-w z7Wf+v%70Ast<|qHUMK*E1^d(H{#gAqqqM}~SeEbrs%0)xK#ufC)*L8fq?iO~3rct= zZ=r~=?(VbCGSy|w6^GFEfx_c2}oEa}amAxmC#PNRbDtY>CDcrc+JZe}iN8_rn*t@zP9aNNdPqIZ?glIfGV zQ5XM>8Ap9JZigaamOg9>w!_i3&ph{=--=>U3vxOO$tD}Pu9Efp~BnJ z5!sjFyA9fR?`8e7dNsXOEv_1r$vVD`iONnVrQ9oA>6KzZxx7BAdcc)zi0?m!AL zzi4sU**R#@8D_tW>d$j4 z87eN09H<;TJQH!7>V0*}U+bZDMT!#Ma0$v{SZR0nF^~Ds8qTXrcfP16XbWaT@P^DC z>_o(?h;j8eto}IqRcaz^J@aa{%DqX&ytbPDYK3n*BntJ?Xc;Rdg?~^j770_X?ht!~ zqc@B>Xe=Z0JwOlS_nKv)4HGvz7MR-*pj~bNy5EC4*+fhN2=lNRyen$0(uAq9_pMLRnZ+_ zG>!CuuuQ~Lz|2m~EVgobfsHM$9&R&t(L}$zMm>7Z#e;4e5U*>YqS)c6tIJ{4aM3bS zGJv5b%mdOMDEm%18ZY6O&FzON03p%o3Dp_tDb8b3J{uWrFTB-tuA&^-uG3*yWx=$R zGFwWh@^ho%_t)1-J;rusf47uSa2D&Q@^)&5rY6mB2*mgxvuP1Ts}#sKX6WK$qT7{X z==4qdjza-|Sql#=C#6PzErW)2&sxd#&?uW$sx!DA+?f-|kbUXL%>|Ep)lB*cCSLA% z#{0HmUKGXIZ~w^c(Mp0A;RF)=#<-u_3$pXX@_t?gF0J~rwi@@=E!>HCu?0Fp5&_`P z;`;hb^r){Wj2uh?CAjF?lY!+rG3W-K-YRBH*gMM^MehVJ5qMN3+*#&I+rZ;J1=#K8 z_S%q7*j!PI+CiJas`^)-hG0u}4s4pBb_~>J?q&FGFl6&^+{8jPAeNbf{Wb&Yv5W6h z_46D`mAZOWRGpdU3;5&;K zkqr&;nrB7ulv?eT`?s#V60Fj58#GUFBI4YSX7w!MW^Q%@O1K?Prr}H=ND@jv?DS*s zdS7oaEG7X!{_h#PP3%hrU1MsW7Vke_D>wNjWUpd7;^i;Y9shbWk?gOTlUNc)v@Nsn z^A~_eS0JSQ8%*-o9ouHgY*{BMiA;X?9WGKWdq(WBbBR#dz_^SDXlnrNp+jJ7EKVvw z8kjHe>J{rmr5&q7ft9QGP0V~LUmLc4)|k=*f(r`p1eTHHz@VNp3`)3pA@q7SbR*Nx zVif2wH)|J97#)&xiuIS$PfG zL=weW%Wc*2y_wd%_x2n&qH4W=8o5aS%ziAA8D0 z`O5MJYxpfVxO_k3~Z!zh28&+cdu(EWfL2v)XhJS->8-Evh8n%40QfN zQ1qAzg~Khs`8f|X!Q?F*Fs@b*Z%~hfta|#Wm?_M|vYQB_vr2a|h)xf2a z&TCICLrX8O^ss>%y}Jv`FUV1Y>x4P}^(A6*EwC1<#jDy^y7Mha=d#r!u|ZYF8OzHq zH^{_MADpGxb(JVJtUfQv^3|ufc7>YK6om0cnxn(^v<@_5YYjIy(5MQa zgp07OtZ$++@N}ijxxd~OGc6CDjolrC9nJ-@29D%odL7(rDAX;O5AjG3|(n1hPC^taHh+0xZssNWyj9W7oCEtk&zJMORl z6Hw*I_cw$)wj7Iv+S82>UEQgr#U%vs3|GO1k8e9g7xmfoV3TXg2{34bkg@U$#K>TA zP3T~aL(6$$^4k8E>`7|hKtCim-a$r2dncEsWLqmb#5S{jzFa|SPD7StC_>Cq?X-n9 z#x@ci;gIY+@Pfnw&GXw5!KgDrTYkSOjLzz}Albx!$HBoxC$37A|x9L(o7~ z>Zo@tOG3(K5=ADxfq}owH+ak=E zS4YQhPe#1ooLmYm{qAwcE4?9CbnLCNJmsC0%?HOHuYK<>U$gl~g5eYVh4)Vksx2ER zNmxBy8y-)S9Ex@ESI{2pA%hyZBi!_p^vew1LITf`w+p(_D+LtOmli%*qoCY5PK4mI zuLQr@{{+tKZnU&`TGRmD{_!N0v*zkhy;H@TCl@rIc*asgN?)lYbhT*QkleH@!{WR7 z?N(wLrl$IS(R2n9YM@Iy=CjYpHrp+h2}L83NQ?vq$ba%jD4oV%L}Cn%IOJ3YBPw{*IDm@fNCd&CfI}QA5up+h6%->*5hKnq zj!|P$m;1imz207{Z?Ep#ANtGvvRUi-z-?B62OnKt5y)VT&EUSt?a zUgvK0_`!CvHkV_#4%XYknA{#DQS{9x-Oue&hnKY9<~FTQ6gBq#<$qoJUo-Ll*E<0^ zG1%ZX@!5`9$9UkRrN!`bKlfdD#+N72L?{2Hz3s@oFaNSi)!B3w>|w2zEROx_NUF|D z+p{Py>|Cir=Y@63Y3WT5n{d-gID2Ts{RHRdMb|)-m2ws!%0m@ty(-~W-35_be zWhf=quE|n6QjoDpKrB~bw0;Y~LD8u5KJBBCpSQ?0N?XhqaS(XX0oiSV#I!CY_cRz@s6cdan_XLnS+}csK6%d+iIxNR!rMu zCQ~BG;$!O-(^fp^qB1FY9c0-Gu8v87<5Z4q0K<;okd_5`S*%3IlzWEC09<`GMUjSt zHZN$G82%lkhFZ&i{MSIwJMRwg-)iL)w+3E+C(-oVxMYc@uALgzx;Zw~7p4huU50tJ zxJi-N$nvJ{@j;I@6%F!|#>_Xqt*!P<6*ZQX=uH`ZPD=!w#@h#I8#FtY{^cif3_m6< zt7_?woH$GRc+|d&TiqYamRC8ZrEeRzr<9Fw2;}GR#RV_J^dvINO6cezt=29Mw4_Kfj{yOOt#OS30a1n=QcwYS7sRXZfrAx zQQHCr4AWC!@fX9AF~|<=f-b@hjF|?*>kKR%N8@%#OBpIFCtuu4S0zk2-lQ>8M83|N5l=JV0#~ z=Of`-gasTN6BpDo-i;;irttN#8V#OZZ^*sjj#jTP^#9D#pMeLYD?-?eWIa zv)abwH)sC}HH))2&4~?dGBaY*ql_;!SX{d2^P1iP7^4Pj=7tlr42OJZjO(ML+=e-C zcQ0(r8Z?$A6sp6ZbZOSAPN0^C6L^>}g9Zc0!^hraRd^bc(@4;_5qW#-9z>8g8x z`!CP@nwvZ35?aP!7bpIf7tqWXg;e}G2b3@)_VO+N5B8GpAE{vp<%at>^~f; zeW~_=(W<=)uXXfXtHokIaY_g~1Zo~oYLMa#vTs&YNTPz=N(ZWeOF(rYZ@UZ$>K>mO zy)}FCN#dklvfzWyw8wa17KF>O6%9xG&u>JTnsww=5}Jr-tq$@jQDclW%~s#j4m)OJ;@xd+m(w zkiL4+c6;jO*QedDk?5-yZ|;Be{OK`f`5)X~-^Ms4m!dM?TAk+tg1T6_@#g(dDoSIs zln>k7)b=c>gpKP%*oub9N(jED*X+?+BkhL1aps7E18CU9d7BM)3Y|f%0b8mcvR_S} z4?J%s?jehPr37EcLk_g zUS)vGB$rpAR5Qt5rXj<`qzWW1mjoC;nvufeG+M zpa~^AC8U=AF_o2^`0c_+?d88#r1sowTOUa4Jt13a47y(WcUJUizZ%Agr`xw{GT+gP zE3+018r=+@NOZ#779*E2%h)g*%ET-%n9YZ*g%D&8Ithth1i!PlqZVgW zwf%!!f6{?PJB4k$J|l+kka%aLp+@A!vbT+R)S5{N;@!5yl#GK+%IOz|dr zZGFPsYfC2pp@r(8dduQ z#H%ZBHtzI(;R4Zox`ZqUt8O^d2WgNelEO|?T}M8@;eWQG=IstfuGlu&wa$W4_)_$S zwtCSEwT@P++85lh$u)3OcgpSRKx+WWvU-jlAgSD{4yI`54i7z=v^S0V&{RL>+aWEB zr@BXN3ca6aGWAhXP;S)CK#ts$w;3ndtOChmH0{Po-8%|^jse0!=)rlq^~tEBBz8V@ zYwNkT{1S!H@j|;;&tU1#qyE=oNCCrID+U=>PS?j$Mdd;#uZrv%MPc=g!za|k?z{ne zWAcP8e_XWD3@zlA!*@wdpcQLUOXiG9_a0xexZ1U#2zUeJScZ+tzwIdf?2fKzfkp4M ze&xrljQi&@p}sQJU)irYG&U~DD!+2p)znDDt!1xhNn>qf`mtrmSPdR;&SqL|dQ>7N zqAQRH3a}fqqM|j2o>3pa-fMT!)Vk=;BV=_0OhiQ8{sXgC_jn!0BKPuu=IiwrVVAvq zLQZQtVrs+cP8a74jW|~^N@=D$b*Yd^tbsm8v%pRXi$_5#u+`%PYN%*Sre8_QvR3w;i5mla67>4YMD{V>2#Ue$%Bm1FWX3XRoq9x+UHDOq~nnv(N9l{myYUrM5g^t1)0~!f z)dVs2%lG}Ada&H4N6V1oM>`BWeV}>WNKP&eI_k_DvpEP=Q~`Vf;wlH+lLNEDZOBRDLPjl|Hf#{MZWnkju^CKS7glL~W;wo#XjtnWU(sc`uOAC7dj^~=P+q$PnR2t4W>Z$0PBggx%AAw z6$r3VbyXC*V%NPpl^{f6Y3QjbzY2|T?~*I4?X9nu8=PO=yq|Pol{5OYuRF+RL*V0g z+U$P0D5^TI`H#_|r#(MZp45{Xq;_qT3|=2VSZ0!CNL!wVC}H`14ZjeB`LgXOU>U@! z0;T$>PM#fLaFw2ZQ&5NtXFT0d!pCC2Yp@7bEsajKWXzma^L;5xo+e_718ubFnH>&4 zRvw6y->hoAWKG}+9oLmTH-OH8EmPT7^{bJCq@de*AT7tiNb09}r=SnCKe7`pE425f zUClZfYJ_zfJILS1B{px7@9V&|4)b_b_#P9lqpt+2jr2)+I}OROEtF16CYc&KSE`~a zu|GHV8Or6dCYw~16;+G`)Zui}!b%=LEONW)0bl((U7#WftD@h}s1D4ib8~YeZ^vF5 z31|k>Xn6+%qqmyD{S8yc#19^SjvDqfRVcleKsxXiTPQ*dvYF6O3g8V{PYu6+9$ zO={B9;3bOAO#YO6sqReA%}H>2$7->5NPy`{>iPRYmUT(9se`#;%s^mL+w{A!z?CSv zLE+-*15ase@C_cp-^3FT=xEQpjF6I=8}Y2speC=J?$vDXKPoXjYF;G z!X;!L93+NE)NkL2ahUdZVtycGmV8QJV>QY|5QVPF!fwEoOLMQ=8%IZyNO%JJVkNVA z%vUn%trf7I^=`S02C*#jr4`A7pa3Z{31TT--jOl7i&BJbGsOb!g%1FQML$!*5#dr5oPO)HXF_a zN&$1$r2UtA$3EmW&43y;ZZXBL5^CFKCu+gn_p6Eq!-L%YUtNIL;}+@_kiV8&3zzcGC%AH7}%!=6ci~L^TXqLAt({#m#tt6gLE1_x8 z#9$5gpZ^nt-j|@6`FOqArY{Y@VfJj_5pzGtV#JZEM~>_<9-0L$kp9lM%JD0-TJ@T= z%{11-J*{gAH6+|*={et~K`q_f>*9+#AIj(>Lv)IYpEv)Joz=;{*fW_@^87lv{L4bqzUO|xK49Ob z7y9K`ZS`#MwT9!&Jc^+%3>UP`=VO%O>|kR|&du*s5r zSQT>KGC=zF_IK(oCgwJ$bYmdF+y>Ww9&~&Y{u*RVH!>>acICCN_?Tp6Yo7Qq$>7CW_6I=_^@nyHf=ZMn*@(J!%Z=kx??IS|?e71={2KSEc$X zE-&%|?qCP|3rW3%6%%#hg-cWmW=HS`li-73a|=s1%4u4#{`0=efQVZio0q}m!FgT7 z8~*?r=b5xXnHrtg^3Rm`qQ|9AUAFo;VqUYNL4Ts!>i= zpPZB7ljAIivd`JL-F^yx`|OK=V0YtKOFdTTC|ADf3VffqKzJqxt$Y|37v{ zqFtZ9(p~$e{J0oa3#{dRkd(Zujc>{18~SO+u7DcfU^Nwc31tnp$MtAJDoV~hYG z>BV|lOjuJe7euVq{rAVYXbLYN>AAIekP-%gGmz(dlz)szT&>UNxwG2aPmXCZ>J|A_ zW(L8vaKo1=YJYTnqgZkhRG$&gqG=l0W3a#tAaJ#B={jM%q?MzpwNus`Skt#tH{pqL zE-Wco0W#{N@oTCu$F)vpLm)eXVczCB{Gw>QG5gYv^QE4*#sdeIHcHFs1a=gtV8Y$d$*kcsTlyPd0$-w*HKRV0M94%@ zk~@OLgp$k==F*uN;Ax7`gMSad%fXdb=|{|Y=aGAo+Fq1ch3cK8&t`6shKa_}z)tWqw&KL9c1y;Gp+X0IOf`*ojk=_`RvN7X7;)PY=(rJZaUn z{4=Z9V(K{dM6$>YQ?BOGG5UQ|=mPrIDSRx+*@hnQ5{KSU%|#YyEn4_ zN2vr0CMk+VJSG`()j5RQP9W6`i zIp4ICs{_~8Fw^=i!9?gRKW6sL7vR$0FT27(KN6dbj37?A-s)Y)#4gY_F*A;a9u;?0 ztCyjt4JoME5M?gQ7xyIhK5NPh>?68f#iq=0x{c=&rU0b6C@s!`my1iJx5=*G!W>Q~vyKKn)vsALoN(95WQ)jJ~` z-Su;DbZfM2+Kb>At(nF5V6l4)-7Sk^gSi-XIa+)!_Ob)SRd-CEQK%WBm@Xcg1E+O) z+J|r_plA!qx@{2EK*~Yd13=}k#7cR~OSFptRcXhb!h_i~NUcg(`uf8gyNH%nP&3VT ztJ`E)P)H6wJRgqhH!3@Yc;-KSfo^OT;45rtXzo8jqLD0F41kL(ZPs2ivX`?}JHI5u z*px1R*cFVj3KdAyex0d3hyw?KOevbf3uqi4;MFl+0gDRM*SFmin1^IeBd~SQd*7Tv z;Vyl5_N7L^gZ0)6?|lXCSw;<3#%ZRnGp7GY*SZ)UVh5~g?r}^-gfg_^Cr6~CzSE!r z!W>OVDA!Ps`IC~8`G)P11_koXj7oorqW|pUz(e!LQh*9GxE5hM7ZWulN4D`Cm8NRb z`tZmkMLAIS+t=?;6dgWoi}OK$DB&ss{utFPMme z;#h^;qQf~}90v;o)`pC-9kHse@5C&i3Msj^3J}Qz>KObON!9yRn#Jqm7+6#xaUck} z+#O{zcrFTwN=Fmce>mioRA0I?c)UUI*nIOoL#H41uV>qruN{?TJ$k~DnO)TEXLnJC zpSx8TQbdm4YY7E|Vo8tci`Y&(M;A*L$Q}f-Aj+-w)>Nsb;8<-+sLRpRxni$RE}_Xy z67Gz~qrll0e;%>g95cIQsn@TqNJy3y>@xvT>rH&_Yfn~0PVY1 z+#>J0MA4U{Y+ZYTd?kGzAGWeSt0A#nF`DsNTOQ6xQ&O}C9O1BgdZ`avZ7DbIoCw@d zlZ}+S^l8HopVf5%(l^m?^k(7<00I9V3f5vsQ}kGgZ|=;CXz2dYQ@F z1@DZ}w{2N`z}3!{8!e=yPGufaQrTcl)raqbQ8%1IpE+1@BOWw$68n!^9_vyXIBds_ za$mnbkK6mB!tdN2_v2Ef*=@f55|N(y?ndsl94XNYKx6_eq+KyEye9_Cv zd4dEpJ!izVt5|Dp1zAmcNJvl<*fvT7!<7!-ii#q^UE*Z)SdnvWX+=&I1Na{MFiL!XR{RaHtnz7csClTQ) z=&{DLK=p-98^l&yaxi(UPoCl(&I4 z%stHa-(1QVn=|yso7zV`AFU3E`Kq)Y(v_=ew4(xcsHI;x=LI%6Y)8_+wTD9i8PyK$V`@Y1I)J@e^3I>Mc zS;s%0XlUcFynoX%LNJgPp2U>p)6Ots&MaUS7j0)-6quwmQ}&WCJcZY1o#uuM(lG|E zd5EmaVlF3Dv{cHWh11{`pbQ>^_XxyE;b{~lViMQu$4}NGkENE+9)A@1tl2Z3zbA6% z>;8&C7hq^3zl|1$+;Zl347m>Lb6(YN#MM#nj7!9C8TN5IRl9pZN?UEE#MLWSgF2Pq z4TT?it0sMnseYxWl^$auP)agdY!gLR$#P+*=+H1$lnQ*XNiWuzXvuJ*S9iWjuC2=& zHIeQ;t;y=JDa-G^u*U+HU5f~CF^&V{JwDDZWW7c8V(pp+>+Plx#lDl{yqLjIp$l3m zn|MS~npYV<4tPJR$uWozV7hW3BdTN}d zF1GvWpZzBl`FVK)qfqRY+fq%SKmaC@z5|0xs~o)^j2z0rhwgqi5XH_1Ef{H{P+17u zgnbHqz(+VzA6t?!|9h+opZl? zzLu4i=ju(d#wL*G>z)r^rrOh|`Igk+bA>H6WpPl7Pu9S9F2;E(-dcp=9+kJUvNJ&Z z7!)9$JSgspwdZylma3?)*x+Rl_V7lS1IVU9L`uWFf=b7Ca#6q>rPSeP^{-908qcxb zoYkFZriY^R=N}axoo?7p%+kcKI7R%>khM7DWBiXI!v+>+=e&fe6#&ljMiJ!3b|{Tk z8g<;-7}!0eX{d+S9#3Ha>V_kpzna}Pu)eKVib3z^0awm)lt*lh9D$jEp%xTfdeh}#IGj;38lqO0 z{vBsnT5eNOsTd&>W^4$3_0ugFC#>@5I7TrD+Q30%+8cVF{lN_av?w`7Hp;?kA=4Xr z(+6yo#blmj)+>#|R?WsW1r7UNV@YERT)lRI9U}iIG%nAAUS%kvmCNx~a%ISvj`aXD zB=Zwe>(DlkqKzzK)yO#z~|5B_ln)-1bkWS8cW5p9i83z_me4u3u* zX*d-ZlXoM}WJJtg5EyWY0fm|L+-s9#QreeK+5v`;Gh?-DAKrCBJMwgJne%J}D1@}r zV3&5%SnzQ0R1%*V*9cVx3c51uK}$BmglwMIuw@}lJ>q@Btb1MDY?)S}f17Bc%(sJ{ zwRK)zRAlU50Gq4IH(FHOb=#|!xA9|776xcDhens|l4lnPZw-56?7F8GN*nGj!%D4* z`zz$kHR}e&r5$vm_7xp(l}tt7e}C5@$^5NW(-x%(jEyWv`|T@XSjY<_%EMs zv&XS{#&sk$I@Gnk?G?Bd2c3DKIwfA;Uh*4}w`IyxgWPj6`w%yZD;^`#8x_1Cu*+;WDB-U*>2swO@ip2Os~noJJlxVqkSHMM4#My{S##a^lk z2nD32v)xA1TjDH}x>bj@K6IR|j7vInChpjolV%yZ)3q(ZI|pW=2QD0Ce_^b8WOfJR z{r%hcH3v}{)YO5j42PVvbrg*w3hlIUL8n6Re$@1TTNaTy;aQZBqnFIS>5;yAC6+tp zi@w_)L1KLqReAT1U3x5|Qs|-#!^;3+nd6+e zl@w;{_h5EODH_@02~wdLAW+FvELPRXESaCVHEsXOBjVvRg&OVcN`d;sN%$uR&4{CQ z`#@so+*LttjCr$n-j@1 zr9~K^SV|2kmv`Y*jX2f5hbEq%nMt*9Q!QtNMm9>L0r^ITx`({!{@Bp^+6>73>&aM! zI=>aV@)g|Sb4X?_KW_78ADc6J@i>~9;vRN_m=z5B^GJeJoV$l%O1v{%3a~n!VRaor zOhqEOi@g>qP2_pffVx6QB^w(izv8YrBA(`Q4Dz(#oRQk5+vJigQQE3#!-(^_ZN~IN z`qyti#=T(2Z+}=&Ukh~X{N7S+`;4I!fL%xlpJ z>#T}Pu_8Tw$W2mHx2iy$3p)%tZg9&zn%jHSeJ*8=#qDT5jhjt5d#ySvEP+$;L)`;q z*r78uqi~m7SO=G^Iggb}hqZ(rNRWD_g248AzacGmUyfQ@KU?DrKu>sG1S|6oOjX8g zlzrLnr`tT=nuO=vl@~1(6}@3>u#R{BERVKzw^)6|0sD(nA&J*_oSV)++H$1NJAR?L zJy{lUsoV&er^SWV69#{BrNz zvHL~Hw}(S75>_68P2Olhb9LqP8hS3&198-DsEeRmj z6Gymvt)T43b#nEC_tsm-=U~kE2SB~_2Hxd4Nb;pQrfIy}RT<`ncJg$drVdl+zfaBJMJdie>6ja zx=6uuL+>+(d2Qy1$L03uU3g)Z<9d{GA-3u8)F)>Og$wFDZB%RHS@FlZElV~Q=Ez$o zd|qlbSyP#@DSm(NZR%LP@JV0$sp2=(@f_s+`-QRb20ym;MlfikfV(enmDGTIr1Mxm zeerFGYWUl(Z$8*a0=_A)s4cnW^}Ne(SKK$ zAAi%KqkVUU)38@)q4Qyps2Pcdp~Y+=H0`Nu3oipCXFDT+nWV^4$M*{wm?+O3Opm>> zstQ|SAuq~N;$H|fS!AfH1wK4`Q+U`u`C^M2?lqf+S) zWKbA(ff}SGkL{i4m*n*Ctp5@7*|cZg{J1Vau311+av6?c|07J&X3UBOBIUFstUXK9voNf0n%R7dTufN>tk-dsIQ;4b~xBa+_|xY}-14V7SNZ0nUtX zoNQVTo`6L$Ggx2SI9Ryq2Y~}DXv&nt<`E9iG6z-FG0i1siSCrhXa|G7D%bzx%&b*PYtSCzi zx}05?pxWu}RPrQt98>ph!7^+A^qtqAGr~gD^q)Eg$iwLZI#|XUjoMyxvSnT&^J_V2 zWiFhFC`IBI5lUl;yyIN3FZ8{*=8U`XNhHb5*`8>0^n%G#HA;%`p>JR8J`E8?S-1$>OUFfEd$pTY-@Yp|nk zBo3aFG6jEAYJeQY&8gvwk4=HWEw|qBT!Al#jpoH#HHc_|Rlo`VBuCR4j(`i+z^8X9-_#?D!u)SP zr|=wuA{{-iT>UX_`Nn9L$C?gcZJB;Cm^}?h=Al+aS48QNStq{y4i?o%P0`;%Q=y07 zQ?`Gw&3O)1N#ZVpW$GRG)^zC8q(U#^CGkO|m~xMoJ&^v|vufrW!TIj#36bi^!Nrm6 z^aZnf3t67&GwD6Zv>$)N`h7;#A9*TkZZvKPrl-au`t8o&<`sISEOhz?FWqDn4oX<8 z@&;FJmbfDzOhT(cr=7IxGG#1jx%@G5fWbw@T+xxP{g>MGpB3u+JBshL>MOfLpKcrl z2RLX_@R*kRKabGa1=loPO=2)AL19c>05p>!R;Ef(*y5o>`PM2Nb!D7PSXu%CcX>2V z+bE37so>q+iBCSNde$Ws&0)bfwTX?}dR%$wQ4Aehj1Oi+D&oE)+{$pSE}2Kja&H&W z%^vF;r7mU=!;BYi#O(l7wcN_>-ByCA<5l&X3CGm)x|j`@09AApsK^|=lE5qt zknVILcglEb%XZ|Lu6xd9*M&4BqEA$IuO#HBLB3 z$IaNw^9AlF<5=nC@o{VL>+=bPV*2}Xdw}~QdVUETeqH9^Oq0M${gU_IxBlnD{(my& zaN(cq74z{evY0qdJlAI2^X`2^&R|X92G_Y+39R02TDz-%C~{KUT~Y*13Hj;Z6}`l} z(*x~>EdAIIW%Bex?^lNZ zwWRBLA7KCLe?;%7!hXYxe)uQ^7$Pm9|}$HOida((w}Ip+e|w!Pkd~ zre=no4@}QIUfmg6;kaK^E~Dbn5#i3z8#$4_(0*@NMa{cb9>4N(apuNnW7R8$n@W%0 zHlo{-AG1#1VSIe!i_Yt>6lVfvwa0qk6W?hG)C3wo!XgJ5KuBe=mMBN=J;WqI28^GjwJ1YGFEm_H?o*`L6d2&5|EIMU$mzg9m zfhAWYz#|w}C=w`FVCPlLQK>-rns)L&wP#n4yhs(Q44RfB8UY#rmqwwbpYe`71!Rtra#Io%JwP%BHzSTJ zx)hO@NEe@tz-qdc_ss$)EN5Bz`%h>j_@{2Pzs{A!v{#D!vHvTS4+5&q z=zpF75+oUa8y0*1KtNbwsh4j~`7T`3?6!>x6NhLw>+PO6OIEXIq+1u%K0w&7@6dNk>YNAlWnz2?tu1@Z2sl z&LWQ2)`hlk6)dCnn|O~U&aCDMS9g4oEitS)%D<>C4N+}OXg*@yy-#f7te8Aw=9d@xTMB=#ukL~^xDm7umNU8oI~D~^LS#)oBM;z@ z(W|*5dn0dQN~IE}61~8x&2s=TQ4&=pcTFh_)v9GwLdxvWi@VtaO6L3RedYqq#E};S z)B)NTWs9dk7<*vt%1OAo>G$*LCyV+cI!2n#d~3WTK23!d13Io$dc6*Vq@N=%_SjJ4 zIEY7bPQ6`gSG8NqVFyk%?GrA_BM3O--4Q1hcefs+0SB(_ZjKuo9ahxtmAb4X3ciC&Bj3^ zrCrwb5?0P#bnDJgSGZY?2~GeF;NW73X8<5AQvfwe1{LMU3AY&mTJ zPsV!z$y3Hb2KZmJe-lOfPbQuSyzs%IP?{l8fW3?wE898EW}|AMBSWuF+yz~&RtX)6 z!-ptYV?Xi~9l8NqXUee`a-0o}G1Ars5zfA;TyBsE`&|l7qCl-0i|avTAr#)utO928 z_xbv1<=l|)2tn}Imm>O8kC&BD)SFm=Q|8|X$ot=Y3|_OsC!?#gST<3iA9(|6UeYKt z+KZ^_xJn13;+};vGb*6J{9?m)8B|utN$&XMb3Li#cXb6Q^kTPE*}T3+l5jZN7wweOy|L%)8$eQ4BU zA=b68Mikg#T?8IYP}(0Bu$N--N}^0tEf!r{mB3EX9`;JxIzx5qYq2i%c1>tvyp#lf z+}%Mq$9Tjjcf6{0&*2CVngs-^NG~r6L&G!gGReIUE++{_(6^?#fGUEgq zx2o^SO&@693jDg_LG9)+A!@@?3BZo}rANW-T*B^# zG1*?D(z)8Oq~Y`*9M^!=-9LcURD640Ye#-K*rKVILzbOTo$)hkIoR7xvYX-Wc9^`V zJG&!g`_yoYgmO9R_OQ(~08OMBan`?|GClmddHw6zj>p$BL0_M`d8mWN1-53T@^k@k; zmy|sk+o1OZN4cGChQzkXEM%&s^z>U3t6%SK_JK3Byi6#PlSb7mfc!+qF|N%{$loEp z&RrcU7BpAI$_Ad&VV9Wc*_{tgSXWW4g&~R(crq0C^N$Sg$0Xe(v+p`i9^8;4KTY{qW-bPvpIV?*o-pNloYWKiIQn!H2eSS;|<3*N4}1R55^}uQ*AM zua7uMo(hZ>Jv+8&FE+SbHlp;kVBNW#8C>c+vaT)79ONkd-vx0N=-Y%ETh=zGuI5-so4oGk=s7YQ(X5?2|F!d3qaL&eCmAiyBZGike|fP17LDQC@bk6=;F(wxcg5YcsB|Iz3u3IpN`S^NN*= zuZ6J+h?ZtFA;u0^3b*11VpT|aQg|{26Z^&R8B&D0Ihjbz&uYK?vLE+b4ffTBxE2Gu zA$zqbn|JhHK6jlJ1?T`sHh!$&HHLrg;bD7`29%=t17*~kQma9fPhCl|AG>4Gx!-eX{#Yf$#IaivqZ(s}62jhS_g%7lLHQ{?s+*_YPp_gzptfA2~j4rUZH=9+%e zUlPE1W%8cDBm{!-RSon70GnE&SL-d>`{XR4MfG8H^b24f6U8-(;eN7G84n~UA6nMV zWz1GL+=x%6bz`FX(A1_?q38{Hhi5zvh-I|DO?~{@eHuPztR&9-T??F3NmKbp#0%d-zKymsm2MR11JTRSzyu| zVWOhLNiU9)*v4~e1kbRiQ+6g)a=LThR%DyERs3D<2CFlq^L8igvsp#9YG3G%MVRuA zuQ`CcO^%8|VEZjXg!j6fO{XvUHK9k9TH1AW&I-sn2X&T%iB3>71isl@=&)=+$3kTm zWzv?=m@*GBmxEI#)qu3tb!_#m9jA>kIF#|`>#f`vb*r!Cfm3yDK{8e@*Bb`&)^_*2 z#sxz6^8VPb8AVVQHlQkawQsGX_&sMCo~|D)_8A%iaJ$^}imux_NolKc=lKmu^-Jk? zZi=rlnxBm6N0XkrfU5xZDbAPsnh%=i@2~pEo^daC^?Owd0+f;ruS$?34Rs(q*xK|ZT2=#kPzzixPfGQ|0V@Wo z5cYVFu~k{v>3v4ldWhfw$oQ{=nWk-+?-eF{fm|)pFW#ZDwh4A0;LINih^A%DzcEe1 zJlzpT@zi&Vj~{(X5NEH4VNKn6DU$bIwP}r0B|JwS5+cPL%5MlUf!({r?G?~_L5~|u z<E+>*(y*5pT*ocwDQiZm=c%X zuveP+gYbj9u~`Zrkc$jUM&IfkNqX?m((E><=Vy3nf-z6av{~CH=%^)wnde|rG(GNn zw&yCN)QHd8yYRH=;3(HAbU6x@rlNfbH*bNc_7<(O;hot!pT z;Gl_RQjA(1s!n1#p?c+(;fAMlsc3)ADJymw$b0kPtEqL%6icll0kQU00=brSfqe9( zG#3#yVbaex`SXZ0ouKLtCDaVQfMOdVki6|yZk?e<1K%Jy_EW)iC8a(G=ZGA(HJ7iP zo}CLU*v7{-$RIFGOzM|I7+C1BpuuxrJfTbn#U*Wh(NN-4x9lo3ln#}cKaap?=FPod z-ka9cn`r#}^PZKi&-5nf{>g%qeu9v&;$$McS2Cf@-wx>gDCjOiR|bR`J>fvk<@H$L z0Hs6uVAUmS;$fou{T^uS>2jN$+IR#6w^0-0x_+JVvU5!;%4<06 z`j-)TYc3PqngY%d@OE1J->1m6Vbqd>zfN85_qoJm&7?v#BkRR8I*#DG614MJ?9Qn? z%6XU8YhcYx+9Snsm<^adZoR~9=5o2_TXdBU(yXe$t>bcI}?ev9$pS_ z2)!0Oax9*UbVmOIOHWXY1Wua&YY7;Z3)38PH9eo29X!UEPZ`}3)D@k-UWlbaC(AS$@#WP^VKu4x&F>DVMT0Ib&XmrWW4#ko%D_Fm!W^nFY{1@r zlf(ZxDf@CuuEmD)jhm<&MWmwjLhj_xd_k_^%kLW7MIA;4SSNcEQAL&`F)8;xZw_2G zR#jzK&5uM7=T>N|`-w_;`Mne_8R&P|#<`ebK`Q+@NQsHI0sBkDOWCKbtt~J6rA8B{ z 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 version.parse('1.12.0'): + # on >=kinetic, it raises ROSTimeMovedBackwardsException + # when we use rosbag play --loop. + kwargs['reset'] = True + self.timer = rospy.Timer(**kwargs) + + def update_diagnostics(self, event): + self.updater.update() + + +if __name__ == '__main__': + rospy.init_node('pseudo_diagnostics') + pusedo_diagnotics = PseudoDiagnostics( + sensor_name='sensor1', + duration_time=5) + pusedo_diagnotics = PseudoDiagnostics( + sensor_name='sensor2', + duration_time=10) + rospy.spin() diff --git a/jsk_tools/sample/sample_audible_warning.launch b/jsk_tools/sample/sample_audible_warning.launch new file mode 100644 index 000000000..25abf7e2f --- /dev/null +++ b/jsk_tools/sample/sample_audible_warning.launch @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + speak_interval: 0 + seconds_to_start_speaking: 5 + + + + + + + + + From f0aec043efd74069effbc37b72c93c5106d1bf2c Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 22:13:30 +0900 Subject: [PATCH 21/54] [jsk_tools/audible_warning] Add test --- jsk_tools/CMakeLists.txt | 2 ++ jsk_tools/sample/sample_audible_warning.launch | 4 +++- jsk_tools/test/test_audible_warning.test | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 jsk_tools/test/test_audible_warning.test diff --git a/jsk_tools/CMakeLists.txt b/jsk_tools/CMakeLists.txt index c1a9e6779..6ae62abfa 100644 --- a/jsk_tools/CMakeLists.txt +++ b/jsk_tools/CMakeLists.txt @@ -27,6 +27,7 @@ endforeach(exec) if (CATKIN_ENABLE_TESTING) find_package(roslint REQUIRED) + roslint_python(src/audible_warning.py) roslint_python(src/jsk_tools/cltool.py) roslint_python(src/test_topic_published.py) roslint_python(src/test_rosparam_set.py) @@ -49,6 +50,7 @@ if (CATKIN_ENABLE_TESTING) # https://github.com/jsk-ros-pkg/jsk_common/pull/1293#issuecomment-164158260 jsk_tools_add_rostest(test/test_rosparam_set.test) endif() + jsk_tools_add_rostest(test/test_audible_warning.test) jsk_tools_add_rostest(test/test_stdout.test) jsk_tools_add_rostest(test/test_rostopic_host_sanity.test) jsk_tools_add_rostest(test/test_sanity_diagnostics.test) diff --git a/jsk_tools/sample/sample_audible_warning.launch b/jsk_tools/sample/sample_audible_warning.launch index 25abf7e2f..df665639c 100644 --- a/jsk_tools/sample/sample_audible_warning.launch +++ b/jsk_tools/sample/sample_audible_warning.launch @@ -2,6 +2,7 @@ + + pkg="sound_play" type="soundplay_node.py" + if="$(arg launch_sound_play)" > diff --git a/jsk_tools/test/test_audible_warning.test b/jsk_tools/test/test_audible_warning.test new file mode 100644 index 000000000..12ff0904e --- /dev/null +++ b/jsk_tools/test/test_audible_warning.test @@ -0,0 +1,17 @@ + + + + + + + + + + topic_0: /sound_play/goal + timeout_0: 10 + + + + From 9993337425058d7b44199e7039494e72e3b390b5 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 23:03:57 +0900 Subject: [PATCH 22/54] [jsk_tools/diagnostics_utils] Import zip_longest for python2 --- jsk_tools/src/jsk_tools/diagnostics_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jsk_tools/src/jsk_tools/diagnostics_utils.py b/jsk_tools/src/jsk_tools/diagnostics_utils.py index 42b8142aa..34d4ea8bb 100644 --- a/jsk_tools/src/jsk_tools/diagnostics_utils.py +++ b/jsk_tools/src/jsk_tools/diagnostics_utils.py @@ -1,4 +1,7 @@ -import itertools +try: + from itertools import zip_longest +except: + from itertools import izip_longest as zip_longest import re @@ -62,7 +65,7 @@ def filter_diagnostics_status_list(status_list, blacklist, if is_leaf(ns) is False: continue matched = False - for bn, message in itertools.zip_longest( + for bn, message in zip_longest( blacklist, blacklist_messages): if re.match(bn, ns): if message is None or re.match(message, s.message): From 27d2fe17871cf2c9cbedc3051805c3a5af5d81f2 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 17 May 2022 23:37:40 +0900 Subject: [PATCH 23/54] [jsk_tools/audible_warning] Use LooseVersion for python2 --- jsk_tools/sample/pseudo_diagnostics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsk_tools/sample/pseudo_diagnostics.py b/jsk_tools/sample/pseudo_diagnostics.py index c45aed031..92f0b4d4c 100755 --- a/jsk_tools/sample/pseudo_diagnostics.py +++ b/jsk_tools/sample/pseudo_diagnostics.py @@ -2,7 +2,7 @@ import diagnostic_msgs import diagnostic_updater -from packaging import version +from distutils.version import LooseVersion as version import pkg_resources import rospy @@ -34,8 +34,8 @@ def __init__(self, sensor_name, duration_time=5): callback=self.update_diagnostics, oneshot=False, ) - if version.parse(pkg_resources.get_distribution('rospy').version) \ - >= version.parse('1.12.0'): + if version(pkg_resources.get_distribution('rospy').version) \ + >= version('1.12.0'): # on >=kinetic, it raises ROSTimeMovedBackwardsException # when we use rosbag play --loop. kwargs['reset'] = True From 769ccc45fc24f5a79d9d06d6dd1e9b8aaa33a98e Mon Sep 17 00:00:00 2001 From: iory Date: Wed, 18 May 2022 00:29:11 +0900 Subject: [PATCH 24/54] [jsk_tools/audible-warning] Set volume if it has volume property --- jsk_tools/src/audible_warning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index f0b49171f..5dbf904a6 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -98,7 +98,8 @@ def run(self): goal.sound_request.command = SoundRequest.PLAY_ONCE goal.sound_request.arg = sentence goal.sound_request.arg2 = self.language - goal.sound_request.volume = self.volume + if hasattr(goal.sound_request, 'volume'): + goal.sound_request.volume = self.volume self.previous_spoken_time[e.name] = rospy.Time.now().to_sec() self.talk.send_goal(goal) From 9b24b9c2b552e1cd98115d138d38e48cdd573ce6 Mon Sep 17 00:00:00 2001 From: iory Date: Wed, 18 May 2022 01:13:30 +0900 Subject: [PATCH 25/54] [jsk_tools/audible-warning] Fixed test name --- jsk_tools/test/test_audible_warning.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsk_tools/test/test_audible_warning.test b/jsk_tools/test/test_audible_warning.test index 12ff0904e..9efa2e8b3 100644 --- a/jsk_tools/test/test_audible_warning.test +++ b/jsk_tools/test/test_audible_warning.test @@ -5,8 +5,8 @@ - topic_0: /sound_play/goal From 188c2e82f497b03124731b9639bf80aa6d4d2bf7 Mon Sep 17 00:00:00 2001 From: iory Date: Mon, 23 May 2022 14:51:06 +0900 Subject: [PATCH 26/54] [jsk_tools/audible_warning] Split sample diagnostics_analyzer --- ...mple_audible_warning_diagnostics_analyzer.yaml | 15 +++++++++++++++ jsk_tools/sample/sample_audible_warning.launch | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 jsk_tools/sample/config/sample_audible_warning_diagnostics_analyzer.yaml diff --git a/jsk_tools/sample/config/sample_audible_warning_diagnostics_analyzer.yaml b/jsk_tools/sample/config/sample_audible_warning_diagnostics_analyzer.yaml new file mode 100644 index 000000000..c6c0feb93 --- /dev/null +++ b/jsk_tools/sample/config/sample_audible_warning_diagnostics_analyzer.yaml @@ -0,0 +1,15 @@ +analyzers: + cameras: + type: diagnostic_aggregator/AnalyzerGroup + path: Sensors + analyzers: + pseudo_diagnostics1: + type: diagnostic_aggregator/GenericAnalyzer + path: sensor1 + find_and_remove_prefix: 'pseudo_diagnostics: ' + num_items: 1 + pseudo_diagnostics2: + type: diagnostic_aggregator/GenericAnalyzer + path: sensor2 + find_and_remove_prefix: 'pseudo_diagnostics: ' + num_items: 1 diff --git a/jsk_tools/sample/sample_audible_warning.launch b/jsk_tools/sample/sample_audible_warning.launch index df665639c..7ebc1a4d8 100644 --- a/jsk_tools/sample/sample_audible_warning.launch +++ b/jsk_tools/sample/sample_audible_warning.launch @@ -12,7 +12,7 @@ - + Date: Tue, 28 Jun 2022 22:36:55 +0900 Subject: [PATCH 27/54] [jsk_tools] add rossetclient --- jsk_tools/env-hooks/99.jsk_tools.sh | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/jsk_tools/env-hooks/99.jsk_tools.sh b/jsk_tools/env-hooks/99.jsk_tools.sh index c07ec0fb4..22e2ccdfb 100644 --- a/jsk_tools/env-hooks/99.jsk_tools.sh +++ b/jsk_tools/env-hooks/99.jsk_tools.sh @@ -44,7 +44,11 @@ _update_prompt() { export PS1="${WITHOUT_ROS_PROMPT}" fi elif [ "$master_host" != "" ]; then - local ros_prompt="[$ROS_MASTER_URI][$ROS_IP]" + if [[ "$ROS_HOSTNAME" != "" && "$ROS_IP" == "" ]]; then + local ros_prompt="[$ROS_MASTER_URI][$ROS_HOSTNAME]" + else + local ros_prompt="[$ROS_MASTER_URI][$ROS_IP]" + fi if [ "$CATKIN_SHELL" = "bash" ]; then export PS1="\[\033[00;31m\]$ros_prompt\[\033[00m\] ${WITHOUT_ROS_PROMPT}" elif [ "$CATKIN_SHELL" = "zsh" ]; then @@ -86,6 +90,7 @@ rossetip_dev() { rossetip_addr() { local target_host=${1-"133.11.216.211"} + echo "target_host: $target_host" # Check if target_host looks like ip address or not if [ "$(echo $target_host | sed -e 's/[0-9\.]//g')" != "" ]; then target_host_ip=$(timeout 0.01 getent hosts ${target_host} | cut -f 1 -d ' ') @@ -125,6 +130,24 @@ rossetip() { fi } +rossetclient() { + local client_interface=$1 + if [[ $client_interface =~ [0-9]+.[0-9]+.[0-9]+.[0-9]+ ]]; then + export ROS_IP="$client_interface" + else + local addr_if_client_is_device=${LANC=C ip -o -4 a | grep lo\ \ \ | cut -d\ -f7 | cut -d\/ -f1} + if [[ $addr_if_client_is_device = "" ]]; then + export ROS_IP="$addr_if_client_is_device" + else + export ROS_HOSTNAME="$client_interface" + fi + fi + echo -e "\e[1;31mset ROS_IP to \"$ROS_IP\" and ROS_HOSTNAME to \"$ROS_HOSTNAME\"\e[m" + if [ "$NO_ROS_PROMPT" = "" ]; then + _update_prompt + fi +} + rosn() { if [ "$1" = "" ]; then select=$(rosnode list | percol | xargs -n 1 rosnode info | percol) From c6073bc436b5fdf48be09c0786aeffdf646ab60c Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 28 May 2022 19:46:17 +0900 Subject: [PATCH 28/54] [audible_warning] Add diagnostics_level_to_str --- jsk_tools/src/jsk_tools/diagnostics_utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/jsk_tools/src/jsk_tools/diagnostics_utils.py b/jsk_tools/src/jsk_tools/diagnostics_utils.py index 34d4ea8bb..a65c506bc 100644 --- a/jsk_tools/src/jsk_tools/diagnostics_utils.py +++ b/jsk_tools/src/jsk_tools/diagnostics_utils.py @@ -4,6 +4,8 @@ from itertools import izip_longest as zip_longest import re +from diagnostic_msgs.msg import DiagnosticStatus + cached_paths = {} cached_result = {} @@ -75,3 +77,16 @@ def filter_diagnostics_status_list(status_list, blacklist, continue filtered_status.append(s) return filtered_status + + +def diagnostics_level_to_str(level): + if level == DiagnosticStatus.OK: + return 'OK' + elif level == DiagnosticStatus.WARN: + return 'WARN' + elif level == DiagnosticStatus.ERROR: + return 'ERROR' + elif level == DiagnosticStatus.STALE: + return 'STALE' + else: + raise ValueError('Not valid level {}'.format(level)) From 012f51436a55cc978056d19789c92acb14c760d7 Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 28 May 2022 19:48:15 +0900 Subject: [PATCH 29/54] [audible_warning] Add dynamic reconfigure parameters --- jsk_tools/CMakeLists.txt | 21 +++++++++++++++++++-- jsk_tools/cfg/AudibleWarning.cfg | 19 +++++++++++++++++++ jsk_tools/package.xml | 4 ++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100755 jsk_tools/cfg/AudibleWarning.cfg diff --git a/jsk_tools/CMakeLists.txt b/jsk_tools/CMakeLists.txt index 6ae62abfa..1140b01fa 100644 --- a/jsk_tools/CMakeLists.txt +++ b/jsk_tools/CMakeLists.txt @@ -1,11 +1,19 @@ cmake_minimum_required(VERSION 2.8.3) project(jsk_tools) -find_package(catkin REQUIRED) +find_package(catkin REQUIRED + COMPONENTS + message_generation + dynamic_reconfigure) catkin_python_setup() set(ROS_BUILD_TYPE RelWithDebInfo) + +generate_dynamic_reconfigure_options( + cfg/AudibleWarning.cfg +) + catkin_package( - CATKIN_DEPENDS # + CATKIN_DEPENDS message_runtime # LIBRARIES # INCLUDE_DIRS # DEPENDS # @@ -25,6 +33,15 @@ foreach(exec ${BIN_EXECUTABLES}) install(PROGRAMS bin/${exec} DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}) endforeach(exec) +file(GLOB SCRIPT_PROGRAMS src/*.py) +catkin_install_python( + PROGRAMS ${SCRIPT_PROGRAMS} + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/scripts/) +file(GLOB SCRIPT_BIN_PROGRAMS bin/*.py) +catkin_install_python( + PROGRAMS ${SCRIPT_BIN_PROGRAMS} + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/bin/) + if (CATKIN_ENABLE_TESTING) find_package(roslint REQUIRED) roslint_python(src/audible_warning.py) diff --git a/jsk_tools/cfg/AudibleWarning.cfg b/jsk_tools/cfg/AudibleWarning.cfg new file mode 100755 index 000000000..21e4997bc --- /dev/null +++ b/jsk_tools/cfg/AudibleWarning.cfg @@ -0,0 +1,19 @@ +#! /usr/bin/env python + + +from dynamic_reconfigure.parameter_generator_catkin import * + +PACKAGE = 'jsk_tools' + +gen = ParameterGenerator() +speak_group = gen.add_group("speak flag") +speak_group.add("enable", bool_t, 0, "Flag of speak", True) +speak_group.add("speak_ok", bool_t, 0, "Speak ok level diagnostics", False) +speak_group.add("speak_stale", bool_t, 0, "Speak stale level diagnostics", True) +speak_group.add("speak_warn", bool_t, 0, "Speak warn level diagnostics", True) +speak_group.add("speak_error", bool_t, 0, "Speak error level diagnostics", True) + +gen.add("volume", double_t, 0, "Volume of sound.", 1.0, 0.0, 1.0) +gen.add("speak_interval", double_t, 0, "Volume of sound.", 120.0, 0.0, 3600.0) + +exit(gen.generate(PACKAGE, PACKAGE, "AudibleWarning")) diff --git a/jsk_tools/package.xml b/jsk_tools/package.xml index ef0a9d455..afeeb7a2d 100644 --- a/jsk_tools/package.xml +++ b/jsk_tools/package.xml @@ -15,12 +15,16 @@ catkin + dynamic_reconfigure git + message_generation rosgraph_msgs cv_bridge diagnostic_aggregator diagnostic_msgs diagnostic_updater + dynamic_reconfigure + message_runtime python-percol python-colorama python3-colorama From 7473d0736cefbf515417885ffc6a74cff199d74c Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 28 May 2022 19:48:39 +0900 Subject: [PATCH 30/54] [audible_warning/sample] Fixed audible_warning node name --- jsk_tools/sample/sample_audible_warning.launch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsk_tools/sample/sample_audible_warning.launch b/jsk_tools/sample/sample_audible_warning.launch index 7ebc1a4d8..236c97e14 100644 --- a/jsk_tools/sample/sample_audible_warning.launch +++ b/jsk_tools/sample/sample_audible_warning.launch @@ -22,7 +22,7 @@ - From 9bf9f1b652ecf8b591b59bddce862d0588bf86b4 Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 28 May 2022 19:49:23 +0900 Subject: [PATCH 31/54] [audible_warning] Use dynamic reconfigure params --- jsk_tools/src/audible_warning.py | 85 +++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 5dbf904a6..f93186283 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -12,11 +12,14 @@ import actionlib from diagnostic_msgs.msg import DiagnosticArray from diagnostic_msgs.msg import DiagnosticStatus +from dynamic_reconfigure.server import Server import rospy from sound_play.msg import SoundRequest from sound_play.msg import SoundRequestAction from sound_play.msg import SoundRequestGoal +from jsk_tools.cfg import AudibleWarningConfig as Config +from jsk_tools.diagnostics_utils import diagnostics_level_to_str from jsk_tools.diagnostics_utils import filter_diagnostics_status_list from jsk_tools.diagnostics_utils import is_leaf @@ -33,7 +36,8 @@ def __init__(self, rate=1.0, wait=True, language='', volume=1.0, speak_interval=0, - wait_speak_duration_time=10): + wait_speak_duration_time=10, + diagnostics_level_list=None): super(SpeakThread, self).__init__() self.wait_speak_duration_time = wait_speak_duration_time self.event = Event() @@ -43,9 +47,11 @@ def __init__(self, rate=1.0, wait=True, self.lock = Lock() self.status_list = [] self.speak_interval = speak_interval + self.diagnostics_level_list = diagnostics_level_list or [] tm = rospy.Time.now().to_sec() \ - speak_interval self.previous_spoken_time = defaultdict(lambda tm=tm: tm) + self.speak_flag = True self.language = language self.talk = actionlib.SimpleActionClient( @@ -55,6 +61,36 @@ def __init__(self, rate=1.0, wait=True, def stop(self): self.event.set() + def set_diagnostics_level_list(self, level_list): + self.diagnostics_level_list = level_list + + def set_speak_flag(self, flag): + if flag is True and self.speak_flag is False: + # clear queue before start speaking. + with self.lock: + self.status_list = [] + self.speak_flag = flag + if self.speak_flag is True: + rospy.loginfo('audible warning is enabled. speak [{}] levels' + .format(', '.join(map(diagnostics_level_to_str, + self.diagnostics_level_list)))) + else: + rospy.loginfo('audible warning is disabled.') + + def set_volume(self, volume): + volume = min(max(0.0, volume), 1.0) + if self.volume != volume: + self.volume = volume + rospy.loginfo("audible warning's volume was set to {}".format( + self.volume)) + + def set_speak_interval(self, interval): + interval = max(0.0, interval) + if self.speak_interval != interval: + self.speak_interval = interval + rospy.loginfo("audible warning's speak interval was set to {}" + .format(self.speak_interval)) + def add(self, status_list): with self.lock: for status in status_list: @@ -79,7 +115,14 @@ def run(self): while not self.event.wait(self.rate): e = self.pop() if e: - if e.level == DiagnosticStatus.WARN: + if self.speak_flag is False: + continue + if e.level not in self.diagnostics_level_list: + continue + + if e.level == DiagnosticStatus.OK: + prefix = 'ok.' + elif e.level == DiagnosticStatus.WARN: prefix = 'warning.' elif e.level == DiagnosticStatus.ERROR: prefix = 'error.' @@ -112,9 +155,7 @@ class AudibleWarning(object): def __init__(self): speak_rate = rospy.get_param("~speak_rate", 1.0) - speak_interval = rospy.get_param("~speak_interval", 120.0) wait_speak = rospy.get_param("~wait_speak", True) - volume = rospy.get_param("~volume", 1.0) language = rospy.get_param('~language', '') seconds_to_start_speaking = rospy.get_param( '~seconds_to_start_speaking', 0) @@ -127,14 +168,6 @@ def __init__(self): < seconds_to_start_speaking: rate.sleep() - self.diagnostics_list = [] - if rospy.get_param("~speak_warn", True): - self.diagnostics_list.append(DiagnosticStatus.WARN) - if rospy.get_param("~speak_error", True): - self.diagnostics_list.append(DiagnosticStatus.ERROR) - if rospy.get_param("~speak_stale", True): - self.diagnostics_list.append(DiagnosticStatus.STALE) - blacklist = rospy.get_param("~blacklist", []) self.blacklist_names = [] self.blacklist_messages = [] @@ -149,9 +182,11 @@ def __init__(self): else: message = re.compile(bl['message']) self.blacklist_messages.append(message) - self.speak_thread = SpeakThread(speak_rate, wait_speak, - language, volume, speak_interval, - seconds_to_start_speaking) + self.speak_thread = SpeakThread( + speak_rate, wait_speak, + language, + wait_speak_duration_time=seconds_to_start_speaking) + self.srv = Server(Config, self.config_callback) # run-stop self.speak_when_runstopped = rospy.get_param( @@ -190,6 +225,22 @@ def __init__(self): self.diag_cb, queue_size=1) self.speak_thread.start() + def config_callback(self, config, level): + level_list = [] + if config.speak_ok: + level_list.append(DiagnosticStatus.OK) + if config.speak_warn: + level_list.append(DiagnosticStatus.WARN) + if config.speak_error: + level_list.append(DiagnosticStatus.ERROR) + if config.speak_stale: + level_list.append(DiagnosticStatus.STALE) + self.speak_thread.set_diagnostics_level_list(level_list) + self.speak_thread.set_speak_flag(config.enable) + self.speak_thread.set_volume(config.volume) + self.speak_thread.set_speak_interval(config.speak_interval) + return config + def run_stop_callback(self, msg): if isinstance(msg, rospy.msg.AnyMsg): package, msg_type = msg._connection_header['type'].split('/') @@ -208,9 +259,7 @@ def on_shutdown(self): self.speak_thread.join() def diag_cb(self, msg): - target_status_list = filter( - lambda n: n.level in self.diagnostics_list, - msg.status) + target_status_list = msg.status if self.run_stop: if self.speak_when_runstopped is False: rospy.logdebug('RUN STOP is pressed. Do not speak warning.') From 024f67b45c1ea7560ca0890e4759bd28e98ab80c Mon Sep 17 00:00:00 2001 From: iory Date: Fri, 3 Jun 2022 23:48:44 +0900 Subject: [PATCH 32/54] [audible_warning] Add inflection_utils for camel_to_snake case --- jsk_tools/src/audible_warning.py | 2 ++ jsk_tools/src/jsk_tools/inflection_utils.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 jsk_tools/src/jsk_tools/inflection_utils.py diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index f93186283..6d4fecefd 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -22,6 +22,7 @@ from jsk_tools.diagnostics_utils import diagnostics_level_to_str from jsk_tools.diagnostics_utils import filter_diagnostics_status_list from jsk_tools.diagnostics_utils import is_leaf +from jsk_tools.inflection_utils import camel_to_snake def expr_eval(expr): @@ -131,6 +132,7 @@ def run(self): else: prefix = 'ok.' sentence = prefix + e.name + ' ' + e.message + sentence = camel_to_snake(sentence) sentence = sentence.replace('/', ' ') sentence = sentence.replace('_', ' ') rospy.loginfo('audible warning error name "{}"'.format(e.name)) diff --git a/jsk_tools/src/jsk_tools/inflection_utils.py b/jsk_tools/src/jsk_tools/inflection_utils.py new file mode 100644 index 000000000..455869920 --- /dev/null +++ b/jsk_tools/src/jsk_tools/inflection_utils.py @@ -0,0 +1,15 @@ +import re + + +def camel_to_snake(word): + """Convert Camel case to snake case. + + Examples + -------- + >>> camel_to_snake('clusterPointIndices') + 'cluster_point_indices' + """ + word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word) + word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word) + word = word.replace("-", "_") + return word.lower() From 1ffabe7c467e9b28cbb718dbd841103ecd002804 Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 28 May 2022 20:56:20 +0900 Subject: [PATCH 33/54] [audible_warning] Add ignore after runstop time --- jsk_tools/cfg/AudibleWarning.cfg | 6 ++++++ jsk_tools/src/audible_warning.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/jsk_tools/cfg/AudibleWarning.cfg b/jsk_tools/cfg/AudibleWarning.cfg index 21e4997bc..bd1144e44 100755 --- a/jsk_tools/cfg/AudibleWarning.cfg +++ b/jsk_tools/cfg/AudibleWarning.cfg @@ -15,5 +15,11 @@ speak_group.add("speak_error", bool_t, 0, "Speak error level diagnostics", True) gen.add("volume", double_t, 0, "Volume of sound.", 1.0, 0.0, 1.0) gen.add("speak_interval", double_t, 0, "Volume of sound.", 120.0, 0.0, 3600.0) +gen.add("ignore_time_after_runstop_is_enabled", double_t, 0, + "Time to ignore diagnostics after runstop is enabled.", + 0.0, 0.0, 3600.0) +gen.add("ignore_time_after_runstop_is_disabled", double_t, 0, + "Time to ignore diagnostics after runstop is disabled.", + 0.0, 0.0, 3600.0) exit(gen.generate(PACKAGE, PACKAGE, "AudibleWarning")) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 6d4fecefd..456347362 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -163,6 +163,9 @@ def __init__(self): '~seconds_to_start_speaking', 0) # Wait until seconds_to_start_speaking the time has passed. + self.run_stop_enabled_time = None + self.run_stop_disabled_time = None + self.run_stop_time = None rate = rospy.Rate(10) start_time = rospy.Time.now() while not rospy.is_shutdown() \ @@ -241,6 +244,10 @@ def config_callback(self, config, level): self.speak_thread.set_speak_flag(config.enable) self.speak_thread.set_volume(config.volume) self.speak_thread.set_speak_interval(config.speak_interval) + self.ignore_time_after_runstop_is_enabled = \ + config.ignore_time_after_runstop_is_enabled + self.ignore_time_after_runstop_is_disabled = \ + config.ignore_time_after_runstop_is_disabled return config def run_stop_callback(self, msg): @@ -253,8 +260,15 @@ def run_stop_callback(self, msg): self.run_stop_topic, msg_class, self.run_stop_callback) self.run_stop_sub = deserialized_sub return - self.run_stop = self.run_stop_condition( - self.run_stop_topic, msg, rospy.Time.now()) + tm = rospy.Time.now() + run_stop = self.run_stop_condition( + self.run_stop_topic, msg, tm) + if run_stop != self.run_stop: + if run_stop is True: + self.run_stop_enabled_time = tm + else: + self.run_stop_disabled_time = tm + self.run_stop = run_stop def on_shutdown(self): self.speak_thread.stop() @@ -262,6 +276,16 @@ def on_shutdown(self): def diag_cb(self, msg): target_status_list = msg.status + + if self.ignore_time_after_runstop_is_enabled > 0.0: + if ((rospy.Time.now() - self.run_stop_enabled_time).to_sec < + self.ignore_time_after_runstop_is_enabled): + return + if self.ignore_time_after_runstop_is_disabled > 0.0: + if ((rospy.Time.now() - self.run_stop_disabled_time).to_sec < + self.ignore_time_after_runstop_is_disabled): + return + if self.run_stop: if self.speak_when_runstopped is False: rospy.logdebug('RUN STOP is pressed. Do not speak warning.') From b16778a1380895b37a272461af7cbd4a0f91a14d Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 4 Jun 2022 02:16:44 +0900 Subject: [PATCH 34/54] [audible_warning] Ignore if self.run_stop_(disabled|enabled)_time is None --- jsk_tools/src/audible_warning.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/src/audible_warning.py index 456347362..787dfc6df 100755 --- a/jsk_tools/src/audible_warning.py +++ b/jsk_tools/src/audible_warning.py @@ -165,7 +165,6 @@ def __init__(self): # Wait until seconds_to_start_speaking the time has passed. self.run_stop_enabled_time = None self.run_stop_disabled_time = None - self.run_stop_time = None rate = rospy.Rate(10) start_time = rospy.Time.now() while not rospy.is_shutdown() \ @@ -278,12 +277,16 @@ def diag_cb(self, msg): target_status_list = msg.status if self.ignore_time_after_runstop_is_enabled > 0.0: - if ((rospy.Time.now() - self.run_stop_enabled_time).to_sec < - self.ignore_time_after_runstop_is_enabled): + if self.run_stop_enabled_time is not None \ + and ((rospy.Time.now() + - self.run_stop_enabled_time).to_sec < + self.ignore_time_after_runstop_is_enabled): return if self.ignore_time_after_runstop_is_disabled > 0.0: - if ((rospy.Time.now() - self.run_stop_disabled_time).to_sec < - self.ignore_time_after_runstop_is_disabled): + if self.run_stop_disabled_time is not None \ + and ((rospy.Time.now() + - self.run_stop_disabled_time).to_sec < + self.ignore_time_after_runstop_is_disabled): return if self.run_stop: From b42daedee71e26c08cd25e504ee22efb0449f7b5 Mon Sep 17 00:00:00 2001 From: iory Date: Sat, 4 Jun 2022 02:20:15 +0900 Subject: [PATCH 35/54] [audible_warning] Add docs for dynamic reconfigure's parameters --- doc/jsk_tools/scripts/audible_warning.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/jsk_tools/scripts/audible_warning.md b/doc/jsk_tools/scripts/audible_warning.md index 61aadee5c..75888827f 100644 --- a/doc/jsk_tools/scripts/audible_warning.md +++ b/doc/jsk_tools/scripts/audible_warning.md @@ -49,6 +49,14 @@ Robots using diagnostics can use this node. This is useful for ignoring errors that occur when the robot starts. +* `~enable` (`Bool`, default: `True`) + + If `True`, speak diagnositcs. If `False`, this node don't speak. + +* `~speak_ok` (`Bool`, default: `False`) + + If `True`, speak ok level diagnostics. + * `~speak_warn` (`Bool`, default: `True`) If `True`, speak warning level diagnostics. @@ -81,6 +89,14 @@ Robots using diagnostics can use this node. run_stop_condition: "m.runstopped is True" ``` +* `~ignore_time_after_runstop_is_enabled` (`Float`, default: `0.0`) + + Time to ignore diagnostics after runstop is enabled. + +* `~ignore_time_after_runstop_is_disabled` (`Float`, default: `0.0`) + + Time to ignore diagnostics after runstop is disabled. + - `~blacklist` (`Yaml`, required) User must always specify `name`. You can specify `message` as an option. From 69757a457c4da33a471685390fd3ca4b7acfc874 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 7 Jun 2022 00:58:28 +0900 Subject: [PATCH 36/54] [audible_warning] Move audible_warning.py to node_scripts. --- jsk_tools/CMakeLists.txt | 14 ++------------ jsk_tools/{src => node_scripts}/audible_warning.py | 0 jsk_tools/package.xml | 2 -- 3 files changed, 2 insertions(+), 14 deletions(-) rename jsk_tools/{src => node_scripts}/audible_warning.py (100%) diff --git a/jsk_tools/CMakeLists.txt b/jsk_tools/CMakeLists.txt index 1140b01fa..c0b5db08e 100644 --- a/jsk_tools/CMakeLists.txt +++ b/jsk_tools/CMakeLists.txt @@ -3,7 +3,6 @@ project(jsk_tools) find_package(catkin REQUIRED COMPONENTS - message_generation dynamic_reconfigure) catkin_python_setup() set(ROS_BUILD_TYPE RelWithDebInfo) @@ -13,7 +12,7 @@ generate_dynamic_reconfigure_options( ) catkin_package( - CATKIN_DEPENDS message_runtime # + CATKIN_DEPENDS dynamic_reconfigure # LIBRARIES # INCLUDE_DIRS # DEPENDS # @@ -33,15 +32,6 @@ foreach(exec ${BIN_EXECUTABLES}) install(PROGRAMS bin/${exec} DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}) endforeach(exec) -file(GLOB SCRIPT_PROGRAMS src/*.py) -catkin_install_python( - PROGRAMS ${SCRIPT_PROGRAMS} - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/scripts/) -file(GLOB SCRIPT_BIN_PROGRAMS bin/*.py) -catkin_install_python( - PROGRAMS ${SCRIPT_BIN_PROGRAMS} - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/bin/) - if (CATKIN_ENABLE_TESTING) find_package(roslint REQUIRED) roslint_python(src/audible_warning.py) @@ -93,7 +83,7 @@ install(DIRECTORY test USE_SOURCE_PERMISSIONS PATTERN "*.test" EXCLUDE ) -install(DIRECTORY src dot-files cmake launch sample +install(DIRECTORY src dot-files cmake launch sample node_scripts DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} USE_SOURCE_PERMISSIONS ) diff --git a/jsk_tools/src/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py similarity index 100% rename from jsk_tools/src/audible_warning.py rename to jsk_tools/node_scripts/audible_warning.py diff --git a/jsk_tools/package.xml b/jsk_tools/package.xml index afeeb7a2d..082dc1b2b 100644 --- a/jsk_tools/package.xml +++ b/jsk_tools/package.xml @@ -17,14 +17,12 @@ dynamic_reconfigure git - message_generation rosgraph_msgs cv_bridge diagnostic_aggregator diagnostic_msgs diagnostic_updater dynamic_reconfigure - message_runtime python-percol python-colorama python3-colorama From 78f3928e73d84d26e120993a1904fefe6d2e4898 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 7 Jun 2022 01:09:14 +0900 Subject: [PATCH 37/54] [audible_warning] Fixed lint test target path --- jsk_tools/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsk_tools/CMakeLists.txt b/jsk_tools/CMakeLists.txt index c0b5db08e..451998060 100644 --- a/jsk_tools/CMakeLists.txt +++ b/jsk_tools/CMakeLists.txt @@ -34,7 +34,7 @@ endforeach(exec) if (CATKIN_ENABLE_TESTING) find_package(roslint REQUIRED) - roslint_python(src/audible_warning.py) + roslint_python(node_scripts/audible_warning.py) roslint_python(src/jsk_tools/cltool.py) roslint_python(src/test_topic_published.py) roslint_python(src/test_rosparam_set.py) From 8e119f61ac5153df53e0d3da7d587efd243f1940 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 7 Jun 2022 01:09:26 +0900 Subject: [PATCH 38/54] [audible_warning] Fixed lint --- jsk_tools/node_scripts/audible_warning.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 787dfc6df..49b604fd3 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -278,14 +278,14 @@ def diag_cb(self, msg): if self.ignore_time_after_runstop_is_enabled > 0.0: if self.run_stop_enabled_time is not None \ - and ((rospy.Time.now() - - self.run_stop_enabled_time).to_sec < + and ((rospy.Time.now() - + self.run_stop_enabled_time).to_sec < self.ignore_time_after_runstop_is_enabled): return if self.ignore_time_after_runstop_is_disabled > 0.0: if self.run_stop_disabled_time is not None \ - and ((rospy.Time.now() - - self.run_stop_disabled_time).to_sec < + and ((rospy.Time.now() - + self.run_stop_disabled_time).to_sec < self.ignore_time_after_runstop_is_disabled): return From 1b10cb3040213d36fc26639a5ac8164a310c3da4 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 7 Jun 2022 13:14:18 +0900 Subject: [PATCH 39/54] [audible_warning] Convert multiple space to one --- jsk_tools/node_scripts/audible_warning.py | 2 ++ jsk_tools/src/jsk_tools/string_utils.py | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 jsk_tools/src/jsk_tools/string_utils.py diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 49b604fd3..eefe464a8 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -23,6 +23,7 @@ from jsk_tools.diagnostics_utils import filter_diagnostics_status_list from jsk_tools.diagnostics_utils import is_leaf from jsk_tools.inflection_utils import camel_to_snake +from jsk_tools.string_utils import multiple_whitespace_to_one def expr_eval(expr): @@ -135,6 +136,7 @@ def run(self): sentence = camel_to_snake(sentence) sentence = sentence.replace('/', ' ') sentence = sentence.replace('_', ' ') + sentence = multiple_whitespace_to_one(sentence) rospy.loginfo('audible warning error name "{}"'.format(e.name)) rospy.loginfo("audible warning talking: %s" % sentence) diff --git a/jsk_tools/src/jsk_tools/string_utils.py b/jsk_tools/src/jsk_tools/string_utils.py new file mode 100644 index 000000000..fe9459ce5 --- /dev/null +++ b/jsk_tools/src/jsk_tools/string_utils.py @@ -0,0 +1,2 @@ +def multiple_whitespace_to_one(text): + return ' '.join(text.split()) From a044fcd1826cbdb79c2dd34d6ec41befcf2558a3 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 7 Jun 2022 13:14:35 +0900 Subject: [PATCH 40/54] [audible_warning] Convert colon symbol to english text --- jsk_tools/node_scripts/audible_warning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index eefe464a8..da2bb8444 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -136,6 +136,7 @@ def run(self): sentence = camel_to_snake(sentence) sentence = sentence.replace('/', ' ') sentence = sentence.replace('_', ' ') + sentence = sentence.replace(':', ' colon ') sentence = multiple_whitespace_to_one(sentence) rospy.loginfo('audible warning error name "{}"'.format(e.name)) rospy.loginfo("audible warning talking: %s" % sentence) From cf21b2ce42eead490e9f8d29b87b9ca98d279826 Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 7 Jun 2022 13:21:30 +0900 Subject: [PATCH 41/54] [audible_warning] Add publishing text --- jsk_tools/node_scripts/audible_warning.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index da2bb8444..769ee5b9a 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -13,6 +13,7 @@ from diagnostic_msgs.msg import DiagnosticArray from diagnostic_msgs.msg import DiagnosticStatus from dynamic_reconfigure.server import Server +import std_msgs.msg import rospy from sound_play.msg import SoundRequest from sound_play.msg import SoundRequestAction @@ -56,6 +57,13 @@ def __init__(self, rate=1.0, wait=True, self.speak_flag = True self.language = language + self.pub_original_text = rospy.Publisher('~output/original_text', + std_msgs.msg.String, + queue_size=1) + self.pub_speak_text = rospy.Publisher('~output/text', + std_msgs.msg.String, + queue_size=1) + self.talk = actionlib.SimpleActionClient( "/robotsound", SoundRequestAction) self.talk.wait_for_server() @@ -140,6 +148,9 @@ def run(self): sentence = multiple_whitespace_to_one(sentence) rospy.loginfo('audible warning error name "{}"'.format(e.name)) rospy.loginfo("audible warning talking: %s" % sentence) + self.pub_original_text.publish(e.message) + self.pub_speak_text.publish( + "audible warning talking: %s" % sentence) goal = SoundRequestGoal() goal.sound_request.sound = SoundRequest.SAY From 42332db0c942f4736bc7255b4686638529a587bf Mon Sep 17 00:00:00 2001 From: iory Date: Wed, 8 Jun 2022 18:53:30 +0900 Subject: [PATCH 42/54] [audible_warning] Add AudibleWarningClient for reconfigure --- jsk_tools/src/jsk_tools/clients/__init__.py | 1 + .../clients/audible_warninig_client.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 jsk_tools/src/jsk_tools/clients/__init__.py create mode 100644 jsk_tools/src/jsk_tools/clients/audible_warninig_client.py diff --git a/jsk_tools/src/jsk_tools/clients/__init__.py b/jsk_tools/src/jsk_tools/clients/__init__.py new file mode 100644 index 000000000..91a96e71c --- /dev/null +++ b/jsk_tools/src/jsk_tools/clients/__init__.py @@ -0,0 +1 @@ +from .audible_warninig_client import AudibleWarningReconfigureClient diff --git a/jsk_tools/src/jsk_tools/clients/audible_warninig_client.py b/jsk_tools/src/jsk_tools/clients/audible_warninig_client.py new file mode 100644 index 000000000..0bc575505 --- /dev/null +++ b/jsk_tools/src/jsk_tools/clients/audible_warninig_client.py @@ -0,0 +1,18 @@ +from dynamic_reconfigure.client import Client + + +class AudibleWarningReconfigureClient(object): + + def __init__(self, client_name='audible_warning'): + super(AudibleWarningReconfigureClient).__init__() + self.client_name = client_name + self.client = Client(client_name, timeout=3) + + def reconfigure(self, **kwargs): + return self.client.update_configuration(kwargs) + + def enable(self): + return self.client.update_configuration({'enable': True}) + + def disable(self): + return self.client.update_configuration({'enable': False}) From 6d3d940297471e0aeb02124a5e5aca47526bb4db Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 9 Jun 2022 01:35:47 +0900 Subject: [PATCH 43/54] [audible_warning] Make speak_when_runstopped dynamic params --- jsk_tools/cfg/AudibleWarning.cfg | 1 + jsk_tools/node_scripts/audible_warning.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jsk_tools/cfg/AudibleWarning.cfg b/jsk_tools/cfg/AudibleWarning.cfg index bd1144e44..705682213 100755 --- a/jsk_tools/cfg/AudibleWarning.cfg +++ b/jsk_tools/cfg/AudibleWarning.cfg @@ -12,6 +12,7 @@ speak_group.add("speak_ok", bool_t, 0, "Speak ok level diagnostics", False) speak_group.add("speak_stale", bool_t, 0, "Speak stale level diagnostics", True) speak_group.add("speak_warn", bool_t, 0, "Speak warn level diagnostics", True) speak_group.add("speak_error", bool_t, 0, "Speak error level diagnostics", True) +speak_group.add("speak_when_runstopped", bool_t, 0, "Speak when runstop is enabled", True) gen.add("volume", double_t, 0, "Volume of sound.", 1.0, 0.0, 1.0) gen.add("speak_interval", double_t, 0, "Volume of sound.", 120.0, 0.0, 3600.0) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 769ee5b9a..9d33a910a 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -207,8 +207,6 @@ def __init__(self): self.srv = Server(Config, self.config_callback) # run-stop - self.speak_when_runstopped = rospy.get_param( - '~speak_when_runstopped', True) self.run_stop = False self.run_stop_topic = rospy.get_param('~run_stop_topic', None) if self.run_stop_topic: @@ -261,6 +259,7 @@ def config_callback(self, config, level): config.ignore_time_after_runstop_is_enabled self.ignore_time_after_runstop_is_disabled = \ config.ignore_time_after_runstop_is_disabled + self.speak_when_runstopped = config.speak_when_runstopped return config def run_stop_callback(self, msg): From d38121c756b9614eb0560bfc2792cb8b066a50f4 Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 9 Jun 2022 01:36:37 +0900 Subject: [PATCH 44/54] [audible_warning] Don't add heap at first --- jsk_tools/node_scripts/audible_warning.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 9d33a910a..a58f4eeca 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -104,6 +104,12 @@ def set_speak_interval(self, interval): def add(self, status_list): with self.lock: for status in status_list: + if is_leaf(status.name) is False: + continue + if rospy.Time.now().to_sec() \ + - self.previous_spoken_time[status.name] \ + < self.speak_interval: + continue heapq.heappush( self.status_list, (rospy.Time.now().to_sec(), status)) From d0c4d4eeb9588f60cba9ed15925e1f3474226e6d Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 9 Jun 2022 01:36:59 +0900 Subject: [PATCH 45/54] [audible_warning] Deserialize image --- jsk_tools/node_scripts/audible_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index a58f4eeca..7d2a34412 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -277,7 +277,7 @@ def run_stop_callback(self, msg): deserialized_sub = rospy.Subscriber( self.run_stop_topic, msg_class, self.run_stop_callback) self.run_stop_sub = deserialized_sub - return + msg = msg_class().deserialize(msg._buff) tm = rospy.Time.now() run_stop = self.run_stop_condition( self.run_stop_topic, msg, tm) From 299832443e4f9760ceb054e42a97aef7fca87389 Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 9 Jun 2022 01:37:34 +0900 Subject: [PATCH 46/54] [audible_warning] Fixed typo by adding () --- jsk_tools/node_scripts/audible_warning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 7d2a34412..5b23575ee 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -298,13 +298,13 @@ def diag_cb(self, msg): if self.ignore_time_after_runstop_is_enabled > 0.0: if self.run_stop_enabled_time is not None \ and ((rospy.Time.now() - - self.run_stop_enabled_time).to_sec < + self.run_stop_enabled_time).to_sec() < self.ignore_time_after_runstop_is_enabled): return if self.ignore_time_after_runstop_is_disabled > 0.0: if self.run_stop_disabled_time is not None \ and ((rospy.Time.now() - - self.run_stop_disabled_time).to_sec < + self.run_stop_disabled_time).to_sec() < self.ignore_time_after_runstop_is_disabled): return From 890dbc29ebc4db2b7bca433449e1f63905a01ca2 Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 9 Jun 2022 01:38:42 +0900 Subject: [PATCH 47/54] [audible_warning] Add loginfo to display runstop state --- jsk_tools/node_scripts/audible_warning.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 5b23575ee..ec048a451 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -283,9 +283,11 @@ def run_stop_callback(self, msg): self.run_stop_topic, msg, tm) if run_stop != self.run_stop: if run_stop is True: - self.run_stop_enabled_time = tm + self.run_stop_enabled_time = rospy.Time.now() + rospy.loginfo('Audible Warning: Runstop is enabled.') else: - self.run_stop_disabled_time = tm + self.run_stop_disabled_time = rospy.Time.now() + rospy.loginfo('Audible Warning: Runstop is disabled.') self.run_stop = run_stop def on_shutdown(self): From 99c6aa941317e3e4e4040ef7249d36fbf036b473 Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 9 Jun 2022 01:39:20 +0900 Subject: [PATCH 48/54] [audible_warning] Add pseudo runstop publisher --- jsk_tools/sample/pseudo_runstop_publisher.py | 41 +++++++++++++++++++ .../sample/sample_audible_warning.launch | 14 ++++++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100755 jsk_tools/sample/pseudo_runstop_publisher.py diff --git a/jsk_tools/sample/pseudo_runstop_publisher.py b/jsk_tools/sample/pseudo_runstop_publisher.py new file mode 100755 index 000000000..7091fc95f --- /dev/null +++ b/jsk_tools/sample/pseudo_runstop_publisher.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +from distutils.version import LooseVersion as version + +import pkg_resources +import rospy +import std_msgs.msg + + +class PseudoRunstopPublisher(object): + + def __init__(self): + duration_time = rospy.get_param('~duration_time', 30) + self.runstop = True + self.pub = rospy.Publisher('~runstop', std_msgs.msg.Bool, + queue_size=1) + kwargs = dict( + period=rospy.Duration(duration_time), + callback=self.publish_callback, + oneshot=False, + ) + if version(pkg_resources.get_distribution('rospy').version) \ + >= version('1.12.0'): + # on >=kinetic, it raises ROSTimeMovedBackwardsException + # when we use rosbag play --loop. + kwargs['reset'] = True + self.timer = rospy.Timer(**kwargs) + + def publish_callback(self, event): + if self.runstop is True: + rospy.loginfo('Runstop is enabled.') + else: + rospy.loginfo('Runstop is disabled.') + self.pub.publish(self.runstop) + self.runstop = not self.runstop + + +if __name__ == '__main__': + rospy.init_node('pseudo_runstop_publisher') + pub = PseudoRunstopPublisher() + rospy.spin() diff --git a/jsk_tools/sample/sample_audible_warning.launch b/jsk_tools/sample/sample_audible_warning.launch index 236c97e14..f9fb14ed0 100644 --- a/jsk_tools/sample/sample_audible_warning.launch +++ b/jsk_tools/sample/sample_audible_warning.launch @@ -9,6 +9,14 @@ output="screen"> + + + duration_time: 30 + + + @@ -27,8 +35,12 @@ output="screen" > - speak_interval: 0 + run_stop_topic: /pseudo_runstop_publisher/runstop + speak_when_runstopped: true + speak_interval: 0.0 seconds_to_start_speaking: 5 + ignore_time_after_runstop_is_enabled: 10.0 + ignore_time_after_runstop_is_disabled: 10.0 From 5217806db0f38a778ccc58982c26a4546a06da6d Mon Sep 17 00:00:00 2001 From: iory Date: Tue, 14 Jun 2022 16:02:49 +0900 Subject: [PATCH 49/54] [audible_warning] Add prefix + error name to original text --- jsk_tools/node_scripts/audible_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index ec048a451..7f1659cbe 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -154,7 +154,7 @@ def run(self): sentence = multiple_whitespace_to_one(sentence) rospy.loginfo('audible warning error name "{}"'.format(e.name)) rospy.loginfo("audible warning talking: %s" % sentence) - self.pub_original_text.publish(e.message) + self.pub_original_text.publish(prefix + e.name + ' ' + e.message) self.pub_speak_text.publish( "audible warning talking: %s" % sentence) From 22c96e10028a12710e114a696f065ed74704aea8 Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 7 Jul 2022 02:05:18 +0900 Subject: [PATCH 50/54] [jsk_tools/audible_warning] Modified default speak rate to speed up. --- doc/jsk_tools/scripts/audible_warning.md | 4 ++-- jsk_tools/node_scripts/audible_warning.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/jsk_tools/scripts/audible_warning.md b/doc/jsk_tools/scripts/audible_warning.md index 75888827f..72849c0d8 100644 --- a/doc/jsk_tools/scripts/audible_warning.md +++ b/doc/jsk_tools/scripts/audible_warning.md @@ -23,9 +23,9 @@ Robots using diagnostics can use this node. ## Parameter -* `~speak_rate` (`Float`, default: `1.0`) +* `~speak_rate` (`Float`, default: `1.0 / 100.0`) - Rate of speak loop. + Rate of speak loop. If `~wait_speak` is `True`, wait until a robot finish speaking. * `~speak_interval` (`Float`, default: `120.0`) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 7f1659cbe..6da9ef6b5 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -176,7 +176,7 @@ def run(self): class AudibleWarning(object): def __init__(self): - speak_rate = rospy.get_param("~speak_rate", 1.0) + speak_rate = rospy.get_param("~speak_rate", 1.0 / 100.0) wait_speak = rospy.get_param("~wait_speak", True) language = rospy.get_param('~language', '') seconds_to_start_speaking = rospy.get_param( From 688ea1e24db3aa5ccc08be0cabfe0d02d6d2df10 Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 7 Jul 2022 02:49:32 +0900 Subject: [PATCH 51/54] [jsk_tools/audible_warning] Add space to published original text. --- jsk_tools/node_scripts/audible_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 6da9ef6b5..4f9cb98dd 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -154,7 +154,7 @@ def run(self): sentence = multiple_whitespace_to_one(sentence) rospy.loginfo('audible warning error name "{}"'.format(e.name)) rospy.loginfo("audible warning talking: %s" % sentence) - self.pub_original_text.publish(prefix + e.name + ' ' + e.message) + self.pub_original_text.publish(prefix + ' ' + e.name + ' ' + e.message) self.pub_speak_text.publish( "audible warning talking: %s" % sentence) From 22972c1356df1f0a8efcbeb2ed9b97534fa8908c Mon Sep 17 00:00:00 2001 From: iory Date: Thu, 7 Jul 2022 03:04:28 +0900 Subject: [PATCH 52/54] [jsk_tools/audible_warning] Set wait_speak_duration_time to wait action server result. --- doc/jsk_tools/scripts/audible_warning.md | 4 ++++ jsk_tools/node_scripts/audible_warning.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/jsk_tools/scripts/audible_warning.md b/doc/jsk_tools/scripts/audible_warning.md index 72849c0d8..95fb4d3a9 100644 --- a/doc/jsk_tools/scripts/audible_warning.md +++ b/doc/jsk_tools/scripts/audible_warning.md @@ -49,6 +49,10 @@ Robots using diagnostics can use this node. This is useful for ignoring errors that occur when the robot starts. +* `~wait_speak_duration_time` (`Float`, default: `30.0`) + + Waiting time in `robotsound` action. + * `~enable` (`Bool`, default: `True`) If `True`, speak diagnositcs. If `False`, this node don't speak. diff --git a/jsk_tools/node_scripts/audible_warning.py b/jsk_tools/node_scripts/audible_warning.py index 4f9cb98dd..f1bd39845 100755 --- a/jsk_tools/node_scripts/audible_warning.py +++ b/jsk_tools/node_scripts/audible_warning.py @@ -181,6 +181,8 @@ def __init__(self): language = rospy.get_param('~language', '') seconds_to_start_speaking = rospy.get_param( '~seconds_to_start_speaking', 0) + wait_speak_duration_time = rospy.get_param( + '~wait_speak_duration_time', 30.0) # Wait until seconds_to_start_speaking the time has passed. self.run_stop_enabled_time = None @@ -209,7 +211,7 @@ def __init__(self): self.speak_thread = SpeakThread( speak_rate, wait_speak, language, - wait_speak_duration_time=seconds_to_start_speaking) + wait_speak_duration_time=wait_speak_duration_time) self.srv = Server(Config, self.config_callback) # run-stop From c17036d9cc3150f8a50e06414a9e308c0f1cac85 Mon Sep 17 00:00:00 2001 From: Shingo Kitagawa Date: Fri, 30 Sep 2022 23:14:26 +0900 Subject: [PATCH 53/54] catch StopIteration when all images do not have stamp --- .../python/jsk_rosbag_tools/bag_to_video.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/jsk_rosbag_tools/python/jsk_rosbag_tools/bag_to_video.py b/jsk_rosbag_tools/python/jsk_rosbag_tools/bag_to_video.py index 0dcc3851a..c8157cc77 100644 --- a/jsk_rosbag_tools/python/jsk_rosbag_tools/bag_to_video.py +++ b/jsk_rosbag_tools/python/jsk_rosbag_tools/bag_to_video.py @@ -110,10 +110,15 @@ def bag_to_video(input_bagfile, # remove 0 time stamp stamp = 0.0 - while stamp == 0.0: - stamp, _, img, _ = next(images) - if show_progress_bar: - progress.update(1) + try: + while stamp == 0.0: + stamp, _, img, _ = next(images) + if show_progress_bar: + progress.update(1) + except StopIteration: + print('[bag_to_video] No timestamp found in all images') + print('[bag_to_video] Skipping {}'.format(image_topic)) + break start_stamp = stamp width, height = img.shape[1], img.shape[0] From acd431e588439105efffadb2539290fb0daad661 Mon Sep 17 00:00:00 2001 From: Shingo Kitagawa Date: Wed, 22 Mar 2023 01:18:31 +0900 Subject: [PATCH 54/54] auto lambda for c++14 --- .../jsk_topic_tools/connection_based_nodelet.h | 16 ++++++++-------- .../src/constant_rate_throttle_nodelet.cpp | 4 ++-- jsk_topic_tools/src/diagnostic_nodelet.cpp | 2 +- jsk_topic_tools/src/hz_measure_nodelet.cpp | 2 +- .../src/lightweight_throttle_nodelet.cpp | 4 ++-- jsk_topic_tools/src/mux_nodelet.cpp | 2 +- jsk_topic_tools/src/relay_nodelet.cpp | 2 +- jsk_topic_tools/src/stealth_relay_nodelet.cpp | 2 +- .../src/synchronized_throttle_nodelet.cpp | 6 +++--- .../src/timered_diagnostic_updater.cpp | 2 +- jsk_topic_tools/src/topic_buffer_client.cpp | 2 +- jsk_topic_tools/src/topic_buffer_server.cpp | 2 +- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/jsk_topic_tools/include/jsk_topic_tools/connection_based_nodelet.h b/jsk_topic_tools/include/jsk_topic_tools/connection_based_nodelet.h index 075ac3897..643b56cb5 100644 --- a/jsk_topic_tools/include/jsk_topic_tools/connection_based_nodelet.h +++ b/jsk_topic_tools/include/jsk_topic_tools/connection_based_nodelet.h @@ -192,13 +192,13 @@ namespace jsk_topic_tools { boost::mutex::scoped_lock lock(connection_mutex_); ros::SubscriberStatusCallback connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::connectionCallback, this, _1); #else = [this](auto& pub){ connectionCallback(pub); }; #endif ros::SubscriberStatusCallback disconnect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::connectionCallback, this, _1); #else = [this](auto& pub){ connectionCallback(pub); }; @@ -262,14 +262,14 @@ namespace jsk_topic_tools { boost::mutex::scoped_lock lock(connection_mutex_); image_transport::SubscriberStatusCallback connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::imageConnectionCallback, this, _1); #else = [this](auto& pub){ imageConnectionCallback(pub); }; #endif image_transport::SubscriberStatusCallback disconnect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::imageConnectionCallback, this, _1); #else @@ -334,28 +334,28 @@ namespace jsk_topic_tools { boost::mutex::scoped_lock lock(connection_mutex_); image_transport::SubscriberStatusCallback connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::cameraConnectionCallback, this, _1); #else = [this](auto& pub){ cameraConnectionCallback(pub); }; #endif image_transport::SubscriberStatusCallback disconnect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::cameraConnectionCallback, this, _1); #else = [this](auto& pub){ cameraConnectionCallback(pub); }; #endif ros::SubscriberStatusCallback info_connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::cameraInfoConnectionCallback, this, _1); #else = [this](auto& pub){ cameraInfoConnectionCallback(pub); }; #endif ros::SubscriberStatusCallback info_disconnect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConnectionBasedNodelet::cameraInfoConnectionCallback, this, _1); #else diff --git a/jsk_topic_tools/src/constant_rate_throttle_nodelet.cpp b/jsk_topic_tools/src/constant_rate_throttle_nodelet.cpp index f43a4ac54..cb486fc46 100644 --- a/jsk_topic_tools/src/constant_rate_throttle_nodelet.cpp +++ b/jsk_topic_tools/src/constant_rate_throttle_nodelet.cpp @@ -12,7 +12,7 @@ namespace jsk_topic_tools srv_ = boost::make_shared >(pnh_); dynamic_reconfigure::Server::CallbackType f -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConstantRateThrottle::configCallback, this, _1, _2); #else = [this](auto& config, auto level) {configCallback(config, level); }; @@ -68,7 +68,7 @@ namespace jsk_topic_tools if (!advertised_) { sub_->shutdown(); ros::SubscriberStatusCallback connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&ConstantRateThrottle::connectionCallback, this, _1); #else = [this](auto& pub){ connectionCallback(pub); }; diff --git a/jsk_topic_tools/src/diagnostic_nodelet.cpp b/jsk_topic_tools/src/diagnostic_nodelet.cpp index 365968ec0..a5bef07b5 100644 --- a/jsk_topic_tools/src/diagnostic_nodelet.cpp +++ b/jsk_topic_tools/src/diagnostic_nodelet.cpp @@ -56,7 +56,7 @@ namespace jsk_topic_tools diagnostic_updater_->setHardwareID(getName()); diagnostic_updater_->add( getName(), -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind( &DiagnosticNodelet::updateDiagnostic, this, diff --git a/jsk_topic_tools/src/hz_measure_nodelet.cpp b/jsk_topic_tools/src/hz_measure_nodelet.cpp index f859fe9b9..af662a271 100644 --- a/jsk_topic_tools/src/hz_measure_nodelet.cpp +++ b/jsk_topic_tools/src/hz_measure_nodelet.cpp @@ -78,7 +78,7 @@ namespace jsk_topic_tools diagnostic_updater_.reset(new TimeredDiagnosticUpdater(pnh_, ros::Duration(1.0))); diagnostic_updater_->setHardwareID(getName()); -#if __cplusplus < 201100L +#if __cplusplus < 201400L diagnostic_updater_->add(getName(), boost::bind(&HzMeasure::updateDiagnostic, this, _1)); diff --git a/jsk_topic_tools/src/lightweight_throttle_nodelet.cpp b/jsk_topic_tools/src/lightweight_throttle_nodelet.cpp index dd59d9551..62cf7ee50 100644 --- a/jsk_topic_tools/src/lightweight_throttle_nodelet.cpp +++ b/jsk_topic_tools/src/lightweight_throttle_nodelet.cpp @@ -45,7 +45,7 @@ namespace jsk_topic_tools srv_ = boost::make_shared >(pnh_); dynamic_reconfigure::Server::CallbackType f = -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(&LightweightThrottle::configCallback, this, _1, _2); #else [this](auto& config, auto level){ configCallback(config, level); }; @@ -99,7 +99,7 @@ namespace jsk_topic_tools // This section should be called once sub_->shutdown(); // Shutdown before advertising topic ros::SubscriberStatusCallback connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&LightweightThrottle::connectionCallback, this, _1); #else = [this](auto& pub){ connectionCallback(pub); }; diff --git a/jsk_topic_tools/src/mux_nodelet.cpp b/jsk_topic_tools/src/mux_nodelet.cpp index fe27fcff2..9997ff40d 100644 --- a/jsk_topic_tools/src/mux_nodelet.cpp +++ b/jsk_topic_tools/src/mux_nodelet.cpp @@ -194,7 +194,7 @@ namespace jsk_topic_tools { if (!advertised_) { // first time ros::SubscriberStatusCallback connect_cb -#if __cplusplus < 201100L +#if __cplusplus < 201400L = boost::bind(&MUX::connectCb, this, _1); #else = [this](auto& pub){ connectCb(pub); }; diff --git a/jsk_topic_tools/src/relay_nodelet.cpp b/jsk_topic_tools/src/relay_nodelet.cpp index d26bd254a..c862fcddc 100644 --- a/jsk_topic_tools/src/relay_nodelet.cpp +++ b/jsk_topic_tools/src/relay_nodelet.cpp @@ -50,7 +50,7 @@ namespace jsk_topic_tools diagnostic_updater_->setHardwareID(getName()); diagnostic_updater_->add( getName() + "::Relay", -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind( &Relay::updateDiagnostic, this, _1)); #else diff --git a/jsk_topic_tools/src/stealth_relay_nodelet.cpp b/jsk_topic_tools/src/stealth_relay_nodelet.cpp index 7b309839e..2060f6352 100644 --- a/jsk_topic_tools/src/stealth_relay_nodelet.cpp +++ b/jsk_topic_tools/src/stealth_relay_nodelet.cpp @@ -69,7 +69,7 @@ namespace jsk_topic_tools srv_ = boost::make_shared >(*pnh_); dynamic_reconfigure::Server::CallbackType f = -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(&StealthRelay::configCallback, this, _1, _2); #else [this](auto& config, auto level){ configCallback(config, level); }; diff --git a/jsk_topic_tools/src/synchronized_throttle_nodelet.cpp b/jsk_topic_tools/src/synchronized_throttle_nodelet.cpp index 995a6db6b..56c053092 100644 --- a/jsk_topic_tools/src/synchronized_throttle_nodelet.cpp +++ b/jsk_topic_tools/src/synchronized_throttle_nodelet.cpp @@ -87,7 +87,7 @@ void SynchronizedThrottle::onInit() srv_ = boost::make_shared >(*pnh_); dynamic_reconfigure::Server::CallbackType f = -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(&SynchronizedThrottle::configCallback, this, _1, _2); #else [this](auto& config, auto level){ configCallback(config, level); }; @@ -114,7 +114,7 @@ void SynchronizedThrottle::onInit() { check_sub_[i] = pnh_->subscribe( input_topics_[i], 1, -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(&SynchronizedThrottle::checkCallback, this, _1, i) #else [this,i](auto& msg){ checkCallback(msg, i); } @@ -208,7 +208,7 @@ void SynchronizedThrottle::subscribe() if (n_topics < MAX_SYNC_NUM) { sub_[0]->registerCallback( -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(&SynchronizedThrottle::fillNullMessage, this, _1) #else [this](auto& msg){ fillNullMessage(msg); } diff --git a/jsk_topic_tools/src/timered_diagnostic_updater.cpp b/jsk_topic_tools/src/timered_diagnostic_updater.cpp index c3b384e9d..0bbe549d2 100644 --- a/jsk_topic_tools/src/timered_diagnostic_updater.cpp +++ b/jsk_topic_tools/src/timered_diagnostic_updater.cpp @@ -44,7 +44,7 @@ namespace jsk_topic_tools { timer_ = nh.createTimer( timer_duration, -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(&TimeredDiagnosticUpdater::timerCallback,this,_1) #else [this](auto& event){ timerCallback(event); } diff --git a/jsk_topic_tools/src/topic_buffer_client.cpp b/jsk_topic_tools/src/topic_buffer_client.cpp index 5ab8db2d3..e0d884123 100644 --- a/jsk_topic_tools/src/topic_buffer_client.cpp +++ b/jsk_topic_tools/src/topic_buffer_client.cpp @@ -201,7 +201,7 @@ int main(int argc, char **argv) pub_info->topic_with_header = false; ROS_INFO_STREAM("subscribe " << pub_info->topic_name+string("_update")); pub_info->sub = new ros::Subscriber(n.subscribe(pub_info->topic_name+string("_update"), 10, -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(in_cb, _1, pub_info) #else [pub_info](auto& msg){ in_cb(msg, pub_info); } diff --git a/jsk_topic_tools/src/topic_buffer_server.cpp b/jsk_topic_tools/src/topic_buffer_server.cpp index b79b0f2a5..be881a7d6 100644 --- a/jsk_topic_tools/src/topic_buffer_server.cpp +++ b/jsk_topic_tools/src/topic_buffer_server.cpp @@ -220,7 +220,7 @@ int main(int argc, char **argv) sub_info->periodic = false; ROS_INFO_STREAM("subscribe " << sub_info->topic_name); sub_info->sub = new ros::Subscriber(n.subscribe(sub_info->topic_name, 10, -#if __cplusplus < 201100L +#if __cplusplus < 201400L boost::bind(in_cb, _1, sub_info) #else [sub_info](auto& msg){ in_cb(msg, sub_info); }