diff --git a/package.xml b/package.xml index bc08070..ab15d08 100644 --- a/package.xml +++ b/package.xml @@ -14,7 +14,7 @@ controller_manager_msgs diagnostic_msgs ros_graph_parser - rospkg + python-rospkg diff --git a/scripts/monitor b/scripts/monitor index f8d13e5..448ac23 100755 --- a/scripts/monitor +++ b/scripts/monitor @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import importlib import time @@ -55,7 +55,7 @@ class ModuleManager(object): try: module = self._modules[name] self._observers[name] = getattr(module, name)(name) - self._observers[name].start() + self._observers[name].start(self._observers[name].perform_output) except Exception as exc: print("Could not start {}".format(name)) started = False diff --git a/src/rosgraph_monitor/observer.py b/src/rosgraph_monitor/observer.py index 9b39ccf..ef8478a 100644 --- a/src/rosgraph_monitor/observer.py +++ b/src/rosgraph_monitor/observer.py @@ -1,46 +1,38 @@ import threading -import mutex import rospy -from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus class Observer(object): def __init__(self, name, loop_rate_hz=1): self._name = name self._rate = rospy.Rate(loop_rate_hz) - self._seq = 1 self._lock = threading.Lock() self._thread = threading.Thread( target=self._run) self._thread.daemon = True self._stop_event = threading.Event() - self._pub_diag = rospy.Publisher( - '/diagnostics', DiagnosticArray, queue_size=10) - def __del__(self): if Observer: print("{} stopped".format(self._name)) # Every derived class needs to override this - def generate_diagnostics(self): - msg = DiagnosticArray() - return msg + def generate_output(): + return None def _run(self): while not rospy.is_shutdown() and not self._stopped(): - diag_msg = DiagnosticArray() - diag_msg.header.stamp = rospy.get_rostime() + msg = self.generate_output() + print(msg) - status_msgs = self.generate_diagnostics() - diag_msg.status.extend(status_msgs) - self._pub_diag.publish(diag_msg) + # perform_output() should be implemented by OutputInterface's sub-class + self._perform_output(msg) - self._seq += 1 self._rate.sleep() - def start(self): + def start(self, func): print("starting {}...".format(self._name)) + self._perform_output = func self._thread.start() def stop(self): @@ -76,48 +68,50 @@ def start_service(self): except rospy.ServiceException as exc: print("Service {} is not running: ".format(self.name) + str(exc)) - def generate_diagnostics(self): + def generate_output(self): try: resp = self.client.call() except rospy.ServiceException as exc: print("Service {} did not process request: ".format( self.name) + str(exc)) - status_msg = self.diagnostics_from_response(resp) - return status_msg + msg = self.msg_from_response(resp) + return msg # Every derived class needs to override this - def diagnostics_from_response(self, response): - msg = DiagnosticArray() - return msg + def msg_from_response(self, response): + None class TopicObserver(Observer): def __init__(self, name, loop_rate_hz, topics): - super(TopicObserver, self).__init__(name, loop_rate_hz) + super(TopicObserver, self).__init__(name=name, loop_rate_hz=loop_rate_hz) self._topics = topics self._id = "" self._num_topics = len(topics) + def is_close(self, a, b, rel_tol=1e-09, abs_tol=0.0): + return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) + # Every derived class needs to override this def calculate_attr(self, msgs): # do calculations - return DiagnosticStatus() + return None - def generate_diagnostics(self): + def generate_output(self): msgs = [] received_all = True for topic, topic_type in self._topics: try: + # Add message synchronization here -- message filter + # Consider "asynchronous" reception as well? msgs.append(rospy.wait_for_message(topic, topic_type)) except rospy.ROSException as exc: print("Topic {} is not found: ".format(topic) + str(exc)) received_all = False break - status_msgs = list() - status_msg = DiagnosticStatus() + msg = None if received_all: - status_msg = self.calculate_attr(msgs) - status_msgs.append(status_msg) + msg = self.calculate_attr(msgs) - return status_msgs + return msg diff --git a/src/rosgraph_monitor/observers/dummy_diag_observer.py b/src/rosgraph_monitor/observers/dummy_diag_observer.py new file mode 100644 index 0000000..79c5205 --- /dev/null +++ b/src/rosgraph_monitor/observers/dummy_diag_observer.py @@ -0,0 +1,52 @@ +from rosgraph_monitor.observer import TopicObserver +from rosgraph_monitor.output_interface import ObservationOutputPublisher +from std_msgs.msg import Int32, Bool +from diagnostic_msgs.msg import DiagnosticStatus, KeyValue + + +# example observer to publish result as a DiagnosticStatus message +class DummyDiagnosticObserver(TopicObserver, ObservationOutputPublisher): + def __init__(self, name): #, set_point=None): # reads from param? + topics = [("/speed", Int32), ("/accel", Int32)] # list of pairs + self._set_point = 5.5 #set_point + ObservationPublisher.__init__(self, topic_name="/diagnostics", DiagnosticStatus) + TopicObserver.__init__(self, name=name, loop_rate_hz=10, topics=topics) + + def calculate_attr(self, msgs): + status_msg = DiagnosticStatus() + + attr = msgs[0].data + msgs[1].data + is_close = True + if(self._set_point): + is_close = self.is_close(attr, self._set_point, 0.05) + print("{0} + {1}; is_close={2}".format(msgs[0].data, msgs[1].data, is_close)) + + status_msg.level = DiagnosticStatus.OK if is_close else DiagnosticStatus.ERROR + status_msg.name = self._id + status_msg.values.append( + KeyValue("enery", str(attr))) + status_msg.message = "QA status" + + return status_msg + + +# example observer to publish result as a Bool message +class DummyBoolObserver(TopicObserver, ObservationPublisher): + def __init__(self, name): #, set_point=None): # reads from param? + topics = [("/speed", Int32), ("/accel", Int32)] # list of pairs + self._set_point = 5.5 #set_point + ObservationPublisher.__init__(self, topic_name="/output_topic", Bool) + TopicObserver.__init__(self, name=name, loop_rate_hz=10, topics=topics) + + def calculate_attr(self, msgs): + status_msg = DiagnosticStatus() + + attr = msgs[0].data + msgs[1].data + is_close = False + if(self._set_point): + is_close = self.is_close(attr, self._set_point, 0.05) + print("{0} + {1}; is_close={2}".format(msgs[0].data, msgs[1].data, is_close)) + + msg = Bool() + msg.data = is_close + return msg diff --git a/src/rosgraph_monitor/observers/graph_observer.py b/src/rosgraph_monitor/observers/graph_observer.py index 5b20578..bf0040c 100644 --- a/src/rosgraph_monitor/observers/graph_observer.py +++ b/src/rosgraph_monitor/observers/graph_observer.py @@ -1,10 +1,12 @@ import imp -from rosgraph_monitor.observer import ServiceObserver -from rosgraph_monitor.parser import ModelParser -from pyparsing import * -import rospkg import os.path import re +from pyparsing import * +import rospkg +from rosgraph_monitor.observer import ServiceObserver +from rosgraph_monitor.output_interface import ObservationOutputPublisher +from rosgraph_monitor.parser import ModelParser + from ros_graph_parser.srv import GetROSModel, GetROSSystemModel from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue @@ -14,7 +16,7 @@ def strip_slash(string): return '{}'.format(string[1:] if string.startswith('/') else string) -class ROSGraphObserver(ServiceObserver): +class ROSGraphObserver(ServiceObserver, ObservationOutputPublisher): def __init__(self, name): super(ROSGraphObserver, self).__init__( name, '/get_rossystem_model', GetROSSystemModel) @@ -23,7 +25,8 @@ def __init__(self, name): self.model_path = os.path.join(rospack.get_path('rosgraph_monitor'), "resources/cob4-25.rossystem") self._rossystem_parser = ModelParser(self.model_path) - def diagnostics_from_response(self, resp): + def msg_from_response(self, resp): + diag_array = DiagnosticArray() status_msgs = list() if resp is None: return status_msgs @@ -69,7 +72,8 @@ def diagnostics_from_response(self, resp): KeyValue(params[0], str(params[1]))) status_msgs.append(status_msg) - return status_msgs + diag_msg.status.extend(status_msgs) + return diag_msg # find out missing and additional interfaces # if both lists are empty, system is running fine diff --git a/src/rosgraph_monitor/output_interface.py b/src/rosgraph_monitor/output_interface.py new file mode 100644 index 0000000..ced2340 --- /dev/null +++ b/src/rosgraph_monitor/output_interface.py @@ -0,0 +1,45 @@ +import rospy + + +class OutputInterface(object): + def __init__(self): + pass + + def perform_output(): + pass + + +class ObservationOutputPublisher(OutputInterface): + def __init__(self, topic_name, message_type): + super(ObservationPublisher, self).__init__() + + self._seq = 0; + self._publisher = rospy.Publisher( + topic_name, message_type, queue_size=10) + + def perform_output(self, msg): + try: + msg.header.stamp = rospy.get_rostime() + except Exception as exc: + pass + + self._publisher.publish(msg) + self._seq += 1 + + +class ObservationOutputLogger(OutputInterface): + def __init__(self, file_name): + pass + + def perform_output(self, msg): + # do logging + pass + + +class ObservationOutputPlotter(OutputInterface): + def __init__(self, plt_params): + pass + + def perform_output(self, msg): + # do plotting + pass diff --git a/src/rosgraph_monitor/output_interfaces/__init__.py b/src/rosgraph_monitor/output_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/rosgraph_monitor/parser.py b/src/rosgraph_monitor/parser.py index 0cb245e..514f081 100644 --- a/src/rosgraph_monitor/parser.py +++ b/src/rosgraph_monitor/parser.py @@ -87,13 +87,13 @@ def __init__(self, model, isFile=True): _action_servers = Keyword("RosActionServers").suppress() _action_server = Keyword("RosActionServer").suppress( ) | Keyword("RosServer").suppress() - _ref_server = Keyword("RefServer").suppress() + _ref_server = Keyword("RefServer").suppress() | Keyword("RefActionServer").suppress() # Actio Clients Def _action_clients = Keyword("RosActionClients").suppress() _action_client = Keyword("RosActionClient").suppress( ) | Keyword("RosClient").suppress() - _ref_action_client = Keyword("RefClient").suppress() + _ref_action_client = Keyword("RefClient").suppress() | Keyword("RefActionClient").suppress() # Topic Connections Def _topic_connections = Keyword("TopicConnections").suppress() @@ -196,8 +196,8 @@ def parse(self): self._parse_from_file() else: self._parse_from_string() - except Exception as e: - print(e.args) # Should set a default 'result'? + except ParseException as e: + print(ParseException.explain(e, depth=0)) # Should set a default 'result'? return self._result