예제 #1
0
 def restore_settings(self, do_restore, filename=None):
     if (filename):
         s = QtCore.QSettings(filename, QtCore.QSettings.IniFormat)
     else:
         s = QtCore.QSettings()
     self._init_geometry(s)
     self._init_recorder(s)
     loaded_sources = []
     try:
         if do_restore:
             loaded_sources = self._init_data_sources(s)
     except (TypeError, KeyError):
         # raise
         #raise
         # Be a bit more resilient against configuration problems
         logging.warning(
             "Failed to load data source settings! Continuing...")
     if do_restore:
         try:
             self._restore_data_windows(s, loaded_sources)
         except (TypeError, KeyError):
             # Be a bit more resilient against configuration problems
             logging.warning(
                 "Failed to load data windows settings! Continuing...")
         self.plotdata_widget.restore_state(s)
     self.settings = s
예제 #2
0
 def __init__(self, parent=None):
     QtGui.QMainWindow.__init__(self, None)
     self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
     self._enabled_sources = {}
     self.settings = QtCore.QSettings()
     self.setupUi(self)
     self.alertBlinkTimer = QtCore.QTimer()
     self.alertBlinkTimer.setInterval(500)
     self._setup_connections()
     self._parent = parent
     # If True this DataWindow was restored from saved settings
     self.restored = False
     self.alertBlinking = False
     self.set_sounds_and_volume()
예제 #3
0
 def save_settings(self, filename=None):
     if (filename):
         s = QtCore.QSettings(filename, QtCore.QSettings.IniFormat)
     else:
         s = QtCore.QSettings()
     s.setValue("geometry", self.saveGeometry())
     s.setValue("windowState", self.saveState())
     # Save data sources
     ds_settings = []
     for ds in self._data_sources:
         ds_settings.append([ds.hostname, ds.port, ds.ssh_tunnel, ds.conf])
     s.setValue("dataSources", ds_settings)
     self.plotdata_widget.save_state(s)
     s.setValue("plotData", self.plotdata_widget.save_plotdata())
     s.setValue("dataWindows", self.save_data_windows())
     # Make sure settings are saved
     s.sync()
예제 #4
0
 def _update_bg(self):
     if self._settings_diag.bg is not None:
         VB = self.plot.getViewBox()
         B = pyqtgraph.ImageItem(image=self._settings_diag.bg, autoLevels=True)
         xmin = float(self._settings_diag.bg_xmin.text())
         ymin = float(self._settings_diag.bg_xmin.text())
         width  = float(self._settings_diag.bg_xmax.text()) - xmin
         height = float(self._settings_diag.bg_ymax.text()) - ymin
         rect = QtCore.QRectF(xmin, ymin, width, height)
         B.setRect(rect)
         VB.addItem(B, ignoreBounds=True)
예제 #5
0
    def __init__(self, _type, parent=None, **kwargs):
        QtCore.QObject.__init__(self, parent, **kwargs)

        ctx = ZmqContext.instance()
        self._socket = ctx.socket(_type)

        fd = self._socket.getsockopt(FD)
        self._notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, self)
        self._notifier.activated.connect(self.activity)
        self._socket.setsockopt(RCVHWM, 100)
        self.filters = []
예제 #6
0
 def _init_timer(self):
     """Initialize reploting timer."""
     self._replot_timer = QtCore.QTimer()
     self._replot_timer.setInterval(1000)  # Replot every 1000 ms
     self._replot_timer.timeout.connect(self._replot)
     self._replot_timer.start()
예제 #7
0
 def __init__(self, parent):
     QtGui.QDialog.__init__(self, parent, QtCore.Qt.WindowTitleHint)
     self.setupUi(self)
     settings = QtCore.QSettings()
     self.outputPath.setText(settings.value("outputPath"))
예제 #8
0
class DataSource(QtCore.QObject):
    """Manages a connection with one backend"""
    plotdata_added = QtCore.Signal(PlotData)
    subscribed = QtCore.Signal(str)
    unsubscribed = QtCore.Signal(str)

    def __init__(self, parent, hostname, port, ssh_tunnel=None, conf={}):
        QtCore.QObject.__init__(self, parent)
        self._hostname = hostname
        self._port = port
        self._ssh_tunnel = ssh_tunnel
        self.connected = False
        self._plotdata = {}
        self._subscribed_titles = {}
        self._recorded_titles = {}
        self._recorder = None
        self._data_socket = ZmqSocket(SUB, self)
        self.conf = conf
        self._group_structure = {}
        try:
            self._connect()
            self.connected = True
            self._get_data_port()
            self.titles = None
            self.data_type = None
        except (RuntimeError, zmq.error.ZMQError):
            QtGui.QMessageBox.warning(self.parent(), "Connection failed!",
                                      "Could not connect to %s" % self.name())
            raise

    def subscribe(self, title, plot):
        """Subscribe to the broadcast named title, and associate it with the given plot"""
        if title not in self._subscribed_titles:
            self._subscribed_titles[title] = [plot]
            try:
                self._data_socket.subscribe(title)
                self.subscribed.emit(title)
                logging.debug("Subscribing to %s on %s.", title, self.name())
            # socket might still not exist
            except AttributeError:
                pass
        else:
            self._subscribed_titles[title].append(plot)

    def unsubscribe(self, title, plot):
        """Dissociate the given plot with the broadcast named title.
        If no one else is associated with it unsubscribe"""
        self._subscribed_titles[title].remove(plot)
        # Check if list is empty
        if not self._subscribed_titles[title]:
            self._data_socket.unsubscribe(title)
            self.unsubscribed.emit(title)
            logging.debug("Unsubscribing from %s on %s.", title, self.name())
            self._subscribed_titles.pop(title)

    def subscribe_for_recording(self, title):
        """Subscribe to the broadcast named title, and associate it with recorder"""
        # Only subscribe if we are not already subscribing for plotting
        if title in self._subscribed_titles:
            return
        if title not in self._recorded_titles:
            self._recorded_titles[title] = True
            try:
                self._data_socket.subscribe(title)
                self.subscribed.emit(title)
                logging.debug("Subscribing to %s on %s.", title, self.name())
            # socket might still not exist
            except AttributeError:
                pass

    def unsubscribe_for_recording(self, title):
        """Dissociate the recorder with the broadcast named title.
        If no one else is associated with it unsubscrine"""
        self._recorded_titles[title] = False
        if not title in self._subscribed_titles:
            self._data_socket.unsubscribe(title)
            self.unsubscribed.emit(title)
            logging.debug("Unsubscribing from %s on %s.", title, self.name())
            self._recorded_titles.pop(title)

    def name(self):
        """Return a string representation of the data source"""
        if (self._ssh_tunnel):
            return '%s (%s)' % (self._hostname, self._ssh_tunnel)
        else:
            return self._hostname

    @property
    def plotdata(self):
        """Returns the data source dictionary of plotdata"""
        return self._plotdata

    def _connect(self):
        """Connect to the configured backend"""
        self._ctrl_socket = ZmqSocket(REQ)
        addr = "tcp://%s:%d" % (self._hostname, self._port)
        self._ctrl_socket.ready_read.connect(self._get_request_reply)
        self._ctrl_socket.connect_socket(addr, self._ssh_tunnel)

    def _get_data_port(self):
        """Ask to the backend for the data port"""
        self._ctrl_socket.send_multipart(['data_port'.encode('UTF-8')])

    def query_configuration(self):
        """Ask to the backend for the configuration"""
        self._ctrl_socket.send_multipart(['conf'.encode('UTF-8')])

    def query_reloading(self):
        """Ask the backend to reload its configuration"""
        self._ctrl_socket.send_multipart(['reload'.encode('UTF-8')])

    def _get_request_reply(self, socket=None):
        """Handle the reply of the backend to a previous request"""
        if (socket is None):
            socket = self.sender()
        reply = socket.recv_json()
        if (reply[0] == 'data_port'):
            self._data_port = reply[1]
            logging.debug("Data source '%s' received data_port=%s",
                          self.name(), self._data_port)
            addr = "tcp://%s:%s" % (self._hostname, self._data_port)
            self._data_socket.ready_read.connect(self._get_broadcast)
            self._data_socket.connect_socket(addr, self._ssh_tunnel)
            self.parent().add_backend(self)
            # Subscribe to stuff already requested
            for title in self._subscribed_titles.keys():
                self._data_socket.subscribe(title)
                self.subscribed.emit(title)
                logging.debug("Subscribing to %s on %s.", title, self.name())
            self.query_configuration()
        elif (reply[0] == 'conf'):
            self.conf = reply[1]
            self.titles = self.conf.keys()
            self.data_type = {}
            for k in self.conf.keys():
                if ('data_type' not in self.conf[k]):
                    # Broadcasts without any data will not have a data_type
                    # Let's remove them from the title list and continue
                    self.titles.remove(k)
                    continue
                self.data_type[k] = self.conf[k]['data_type']
                if (k not in self._plotdata):
                    if "group" in self.conf[k]:
                        group = self.conf[k]["group"]
                        if group is None:
                            group = "No group"
                    else:
                        group = "No group"

                    self._plotdata[k] = PlotData(self, k, group=group)
                    self.plotdata_added.emit(self._plotdata[k])
                    self.add_item_to_group_structure(k, group)
            # Remove PlotData which is no longer in the conf
            for k in self._plotdata.keys():
                if k not in self.titles:
                    self._plotdata.pop(k)

    def _get_broadcast(self):
        """Receive a data package on the data socket"""
        socket = self.sender()
        socket.blockSignals(True)
        QtCore.QCoreApplication.processEvents()
        socket.blockSignals(False)

        # Discard key
        socket.recv()
        data = socket.recv_json()
        for i in range(len(data)):
            if data[i] == '__ndarray__':
                data[i] = socket.recv_array()
        self._process_broadcast(data)

    def _process_broadcast(self, payload):
        """Handle a data package received by the data socket"""
        cmd = payload[1]
        title = payload[2]
        data = payload[3]
        if (title not in self.conf):
            # We're getting data we were not expecting
            # Let's discard it and order an immediate reconfigure
            logging.debug(
                "Received unexpected data with title %s on %s. Reconfiguring...",
                title, self.name())
            return
        if (cmd == 'new_data'):
            data_x = payload[4]
            # At the moment x is always a timestamp so I'll add some metadata to show it
            if type(data_x) is not numpy.ndarray:
                data_x = numpy.array(data_x)
            data_x.dtype = numpy.dtype(data_x.dtype, metadata={'units': 's'})

            conf = payload[5]
            self.conf[title].update(conf)
            if self._plotdata[title].recordhistory:
                self._recorder.append(title, data, data_x)
            if 'sum_over' in conf:
                if 'msg' in conf:
                    self._plotdata[title].sum_over(data, data_x, conf['msg'])
                else:
                    self._plotdata[title].sum_over(data, data_x, '')
            else:
                if 'msg' in conf:
                    self._plotdata[title].append(data, data_x, conf['msg'])
                else:
                    self._plotdata[title].append(data, data_x, '')

    @property
    def hostname(self):
        """Give access to the data source hostname"""
        return self._hostname

    @property
    def port(self):
        """Give access to the data source port"""
        return self._port

    @property
    def ssh_tunnel(self):
        """Give access to the data source ssh_tunnel"""
        return self._ssh_tunnel

    @property
    def subscribed_titles(self):
        """Returns the currently subscribed titles"""
        return self._subscribed_titles.keys()

    def restore_state(self, state):
        """Restores any plotdata that are saved in the state"""
        for pds in state:
            if (pds['data_source'][0] == self.hostname
                    and pds['data_source'][1] == self.port
                    and pds['data_source'][2] == self.ssh_tunnel):
                # It's a match!
                k = pds['title']
                group = pds["group"]
                pd = PlotData(self, k, group=group)
                pd.restore_state(pds, self)
                self._plotdata[k] = pd
                self.plotdata_added.emit(self._plotdata[k])
                self.add_item_to_group_structure(k, group)

    @property
    def group_structure(self):
        return self._group_structure

    def add_item_to_group_structure(self, title, group):
        if group in self.group_structure:
            self.group_structure[group].append(title)
        else:
            self.group_structure[group] = [title]
예제 #9
0
class ImageView(QtGui.QWidget):
    """
    Widget used for display and analysis of image data.
    Implements many features:

    * Displays 2D and 3D image data. For 3D data, a z-axis
      slider is displayed allowing the user to select which frame is displayed.
    * Displays histogram of image data with movable region defining the dark/light levels
    * Editable gradient provides a color lookup table
    * Frame slider may also be moved using left/right arrow keys as well as pgup, pgdn, home, and end.
    * Basic analysis features including:

        * ROI and embedded plot for measuring image values across frames
        * Image normalization / background subtraction

    Basic Usage::

        imv = pg.ImageView()
        imv.show()
        imv.setImage(data)
    """
    sigProcessingChanged = QtCore.Signal(object)
    
    def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
        """
        By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
        and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. Custom items may be given instead 
        by specifying the *view* and/or *imageItem* arguments.
        """
        QtGui.QWidget.__init__(self, parent, *args)
        self._parent = parent
        self.levelMax = 4096
        self.levelMin = 0
        self.name = name
        self.image = None
        self.axes = {}
        self.imageDisp = None
        self.ui = Ui_Form()
        self.ui.setupUi(self)
        self.scene = self.ui.graphicsView.scene()
        
        if view is None:
            self.view = pyqtgraph.ViewBox()
        else:
            self.view = view
        self.ui.graphicsView.setCentralItem(self.view)
        self.view.setAspectLocked(True)
        self.view.invertY()
        
        if imageItem is None:
            self.imageItem = pyqtgraph.ImageItem()
        else:
            self.imageItem = imageItem
        self.view.addItem(self.imageItem)
        self.currentIndex = 0
        
        self.ui.histogram.setImageItem(self.imageItem)        

        self.keysPressed = {}
        
        ## wrap functions from view box
        for fn in ['addItem', 'removeItem']:
            setattr(self, fn, getattr(self.view, fn))

        ## wrap functions from histogram
        for fn in ['setHistogramRange', 'autoHistogramRange', 'getLookupTable', 'getLevels']:
            setattr(self, fn, getattr(self.ui.histogram, fn))

        self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
        

    def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True):
        """
        Set the image to be displayed in the widget.
        
        ================== =======================================================================
        **Arguments:**
        img                (numpy array) the image to be displayed.
        xvals              (numpy array) 1D array of z-axis values corresponding to the third axis
                           in a 3D image. For video, this array should contain the time of each frame.
        autoRange          (bool) whether to scale/pan the view to fit the image.
        autoLevels         (bool) whether to update the white/black levels to fit the image.
        levels             (min, max); the white and black level values to use.
        axes               Dictionary indicating the interpretation for each axis.
                           This is only needed to override the default guess. Format is::
                       
                               {'t':0, 'x':1, 'y':2, 'c':3};
        
        pos                Change the position of the displayed image
        scale              Change the scale of the displayed image
        transform          Set the transform of the displayed image. This option overrides *pos*
                           and *scale*.
        autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
                           image data.
        ================== =======================================================================
        """
        if hasattr(img, 'implements') and img.implements('MetaArray'):
            img = img.asarray()
        
        if not isinstance(img, numpy.ndarray):
            raise Exception("Image must be specified as ndarray.")
        self.image = img
        
        if xvals is not None:
            self.tVals = xvals
        elif hasattr(img, 'xvals'):
            try:
                self.tVals = img.xvals(0)
            except:
                self.tVals = numpy.arange(img.shape[0])
        else:
            self.tVals = numpy.arange(img.shape[0])
        
        if axes is None:
            if img.ndim == 2:
                self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None}
            elif img.ndim == 3:
                if img.shape[2] <= 4:
                    self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2}
                else:
                    self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None}
            elif img.ndim == 4:
                self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3}
            else:
                raise Exception("Can not interpret image with dimensions %s" % (str(img.shape)))
        elif isinstance(axes, dict):
            self.axes = axes.copy()
        elif isinstance(axes, list) or isinstance(axes, tuple):
            self.axes = {}
            for i in range(len(axes)):
                self.axes[axes[i]] = i
        else:
            raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes)))
            
        for x in ['t', 'x', 'y', 'c']:
            self.axes[x] = self.axes.get(x, None)
            
        self.imageDisp = None
        
        self.currentIndex = 0
        self.updateImage(autoHistogramRange=autoHistogramRange)
        if levels is None and autoLevels:
            self.autoLevels()
        if levels is not None:  ## this does nothing since getProcessedImage sets these values again.
            self.setLevels(*levels)
            
        if self.axes['t'] is not None:
            if len(self.tVals) > 1:
                start = self.tVals.min()
                stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02
            elif len(self.tVals) == 1:
                start = self.tVals[0] - 0.5
                stop = self.tVals[0] + 0.5
            else:
                start = 0
                stop = 1
            
        self.imageItem.resetTransform()
        if scale is not None:
            self.imageItem.scale(*scale)
        if pos is not None:
            self.imageItem.setPos(*pos)
        if transform is not None:
            self.imageItem.setTransform(transform)
            
        if autoRange:
            self.autoRange()

    def autoLevels(self):
        """Set the min/max intensity levels automatically to match the image data."""
        self.setLevels(self.levelMin, self.levelMax)

    def setLevels(self, min, max):
        """Set the min/max (bright and dark) levels."""
        self.ui.histogram.setLevels(min, max)

    def autoRange(self):
        """Auto scale and pan the view around the image."""
        image = self.getProcessedImage()
        self.view.autoRange()
        
    def getProcessedImage(self):
        """Returns the image data after it has been processed by any normalization options in use.
        This method also sets the attributes self.levelMin and self.levelMax 
        to indicate the range of data in the image."""
        if self.imageDisp is None:
            image = self.normalize(self.image)
            self.imageDisp = image
            self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp)))
            
        return self.imageDisp
        
        
    def close(self):
        """Closes the widget nicely, making sure to clear the graphics scene and release memory."""
        self.ui.graphicsView.close()
        self.scene.clear()
        del self.image
        del self.imageDisp
        self.setParent(None)
        
    def keyPressEvent(self, ev):
        #print ev.key()
        if ev.key() == QtCore.Qt.Key_Home:
            self.setCurrentIndex(0)
            ev.accept()
        elif ev.key() == QtCore.Qt.Key_End:
            self.setCurrentIndex(self.getProcessedImage().shape[0]-1)
            ev.accept()
        elif ev.key() in self.noRepeatKeys:
            ev.accept()
            if ev.isAutoRepeat():
                return
            self.keysPressed[ev.key()] = 1
            self.evalKeyState()
        else:
            QtGui.QWidget.keyPressEvent(self, ev)

    def keyReleaseEvent(self, ev):
        if ev.key() in [QtCore.Qt.Key_Space, QtCore.Qt.Key_Home, QtCore.Qt.Key_End]:
            ev.accept()
        elif ev.key() in self.noRepeatKeys:
            ev.accept()
            if ev.isAutoRepeat():
                return
            try:
                del self.keysPressed[ev.key()]
            except:
                self.keysPressed = {}
            self.evalKeyState()
        else:
            QtGui.QWidget.keyReleaseEvent(self, ev)


    def evalKeyState(self):
        if len(self.keysPressed) == 1:
            key = list(self.keysPressed.keys())[0]
            if key == QtCore.Qt.Key_Right:
                self.jumpFrames(1)
            elif key == QtCore.Qt.Key_Left:
                self.jumpFrames(-1)
            elif key == QtCore.Qt.Key_Up:
                self.jumpFrames(-10)
            elif key == QtCore.Qt.Key_Down:
                self.jumpFrames(10)
            elif key == QtCore.Qt.Key_PageUp:
                self.jumpFrames(-100)
            elif key == QtCore.Qt.Key_PageDown:
                self.jumpFrames(100)


    def setCurrentIndex(self, ind, autoHistogramRange=True):
        """Set the currently displayed frame index."""
        self.currentIndex = numpy.clip(ind, 0, self.getProcessedImage().shape[0]-1)
        self.updateImage(autoHistogramRange=autoHistogramRange)

    def jumpFrames(self, n):
        """Move video frame ahead n frames (may be negative)"""
        if self.axes['t'] is not None:
            self.setCurrentIndex(self.currentIndex + n)
            self._parent.replot()

    def hasTimeAxis(self):
        return 't' in self.axes and self.axes['t'] is not None

    @staticmethod
    def quickMinMax(data):
        while data.size > 1e6:
            ax = numpy.argmax(data.shape)
            sl = [slice(None)] * data.ndim
            sl[ax] = slice(None, None, 2)
            data = data[sl]
        return data.min(), data.max()

    def normalize(self, image):
        return image


    def updateImage(self, autoHistogramRange=True):
        ## Redraw image on screen
        if self.image is None:
            return

        image = self.getProcessedImage()

        if autoHistogramRange:
            self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)
        if self.axes['t'] is None:
            self.imageItem.updateImage(image)
        else:
            self.imageItem.updateImage(image[self.currentIndex])

    def getView(self):
        """Return the ViewBox (or other compatible object) which displays the ImageItem"""
        return self.view

    def getImageItem(self):
        """Return the ImageItem for this ImageView."""
        return self.imageItem

    def getHistogramWidget(self):
        """Return the HistogramLUTWidget for this ImageView"""
        return self.ui.histogram
예제 #10
0
class ZmqSocket(QtCore.QObject):
    """Wrapper around a zmq socket. Provides Qt signal handling"""
    ready_read = QtCore.Signal()
    ready_write = QtCore.Signal()
    def __init__(self, _type, parent=None, **kwargs):
        QtCore.QObject.__init__(self, parent, **kwargs)

        ctx = ZmqContext.instance()
        self._socket = ctx.socket(_type)

        fd = self._socket.getsockopt(FD)
        self._notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, self)
        self._notifier.activated.connect(self.activity)
        self._socket.setsockopt(RCVHWM, 100)
        self.filters = []

    def __del__(self):
        """Close socket on deletion"""
        self._socket.close()

    def set_identity(self, name):
        """Set zmq socket identity"""
        self._socket.setsockopt(IDENTITY, name)

    def identity(self):
        """Return the zmq socket identity"""
        return self._socket.getsockopt(IDENTITY)

    def subscribe(self, title):
        """Subscribe to a broadcast with the given title"""
        # only subscribe if we're not already subscribed
        if title in self.filters:
            return
        # scramble the filter to avoid spurious matches (like CCD matching CCD1)        
        m = hashlib.md5()
        m.update(title.encode('UTF-8'))
        self._socket.setsockopt(SUBSCRIBE, m.digest())
        self.filters.append(title)

    def unsubscribe(self, title):
        """Unsubscribe to a broadcast with the given title"""
        m = hashlib.md5()
        m.update(title.encode('UTF-8'))
        self._socket.setsockopt(UNSUBSCRIBE, m.digest())
        self.filters.remove(title)

    def bind(self, addr):
        """Bind socket to address"""
        self._socket.bind(addr)

    def connect_socket(self, addr, tunnel=None):
        """Connect socket to endpoint, possible using an ssh tunnel
        The tunnel argument specifies the hostname of the ssh server to
        tunnel through.
        Note that this still succeeds even if there's no ZMQ server on
        the other end as the connection is asynchronous. For more details
        check the zmq_connect(3) documentation.
        """
        if(tunnel):
            from zmq import ssh
            # If there's no ssh server listening we're gonna
            # get stuck here for a long time as there's no timeout
            ssh.tunnel_connection(self._socket, addr, tunnel)
        else:            
            self._socket.connect(addr)

    def activity(self):
        """Callback run when there's activity on the socket"""
        self._notifier.setEnabled(False)
        while(self._socket.getsockopt(EVENTS) & POLLIN):
            self.ready_read.emit()
        self._notifier.setEnabled(True)

    def recv(self, flags=0):
        """Receive a message on the socket"""
        return self._socket.recv(flags)

    def recv_json(self, flags=0):
        """Receive and json decode a message on the socket"""
        return self._socket.recv_json(flags)

    def recv_multipart(self):
        """Receive a multipart message on the socket"""
        return self._socket.recv_multipart()

    def send(self, _msg):
        """Send a message on the socket"""
        return self._socket.send(_msg)

    def send_multipart(self, _msg):
        """Send a list of messages as a multipart message on the socket"""
        return self._socket.send_multipart(_msg)

    def recv_array(self, flags=0, copy=True, track=False):
        """Receive a numpy array"""
        md = self._socket.recv_json(flags=flags)
        msg = self._socket.recv(flags=flags, copy=copy, track=track)
        return  numpy.ndarray(shape=md['shape'], dtype=md['dtype'], buffer=msg, strides=md['strides'])