Example #1
0
    def __init__(self, name, endpoint, drop_warn_cutoff, callback):
        """
        Initialise a Node for the Adapter. The node should subscribe to
        a ZMQ socket and pass any frames received at that socket to the main
        adapter.
        """
        logging.debug("Live View Proxy Node Initialising: %s", endpoint)
        self.name = name
        self.endpoint = endpoint
        self.callback = callback
        self.dropped_frame_count = 0
        self.received_frame_count = 0
        self.last_frame = 0
        self.current_acq = 0
        self.drop_warn_cutoff = drop_warn_cutoff
        self.drop_unwarn_cutoff = drop_warn_cutoff * 0.75
        self.has_warned = False
        # subscribe to the given socket address.
        self.channel = IpcTornadoChannel(IpcTornadoChannel.CHANNEL_TYPE_SUB,
                                         endpoint=endpoint)
        self.channel.subscribe()
        self.channel.connect()
        # callback is called whenever data 'arrives' at the socket. This is driven by the IOLoop
        self.channel.register_callback(self.local_callback)

        self.param_tree = ParameterTree({
            'endpoint': (lambda: self.endpoint, None),
            'dropped_frames': (lambda: self.dropped_frame_count, None),
            'received_frames': (lambda: self.received_frame_count, None),
            'last_frame': (lambda: self.last_frame, None),
            'current_acquisition': (lambda: self.current_acq, None),
        })

        logging.debug("Proxy Connected to Socket: %s", endpoint)
Example #2
0
    def __init__(self, parent, endpoint):
        """
        Initialise IPC channel as a subscriber, and register the callback.

        :param parent: the class that created this object, a LiveViewer, given so that this object
        can reference the method in the parent
        :param endpoint: the URI address of the socket to subscribe to
        """
        self.parent = parent
        self.endpoint = endpoint
        self.frame_count = 0
        self.channel = IpcTornadoChannel(IpcTornadoChannel.CHANNEL_TYPE_SUB, endpoint=endpoint)
        self.channel.subscribe()
        self.channel.connect()
        # register the get_image method to be called when the ZMQ socket receives a message
        self.channel.register_callback(self.callback)
    def __init__(self, ip_address, port):
        self.logger = logging.getLogger(self.__class__.__name__)

        self._ip_address = ip_address
        self._port = port

        self.ctrl_endpoint = self.ENDPOINT_TEMPLATE.format(IP=ip_address,
                                                           PORT=port)
        self.logger.debug("Connecting to client at %s", self.ctrl_endpoint)
        self.ctrl_channel = IpcTornadoChannel(
            IpcTornadoChannel.CHANNEL_TYPE_DEALER)
        self.ctrl_channel.connect(self.ctrl_endpoint)
        self.ctrl_channel.register_callback(self._callback)
        self._parameters = {}
        self.message_id = 0

        self._lock = RLock()
class SubSocket(object):
    """
    Subscriber Socket class.

    This class implements an IPC channel subcriber socker and sets up a callback function
    for receiving data from that socket that counts how many images it receives during its lifetime.
    """
    def __init__(self, parent, endpoint):
        """
        Initialise IPC channel as a subscriber, and register the callback.

        :param parent: the class that created this object, a LiveViewer, given so that this object
        can reference the method in the parent
        :param endpoint: the URI address of the socket to subscribe to
        """
        self.parent = parent
        self.endpoint = endpoint
        self.frame_count = 0
        self.channel = IpcTornadoChannel(IpcTornadoChannel.CHANNEL_TYPE_SUB,
                                         endpoint=endpoint)
        self.channel.subscribe()
        self.channel.connect()
        # register the get_image method to be called when the ZMQ socket receives a message
        self.channel.register_callback(self.callback)

    def callback(self, msg):
        """
        Handle incoming data on the socket.

        This callback method is called whenever data arrives on the IPC channel socket.
        Increments the counter, then passes the message on to the image renderer of the parent.
        :param msg: the multipart message from the IPC channel
        """
        self.frame_count += 1
        self.parent.create_image_from_socket(msg)

    def cleanup(self):
        """Cleanup channel when the server is closed. Closes the IPC channel socket correctly."""
        self.channel.close()
Example #5
0
class IpcTornadoClient(object):

    ENDPOINT_TEMPLATE = "tcp://{IP}:{PORT}"

    MESSAGE_ID_MAX = 2**32

    def __init__(self, ip_address, port):
        self.logger = logging.getLogger(self.__class__.__name__)

        self._ip_address = ip_address
        self._port = port

        self._parameters = {'status': {'connected': False}}
        self.ctrl_endpoint = self.ENDPOINT_TEMPLATE.format(IP=ip_address,
                                                           PORT=port)
        self.logger.debug("Connecting to client at %s", self.ctrl_endpoint)
        self.ctrl_channel = IpcTornadoChannel(
            IpcTornadoChannel.CHANNEL_TYPE_DEALER)
        self.ctrl_channel.connect(self.ctrl_endpoint)
        self.ctrl_channel.register_callback(self._callback)
        self.ctrl_channel.register_monitor(self._monitor_callback)
        self.message_id = 0

        self._lock = RLock()

    @property
    def parameters(self):
        return self._parameters

    def _monitor_callback(self, msg):
        # Handle the multi-part message
        self.logger.debug("Msg received from %s: %s", self.ctrl_endpoint, msg)
        if msg['event'] == IpcTornadoChannel.CONNECTED:
            self.logger.debug("  Connected...")
            self._parameters['status']['connected'] = True
        if msg['event'] == IpcTornadoChannel.DISCONNECTED:
            self.logger.debug("  Disconnected...")
            self._parameters['status']['connected'] = False

    def _callback(self, msg):
        # Handle the multi-part message
        reply = IpcMessage(from_str=msg[0])
        if 'request_configuration' in reply.get_msg_val():
            self._update_configuration(reply.attrs)
        if 'status' in reply.get_msg_val():
            self._update_status(reply.attrs)

    def _update_configuration(self, config_msg):
        params = config_msg['params']
        self._parameters['config'] = params

    def _update_status(self, status_msg):
        params = status_msg['params']
        connected = self._parameters['status']['connected']
        params['timestamp'] = status_msg['timestamp']
        self._parameters['status'] = params
        self._parameters['status']['connected'] = connected

    def connected(self):
        return self._parameters['status']['connected']

    def _send_message(self, msg):
        msg.set_msg_id(self.message_id)
        self.message_id = (self.message_id + 1) % self.MESSAGE_ID_MAX
        self.logger.debug("Sending control message [%s]:\n%s",
                          self.ctrl_endpoint, msg.encode())
        with self._lock:
            self.ctrl_channel.send(msg.encode())

    @staticmethod
    def _raise_reply_error(msg, reply):
        if reply is not None:
            raise IpcMessageException(
                "Request\n%s\nunsuccessful. Got invalid response: %s" %
                (msg, reply))
        else:
            raise IpcMessageException(
                "Request\n%s\nunsuccessful. Got no response." % msg)

    def send_request(self, value):
        msg = IpcMessage("cmd", value)
        self._send_message(msg)

    def send_configuration(self, content, target=None, valid_error=None):
        msg = IpcMessage("cmd", "configure")

        if target is not None:
            msg.set_param(target, content)
        else:
            for parameter, value in content.items():
                msg.set_param(parameter, value)

        self._send_message(msg)
Example #6
0
    def __init__(self, **kwargs):
        """
        Initialise the Adapter, using the provided configuration.
        Create the node classes for the subscriptions to multiple ZMQ sockets.
        Also create the publish socket to push frames onto.
        """
        logging.debug("Live View Proxy Adapter init called")
        super(LiveViewProxyAdapter, self).__init__(**kwargs)

        self.dest_endpoint = self.options.get(DEST_ENDPOINT_CONFIG_NAME,
                                              DEFAULT_DEST_ENDPOINT).strip()

        self.drop_warn_percent = float(
            self.options.get(DROP_WARN_CONFIG_NAME, DEFAULT_DROP_WARN_PERCENT))

        try:
            logging.debug("Connecting publish socket to endpoint: %s",
                          self.dest_endpoint)
            self.publish_channel = IpcTornadoChannel(
                IpcTornadoChannel.CHANNEL_TYPE_PUB, self.dest_endpoint)
            self.publish_channel.bind()
        except ZMQError as channel_err:
            # ZMQError raised here if the socket addr is already in use.
            logging.error("Connection Failed. Error given: %s",
                          channel_err.message)
        self.max_queue = self.options.get(QUEUE_LENGTH_CONFIG_NAME,
                                          DEFAULT_QUEUE_LENGTH)

        if SOURCE_ENDPOINTS_CONFIG_NAME in self.options:
            self.source_endpoints = []
            for target_str in self.options[SOURCE_ENDPOINTS_CONFIG_NAME].split(
                    ','):
                try:
                    # config provides the nodes as "node_name=socket_URI" pairs. Split those strings
                    (target, url) = target_str.split("=")
                    self.source_endpoints.append(
                        LiveViewProxyNode(target.strip(), url.strip(),
                                          self.drop_warn_percent,
                                          self.add_to_queue))
                except (ValueError, ZMQError):
                    logging.debug("Error parsing target list: %s", target_str)
        else:
            self.source_endpoints = [
                LiveViewProxyNode("node_1", DEFAULT_SOURCE_ENDPOINT,
                                  self.drop_warn_percent, self.add_to_queue)
            ]

        tree = {
            "target_endpoint": (lambda: self.dest_endpoint, None),
            'last_sent_frame': (lambda: self.last_sent_frame, None),
            'dropped_frames': (lambda: self.dropped_frame_count, None),
            'reset': (None, self.set_reset),
            "nodes": {}
        }
        for sub in self.source_endpoints:
            tree["nodes"][sub.name] = sub.param_tree

        self.param_tree = ParameterTree(tree)

        self.queue = PriorityQueue(self.max_queue)

        self.last_sent_frame = (0, 0)
        self.dropped_frame_count = 0

        self.get_frame_from_queue()
Example #7
0
class LiveViewProxyAdapter(ApiAdapter):
    """
    Live View Proxy Adapter Class

    Implements the live view proxy adapter for odin control
    """
    def __init__(self, **kwargs):
        """
        Initialise the Adapter, using the provided configuration.
        Create the node classes for the subscriptions to multiple ZMQ sockets.
        Also create the publish socket to push frames onto.
        """
        logging.debug("Live View Proxy Adapter init called")
        super(LiveViewProxyAdapter, self).__init__(**kwargs)

        self.dest_endpoint = self.options.get(DEST_ENDPOINT_CONFIG_NAME,
                                              DEFAULT_DEST_ENDPOINT).strip()

        self.drop_warn_percent = float(
            self.options.get(DROP_WARN_CONFIG_NAME, DEFAULT_DROP_WARN_PERCENT))

        try:
            logging.debug("Connecting publish socket to endpoint: %s",
                          self.dest_endpoint)
            self.publish_channel = IpcTornadoChannel(
                IpcTornadoChannel.CHANNEL_TYPE_PUB, self.dest_endpoint)
            self.publish_channel.bind()
        except ZMQError as channel_err:
            # ZMQError raised here if the socket addr is already in use.
            logging.error("Connection Failed. Error given: %s",
                          channel_err.message)
        self.max_queue = self.options.get(QUEUE_LENGTH_CONFIG_NAME,
                                          DEFAULT_QUEUE_LENGTH)

        if SOURCE_ENDPOINTS_CONFIG_NAME in self.options:
            self.source_endpoints = []
            for target_str in self.options[SOURCE_ENDPOINTS_CONFIG_NAME].split(
                    ','):
                try:
                    # config provides the nodes as "node_name=socket_URI" pairs. Split those strings
                    (target, url) = target_str.split("=")
                    self.source_endpoints.append(
                        LiveViewProxyNode(target.strip(), url.strip(),
                                          self.drop_warn_percent,
                                          self.add_to_queue))
                except (ValueError, ZMQError):
                    logging.debug("Error parsing target list: %s", target_str)
        else:
            self.source_endpoints = [
                LiveViewProxyNode("node_1", DEFAULT_SOURCE_ENDPOINT,
                                  self.drop_warn_percent, self.add_to_queue)
            ]

        tree = {
            "target_endpoint": (lambda: self.dest_endpoint, None),
            'last_sent_frame': (lambda: self.last_sent_frame, None),
            'dropped_frames': (lambda: self.dropped_frame_count, None),
            'reset': (None, self.set_reset),
            "nodes": {}
        }
        for sub in self.source_endpoints:
            tree["nodes"][sub.name] = sub.param_tree

        self.param_tree = ParameterTree(tree)

        self.queue = PriorityQueue(self.max_queue)

        self.last_sent_frame = (0, 0)
        self.dropped_frame_count = 0

        self.get_frame_from_queue()

    def cleanup(self):
        """
        Ensure that, on shutdown, all ZMQ sockets are closed
        so that they do not linger and potentially cause issues in the future
        """
        self.publish_channel.close()
        for node in self.source_endpoints:
            node.cleanup()

    def get_frame_from_queue(self):
        """
        Loop to pop frames off the queue and send them to the destination ZMQ socket.
        """
        frame = None
        try:
            frame = self.queue.get_nowait()
            self.last_sent_frame = (frame.acq_id, frame.num)
            self.publish_channel.send_multipart(
                [frame.get_header(), frame.data])
        except QueueEmptyException:
            # queue is empty but thats fine, no need to report
            # or there'd be far too much output
            pass
        finally:
            IOLoop.instance().call_later(0, self.get_frame_from_queue)
        return frame  # returned for testing

    @response_types('application/json', default='application/json')
    def get(self, path, request):
        """
        HTTP Get Request Handler. Return the requested data from the parameter tree
        """
        response = self.param_tree.get(path)
        content_type = 'application/json'
        status = 200
        return ApiAdapterResponse(response, content_type, status)

    @response_types('application/json', default='application/json')
    def put(self, path, request):
        """
        HTTP Put Request Handler. Return the requested data after changes were made.
        """
        try:
            data = json_decode(request.body)
            self.param_tree.set(path, data)
            response = self.param_tree.get(path)
            status_code = 200
        except ParameterTreeError as set_err:
            response = {'error': str(set_err)}
            status_code = 400
        return ApiAdapterResponse(response, status_code=status_code)

    def add_to_queue(self, frame, source):
        """
        Add the frame to the priority queue, so long as it's "new enough" (the frame number is
        not lower than the last sent frame)
        If the queue is full, the next frame should be removed and this frame added instead.
        """
        if (frame.acq_id, frame.num) < self.last_sent_frame:
            source.dropped_frame()
            return
        try:
            self.queue.put_nowait(frame)
        except QueueFullException:
            logging.debug("Queue Full, discarding frame")
            self.queue.get_nowait()
            self.queue.put_nowait(frame)
            self.dropped_frame_count += 1

    def set_reset(self, data):
        """
        Reset the statistics for a new aquisition, setting dropped frames and sent frame
        counters back to 0
        """
        # we ignore the "data" parameter, as it doesn't matter what was actually PUT to
        # the method to reset.
        self.last_sent_frame = (0, 0)
        self.dropped_frame_count = 0
        for node in self.source_endpoints:
            node.set_reset()
Example #8
0
class LiveViewProxyNode(object):
    """
    Live View Proxy. Connect to a ZMQ socket from an Odin Data Live View Plugin, saving any frames
    that arrive in a queue and passing them on to the central proxy controller when needed.
    """
    def __init__(self, name, endpoint, drop_warn_cutoff, callback):
        """
        Initialise a Node for the Adapter. The node should subscribe to
        a ZMQ socket and pass any frames received at that socket to the main
        adapter.
        """
        logging.debug("Live View Proxy Node Initialising: %s", endpoint)
        self.name = name
        self.endpoint = endpoint
        self.callback = callback
        self.dropped_frame_count = 0
        self.received_frame_count = 0
        self.last_frame = 0
        self.current_acq = 0
        self.drop_warn_cutoff = drop_warn_cutoff
        self.drop_unwarn_cutoff = drop_warn_cutoff * 0.75
        self.has_warned = False
        # subscribe to the given socket address.
        self.channel = IpcTornadoChannel(IpcTornadoChannel.CHANNEL_TYPE_SUB,
                                         endpoint=endpoint)
        self.channel.subscribe()
        self.channel.connect()
        # callback is called whenever data 'arrives' at the socket. This is driven by the IOLoop
        self.channel.register_callback(self.local_callback)

        self.param_tree = ParameterTree({
            'endpoint': (lambda: self.endpoint, None),
            'dropped_frames': (lambda: self.dropped_frame_count, None),
            'received_frames': (lambda: self.received_frame_count, None),
            'last_frame': (lambda: self.last_frame, None),
            'current_acquisition': (lambda: self.current_acq, None),
        })

        logging.debug("Proxy Connected to Socket: %s", endpoint)

    def cleanup(self):
        """
        Close the Subscriber socket for this node.
        """
        self.channel.close()

    def local_callback(self, msg):
        """
        Create an instance of the Frame class using the data from the socket.
        Then, pass the frame on to the adapter to add to the Priority Queue.
        """
        frame = Frame(msg)
        self.received_frame_count += 1
        if frame.num < self.last_frame:
            # Frames are assumed to be in order for each socket. If a frame suddenly has
            # a lower frame number, it can be assumed that a new acquisition has begun.
            logging.debug(
                "Frame number has reset, new acquisition started on Node %s",
                self.name)
            self.current_acq += 1
            self.has_warned = False

        frame.set_acq(self.current_acq)
        self.last_frame = frame.num
        self.callback(frame, self)

    def dropped_frame(self):
        """
        Increase the count of dropped frames due to frame age
        """
        self.dropped_frame_count += 1
        current_dropped_percent = float(self.dropped_frame_count) / float(
            self.received_frame_count)
        if current_dropped_percent > self.drop_warn_cutoff and not self.has_warned:
            # If the number of dropped frames reaches a certain percentage threshold of the total
            # received, warn the user, as it likely means one node is running slow and is unlikely
            # to display any frames.
            logging.warning("Node %s has dropped %d%% of frames", self.name,
                            current_dropped_percent * 100)
            self.has_warned = True
        elif current_dropped_percent < self.drop_unwarn_cutoff:
            self.has_warned = False

    def set_reset(self):
        """
        Reset frame counts for dropped and received frames for starting
        a new aqquisition
        """
        self.dropped_frame_count = 0
        self.received_frame_count = 0
        self.last_frame = 0
        self.current_acq = 0
        self.has_warned = False
class IpcTornadoClient(object):

    ENDPOINT_TEMPLATE = "tcp://{IP}:{PORT}"

    MESSAGE_ID_MAX = 2**32

    def __init__(self, ip_address, port):
        self.logger = logging.getLogger(self.__class__.__name__)

        self._ip_address = ip_address
        self._port = port

        self.ctrl_endpoint = self.ENDPOINT_TEMPLATE.format(IP=ip_address,
                                                           PORT=port)
        self.logger.debug("Connecting to client at %s", self.ctrl_endpoint)
        self.ctrl_channel = IpcTornadoChannel(
            IpcTornadoChannel.CHANNEL_TYPE_DEALER)
        self.ctrl_channel.connect(self.ctrl_endpoint)
        self.ctrl_channel.register_callback(self._callback)
        self._parameters = {}
        self.message_id = 0

        self._lock = RLock()

    @property
    def parameters(self):
        return self._parameters

    def _callback(self, msg):
        # Handle the multi-part message
        self.logger.debug("Msg received: %s", msg)
        reply = IpcMessage(from_str=msg[0])
        if 'request_configuration' in reply.get_msg_val():
            self._update_configuration(reply.attrs)
        if 'status' in reply.get_msg_val():
            self._update_status(reply.attrs)

    def _update_configuration(self, config_msg):
        params = config_msg['params']
        self._parameters['config'] = params
        logging.debug("Current configuration updated to: %s",
                      self._parameters['config'])

    def _update_status(self, status_msg):
        params = status_msg['params']
        params['connected'] = True
        params['timestamp'] = status_msg['timestamp']
        self._parameters['status'] = params
        logging.debug("Status updated to: %s", self._parameters['status'])

    def check_for_stale_status(self, max_stale_time):
        if 'status' in self._parameters:
            # Check for the timestamp
            if 'timestamp' in self._parameters['status']:
                ts = datetime.strptime(self._parameters['status']['timestamp'],
                                       "%Y-%m-%dT%H:%M:%S.%f")
                ttlm = datetime.now() - ts
                if ttlm.seconds > max_stale_time:
                    # This connection has gone stale, set connected to false
                    self._parameters['status'] = {'connected': False}
                    self._parameters['config'] = {}
                    logging.debug("Status updated to: %s",
                                  self._parameters['status'])
        else:
            # No status for this client so set connected to false
            self._parameters['status'] = {'connected': False}
            logging.debug("Status updated to: %s", self._parameters['status'])

    def _send_message(self, msg):
        msg.set_msg_id(self.message_id)
        self.message_id = (self.message_id + 1) % self.MESSAGE_ID_MAX
        self.logger.debug("Sending control message:\n%s", msg.encode())
        with self._lock:
            self.ctrl_channel.send(msg.encode())

    @staticmethod
    def _raise_reply_error(msg, reply):
        if reply is not None:
            raise IpcMessageException(
                "Request\n%s\nunsuccessful. Got invalid response: %s" %
                (msg, reply))
        else:
            raise IpcMessageException(
                "Request\n%s\nunsuccessful. Got no response." % msg)

    def send_request(self, value):
        msg = IpcMessage("cmd", value)
        self._send_message(msg)

    def send_configuration(self, content, target=None, valid_error=None):
        msg = IpcMessage("cmd", "configure")

        if target is not None:
            msg.set_param(target, content)
        else:
            for parameter, value in content.items():
                msg.set_param(parameter, value)

        self._send_message(msg)