class HardwareComponent(QtCore.QObject):
    """
    :class:`HardwareComponent`
    
    Base class for ScopeFoundry Hardware objects
    
    to subclass, implement :meth:`setup`, :meth:`connect` and :meth:`disconnect`
    
    """
    connection_succeeded = QtCore.Signal()
    connection_failed = QtCore.Signal()

    def add_logged_quantity(self, name, **kwargs):
        #lq = LoggedQuantity(name=name, **kwargs)
        #self.logged_quantities[name] = lq
        #return lq
        return self.settings.New(name, **kwargs)

    def add_operation(self, name, op_func):
        """
        Create an operation for the HardwareComponent.
        
        *op_func* is a function that will be called upon operation activation
        
        operations are typically exposed in the default ScopeFoundry gui via a pushButton
        
        :type name: str
        :type op_func: QtCore.Slot
        """

        self.operations[name] = op_func

    def __init__(self, app, debug=False, name=None):
        """
        create new HardwareComponent attached to *app*
        """
        QtCore.QObject.__init__(self)

        if not hasattr(self, 'name'):
            self.name = self.__class__.__name__

        if name is not None:
            self.name = name

        self.log = get_logger_from_class(self)

        # threading lock
        #self.lock = threading.Lock()
        #self.lock = DummyLock()
        self.lock = QLock(mode=1)  # mode 0 is non-reentrant lock

        self.app = app

        #self.logged_quantities = OrderedDict()
        self.settings = LQCollection()
        self.operations = OrderedDict()

        self.connected = self.add_logged_quantity("connected", dtype=bool)
        self.connected.updated_value[bool].connect(self.enable_connection)

        self.connect_success = False

        self.debug_mode = self.add_logged_quantity("debug_mode",
                                                   dtype=bool,
                                                   initial=debug)

        self.auto_thread_lock = True

        self.setup()

        if self.auto_thread_lock:
            self.thread_lock_all_lq()

        self.has_been_connected_once = False

        self.is_connected = False

        self.connection_failed.connect(self.on_connection_failed)
        self.connection_succeeded.connect(self.on_connection_succeeded)

        self.add_operation('Reload_Code', self.reload_code)

    def setup(self):
        """
        Runs during __init__, before the hardware connection is established
        Should generate desired LoggedQuantities, operations
        """
        raise NotImplementedError()

    def new_control_widgets(self):

        self.controls_groupBox = QtWidgets.QGroupBox(self.name)
        self.controls_formLayout = QtWidgets.QFormLayout()
        self.controls_groupBox.setLayout(self.controls_formLayout)

        #self.connect_hardware_checkBox = QtWidgets.QCheckBox("Connect to Hardware")
        #self.controls_formLayout.addRow("Connect", self.connect_hardware_checkBox)
        #self.connect_hardware_checkBox.stateChanged.connect(self.enable_connection)

        self.control_widgets = OrderedDict()
        for lqname, lq in self.settings.as_dict().items():
            #: :type lq: LoggedQuantity
            if lq.choices is not None:
                widget = QtWidgets.QComboBox()
            elif lq.dtype in [int, float]:
                if lq.si:
                    widget = pg.SpinBox()
                else:
                    widget = QtWidgets.QDoubleSpinBox()
            elif lq.dtype in [bool]:
                widget = QtWidgets.QCheckBox()
            elif lq.dtype in [str]:
                widget = QtWidgets.QLineEdit()
            lq.connect_bidir_to_widget(widget)

            # Add to formlayout
            self.controls_formLayout.addRow(lqname, widget)
            self.control_widgets[lqname] = widget

        self.op_buttons = OrderedDict()
        for op_name, op_func in self.operations.items():
            op_button = QtWidgets.QPushButton(op_name)
            op_button.clicked.connect(lambda checked, f=op_func: f())
            self.controls_formLayout.addRow(op_name, op_button)

        self.read_from_hardware_button = QtWidgets.QPushButton(
            "Read From Hardware")
        self.read_from_hardware_button.clicked.connect(self.read_from_hardware)
        self.controls_formLayout.addRow("Logged Quantities:",
                                        self.read_from_hardware_button)

        return self.controls_groupBox

    def add_widgets_to_tree(self, tree):
        #tree = self.app.ui.hardware_treeWidget
        #tree = QTreeWidget()
        tree.setColumnCount(2)
        tree.setHeaderLabels(["Hardware", "Value"])

        self.tree_item = QtWidgets.QTreeWidgetItem(tree, [self.name, "o"])
        tree.insertTopLevelItem(0, self.tree_item)
        self.tree_item.setFirstColumnSpanned(False)
        self.tree_item.setForeground(1, QtGui.QColor('red'))

        # Add logged quantities to tree
        self.settings.add_widgets_to_subtree(self.tree_item)

        # Add oepration buttons to tree
        self.op_buttons = OrderedDict()
        for op_name, op_func in self.operations.items():
            op_button = QtWidgets.QPushButton(op_name)
            op_button.clicked.connect(lambda checked, f=op_func: f())
            self.op_buttons[op_name] = op_button
            #self.controls_formLayout.addRow(op_name, op_button)
            op_tree_item = QtWidgets.QTreeWidgetItem(self.tree_item,
                                                     [op_name, ""])
            tree.setItemWidget(op_tree_item, 1, op_button)

        self.tree_read_from_hardware_button = QtWidgets.QPushButton(
            "Read From\nHardware")
        self.tree_read_from_hardware_button.clicked.connect(
            self.read_from_hardware)
        #self.controls_formLayout.addRow("Logged Quantities:", self.read_from_hardware_button)
        self.read_from_hardware_button_tree_item = QtWidgets.QTreeWidgetItem(
            self.tree_item, ["Logged Quantities:", ""])
        self.tree_item.addChild(self.read_from_hardware_button_tree_item)
        tree.setItemWidget(self.read_from_hardware_button_tree_item, 1,
                           self.tree_read_from_hardware_button)

    @QtCore.Slot()
    def read_from_hardware(self):
        """
        Read all settings (:class:`LoggedQuantity`) connected to hardware states
        """
        for name, lq in self.settings.as_dict().items():
            if lq.has_hardware_read():
                if self.debug_mode.val:
                    self.log.debug("read_from_hardware {}".format(name))
                lq.read_from_hardware()

    def connect(self):
        """
        Opens a connection to hardware
        and connects :class:`LoggedQuantity` settings to related hardware 
        functions and parameters 
        """
        raise NotImplementedError()

    def disconnect(self):
        """
        Disconnects the hardware and severs hardware--:class:`LoggedQuantity` links
        """

        raise NotImplementedError()

    @QtCore.Slot(bool)
    def enable_connection(self, enable=True):
        if enable:
            try:
                self.connect()
                # start thread if needed
                if hasattr(self, 'run'):
                    self.update_thread_interrupted = False
                    self._update_thread = threading.Thread(target=self.run)
                    self._update_thread.start()

                self.connection_succeeded.emit()
            except Exception as err:
                self.connection_failed.emit()
                raise err
        else:
            print("disabling connection")
            try:
                try:
                    if hasattr(self, 'run') and hasattr(
                            self, '_update_thread'):
                        self.update_thread_interrupted = True
                        self._update_thread.join(timeout=5.0)
                        del self._update_thread
                finally:
                    self.disconnect()
                    self.tree_item.setText(1, 'X')
                    self.tree_item.setForeground(1, QtGui.QColor('red'))
            except Exception as err:
                # disconnect failed
                self.tree_item.setText(1, '?')
                self.tree_item.setForeground(1, QtGui.QColor('red'))
                raise err

    def run(self):
        if hasattr(self, 'threaded_update'):
            while not self.update_thread_interrupted:
                self.threaded_update()

    def on_connection_succeeded(self):
        print(self.name, "connection succeeded!")
        self.tree_item.setText(1, 'O')
        self.tree_item.setForeground(1, QtGui.QColor('green'))

    def on_connection_failed(self):
        print(self.name, "connection failed!")
        self.settings.connected.update_value(False)
        self.tree_item.setText(1, '!')
        self.tree_item.setForeground(1, QtGui.QColor('red'))

    @property
    def gui(self):
        warnings.warn("Hardware.gui is deprecated, use Hardware.app",
                      DeprecationWarning)
        return self.app

    def web_ui(self):
        return "Hardware {}".format(self.name)

    def thread_lock_lq(self, lq):
        lq.old_lock = lq.lock
        lq.lock = self.lock

    def thread_lock_all_lq(self):
        for lq in self.settings.as_list():
            lq.old_lock = lq.lock
            lq.lock = self.lock

    def reload_code(self):
        import inspect
        import xreload
        mod = inspect.getmodule(self)
        x = xreload.xreload(mod)
        print("Reloading from code", mod, x)
Beispiel #2
0
class BaseApp(QtCore.QObject):
    def __init__(self, argv=[]):
        QtCore.QObject.__init__(self)
        self.log = get_logger_from_class(self)

        self.this_dir, self.this_filename = os.path.split(__file__)

        self.qtapp = QtWidgets.QApplication.instance()
        if not self.qtapp:
            self.qtapp = QtWidgets.QApplication(argv)

        self.settings = LQCollection()

        # auto creation of console widget
        self.setup_console_widget()

        # FIXME Breaks things for microscopes, but necessary for stand alone apps!
        #if hasattr(self, "setup"):
        #    self.setup()

        self.setup_logging()

        if not hasattr(self, 'name'):
            self.name = "ScopeFoundry"
        self.qtapp.setApplicationName(self.name)

    def exec_(self):
        return self.qtapp.exec_()

    def setup_console_widget(self, kernel=None):
        """
        Create and return console QWidget. If Jupyter / IPython is installed
        this widget will be a full-featured IPython console. If Jupyter is unavailable
        it will fallback to a pyqtgraph.console.ConsoleWidget.
        
        If the app is started in an Jupyter notebook, the console will be
        connected to the notebook's IPython kernel.
        
        the returned console_widget will also be accessible as self.console_widget
        
        In order to see the console widget, remember to insert it into an existing
        window or call self.console_widget.show() to create a new window      
        """
        if CONSOLE_TYPE == 'pyqtgraph.console':
            self.console_widget = pyqtgraph.console.ConsoleWidget(
                namespace={
                    'app': self,
                    'pg': pg,
                    'np': np
                },
                text="ScopeFoundry Console")
        elif CONSOLE_TYPE == 'qtconsole':

            if kernel == None:
                try:  # try to find an existing kernel
                    #https://github.com/jupyter/notebook/blob/master/docs/source/examples/Notebook/Connecting%20with%20the%20Qt%20Console.ipynb
                    import ipykernel as kernel
                    conn_file = kernel.get_connection_file()
                    import qtconsole.qtconsoleapp
                    self.qtconsole_app = qtconsole.qtconsoleapp.JupyterQtConsoleApp(
                    )
                    self.console_widget = self.qtconsole_app.new_frontend_connection(
                        conn_file)
                    self.console_widget.setWindowTitle(
                        "ScopeFoundry IPython Console")
                except:  # make your own new in-process kernel
                    # https://github.com/ipython/ipython-in-depth/blob/master/examples/Embedding/inprocess_qtconsole.py
                    self.kernel_manager = QtInProcessKernelManager()
                    self.kernel_manager.start_kernel()
                    self.kernel = self.kernel_manager.kernel
                    self.kernel.shell.banner1 += """
                    ScopeFoundry Console
                    
                    Variables:
                     * np: numpy package
                     * app: the ScopeFoundry App object
                    """
                    self.kernel.gui = 'qt4'
                    self.kernel.shell.push({'np': np, 'app': self})
                    self.kernel_client = self.kernel_manager.client()
                    self.kernel_client.start_channels()

                    #self.console_widget = RichIPythonWidget()
                    self.console_widget = RichJupyterWidget()
                    self.console_widget.setWindowTitle(
                        "ScopeFoundry IPython Console")
                    self.console_widget.kernel_manager = self.kernel_manager
                    self.console_widget.kernel_client = self.kernel_client
            else:
                import qtconsole.qtconsoleapp
                self.qtconsole_app = qtconsole.qtconsoleapp.JupyterQtConsoleApp(
                )
                self.console_widget = self.qtconsole_app.new_frontend_connection(
                    kernel.get_connection_file())
                self.console_widget.setWindowTitle(
                    "ScopeFoundry IPython Console")
        else:
            raise ValueError("CONSOLE_TYPE undefined")

        return self.console_widget

    def setup(self):
        pass

    def settings_save_ini(self, fname, save_ro=True):
        """"""
        config = configparser.ConfigParser()
        config.optionxform = str
        config.add_section('app')
        config.set('app', 'name', self.name)
        for lqname, lq in self.settings.as_dict().items():
            if not lq.ro or save_ro:
                config.set('app', lqname, lq.ini_string_value())

        with open(fname, 'w') as configfile:
            config.write(configfile)

        self.log.info("ini settings saved to {} {}".format(
            fname, config.optionxform))

    def settings_load_ini(self, fname):
        self.log.info("ini settings loading from " + fname)

        config = configparser.ConfigParser()
        config.optionxform = str
        config.read(fname)

        if 'app' in config.sections():
            for lqname, new_val in config.items('app'):
                #print(lqname)
                lq = self.settings.as_dict().get(lqname)
                if lq:
                    if lq.dtype == bool:
                        new_val = str2bool(new_val)
                    lq.update_value(new_val)

    def settings_save_ini_ask(self, dir=None, save_ro=True):
        """Opens a Save dialogue asking the user to select a save destination and give the save file a filename. Saves settings to an .ini file."""
        # TODO add default directory, etc
        fname, _ = QtWidgets.QFileDialog.getSaveFileName(
            self.ui,
            caption=u'Save Settings',
            dir=u"",
            filter=u"Settings (*.ini)")
        #print(repr(fname))
        if fname:
            self.settings_save_ini(fname, save_ro=save_ro)
        return fname

    def settings_load_ini_ask(self, dir=None):
        """Opens a Load dialogue asking the user which .ini file to load into our app settings. Loads settings from an .ini file."""
        # TODO add default directory, etc
        fname, _ = QtWidgets.QFileDialog.getOpenFileName(
            None, "Settings (*.ini)")
        #print(repr(fname))
        if fname:
            self.settings_load_ini(fname)
        return fname

    def setup_logging(self):

        logging.basicConfig(
            level=logging.WARN)  #, filename='example.log', stream=sys.stdout)
        logging.getLogger('traitlets').setLevel(logging.WARN)
        logging.getLogger('ipykernel.inprocess').setLevel(logging.WARN)
        logging.getLogger('LoggedQuantity').setLevel(logging.WARN)
        logging.getLogger('PyQt5').setLevel(logging.WARN)
        logger = logging.getLogger('FoundryDataBrowser')

        self.logging_widget = QtWidgets.QWidget()
        self.logging_widget.setWindowTitle("Log")
        self.logging_widget.setLayout(QtWidgets.QVBoxLayout())
        self.logging_widget.search_lineEdit = QtWidgets.QLineEdit()
        self.logging_widget.log_textEdit = QtWidgets.QTextEdit("")

        self.logging_widget.layout().addWidget(
            self.logging_widget.search_lineEdit)
        self.logging_widget.layout().addWidget(
            self.logging_widget.log_textEdit)

        self.logging_widget.log_textEdit.document().setDefaultStyleSheet(
            "body{font-family: Courier;}")

        self.logging_widget_handler = LoggingQTextEditHandler(
            self.logging_widget.log_textEdit, level=logging.DEBUG)
        logging.getLogger().addHandler(self.logging_widget_handler)