Exemplo n.º 1
0
class NdscanApplet(SimpleApplet):
    def __init__(self):
        # Use a small update delay by default to avoid lagging out the UI by
        # continuous redraws for plots with a large number of points. (20 ms
        # is a pretty arbitrary choice for a latency not perceptible by the
        # user in a normal use case).
        super().__init__(_MainWidget, default_update_delay=20e-3)

        self.argparser.add_argument(
            "--port-control",
            default=3251,
            type=int,
            help="TCP port for master control commands")
        self.argparser.add_argument("--prefix",
                                    default="ndscan.",
                                    type=str,
                                    help="Root of the ndscan dataset tree")
        self.argparser.add_argument("--rid",
                                    help="RID of the experiment to plot")

        common_args.verbosity_args(self.argparser)

    def subscribe(self):
        # We want to subscribe only to the experiment-local datasets for our RID
        # (but always, even if using IPC – this can be optimised later).
        self.subscriber = Subscriber("datasets_rid_{}".format(self.args.rid),
                                     self.sub_init, self.sub_mod)
        self.loop.run_until_complete(
            self.subscriber.connect(self.args.server, self.args.port))

        # Make sure we still respond to non-dataset messages like `terminate` in
        # embed mode.
        if self.embed is not None:

            def ignore(*args):
                pass

            self.ipc.subscribe([], ignore, ignore)

    def unsubscribe(self):
        self.loop.run_until_complete(self.subscriber.close())

    def filter_mod(self, *args):
        return True
Exemplo n.º 2
0
class SimpleApplet:
    def __init__(self,
                 main_widget_class,
                 cmd_description=None,
                 default_update_delay=0.0):
        self.main_widget_class = main_widget_class

        self.argparser = argparse.ArgumentParser(description=cmd_description)

        self.argparser.add_argument(
            "--update-delay",
            type=float,
            default=default_update_delay,
            help="time to wait after a mod (buffering other mods) "
            "before updating (default: %(default).2f)")

        group = self.argparser.add_argument_group("standalone mode (default)")
        group.add_argument("--server",
                           default="::1",
                           help="hostname or IP of the master to connect to "
                           "for dataset notifications "
                           "(ignored in embedded mode)")
        group.add_argument("--port",
                           default=3250,
                           type=int,
                           help="TCP port to connect to")

        self._arggroup_datasets = self.argparser.add_argument_group("datasets")

        self.dataset_args = set()

    def add_dataset(self, name, help=None, required=True):
        kwargs = dict()
        if help is not None:
            kwargs["help"] = help
        if required:
            self._arggroup_datasets.add_argument(name, **kwargs)
        else:
            self._arggroup_datasets.add_argument("--" + name, **kwargs)
        self.dataset_args.add(name)

    def args_init(self):
        self.args = self.argparser.parse_args()
        self.embed = os.getenv("ARTIQ_APPLET_EMBED")
        self.datasets = {
            getattr(self.args, arg.replace("-", "_"))
            for arg in self.dataset_args
        }

    def quamash_init(self):
        app = QtWidgets.QApplication([])
        self.loop = QEventLoop(app)
        asyncio.set_event_loop(self.loop)

    def ipc_init(self):
        if self.embed is not None:
            self.ipc = AppletIPCClient(self.embed)
            self.loop.run_until_complete(self.ipc.connect())

    def ipc_close(self):
        if self.embed is not None:
            self.ipc.close()

    def create_main_widget(self):
        self.main_widget = self.main_widget_class(self.args)
        if self.embed is not None:
            self.ipc.set_close_cb(self.main_widget.close)
            if os.name == "nt":
                # HACK: if the window has a frame, there will be garbage
                # (usually white) displayed at its right and bottom borders
                #  after it is embedded.
                self.main_widget.setWindowFlags(QtCore.Qt.FramelessWindowHint)
                self.main_widget.show()
                win_id = int(self.main_widget.winId())
                self.loop.run_until_complete(self.ipc.embed(win_id))
            else:
                # HACK:
                # Qt window embedding is ridiculously buggy, and empirical
                # testing has shown that the following procedure must be
                # followed exactly on Linux:
                # 1. applet creates widget
                # 2. applet creates native window without showing it, and
                #    gets its ID
                # 3. applet sends the ID to host, host embeds the widget
                # 4. applet shows the widget
                # 5. parent resizes the widget
                win_id = int(self.main_widget.winId())
                self.loop.run_until_complete(self.ipc.embed(win_id))
                self.main_widget.show()
                self.ipc.fix_initial_size()
        else:
            self.main_widget.show()

    def sub_init(self, data):
        self.data = data
        return data

    def filter_mod(self, mod):
        if self.embed is not None:
            # the parent already filters for us
            return True

        if mod["action"] == "init":
            return True
        if mod["path"]:
            return mod["path"][0] in self.datasets
        elif mod["action"] in {"setitem", "delitem"}:
            return mod["key"] in self.datasets
        else:
            return False

    def emit_data_changed(self, data, mod_buffer):
        self.main_widget.data_changed(data, mod_buffer)

    def flush_mod_buffer(self):
        self.emit_data_changed(self.data, self.mod_buffer)
        del self.mod_buffer

    def sub_mod(self, mod):
        if not self.filter_mod(mod):
            return

        if self.args.update_delay:
            if hasattr(self, "mod_buffer"):
                self.mod_buffer.append(mod)
            else:
                self.mod_buffer = [mod]
                asyncio.get_event_loop().call_later(self.args.update_delay,
                                                    self.flush_mod_buffer)
        else:
            self.emit_data_changed(self.data, [mod])

    def subscribe(self):
        if self.embed is None:
            self.subscriber = Subscriber("datasets", self.sub_init,
                                         self.sub_mod)
            self.loop.run_until_complete(
                self.subscriber.connect(self.args.server, self.args.port))
        else:
            self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod)

    def unsubscribe(self):
        if self.embed is None:
            self.loop.run_until_complete(self.subscriber.close())

    def run(self):
        self.args_init()
        self.quamash_init()
        try:
            self.ipc_init()
            try:
                self.create_main_widget()
                self.subscribe()
                try:
                    self.loop.run_forever()
                finally:
                    self.unsubscribe()
            finally:
                self.ipc_close()
        finally:
            self.loop.close()
Exemplo n.º 3
0
class WavemeterRemote:
    """
    Simple remote control for a :class:`WavemeterServer`, providing controls to start and stop the measurement,
    calibrate, control autocalibration and displaying the timestamp of the latest successful calibration as well as
    the autocalibration countdown.

    :param host: the address at which the :class:`WavemeterServer` RPC server and Publisher are running
    :param rpc_target: name of the RPC target
    :param rpc_port: port of the RPC server
    :param notifier_port: port of the publisher (notifier "status", containing the calibration timestamp and countdown)
    :param title: the window title (generated from the RPC target name by default)
    :param window_x: initial x position of the window in pixels
    :param window_y: initial y position of the window in pixels
    :param cal_channel: startup entry for the calibration channel
    :param cal_wl: startup entry for the calibration wavelength (in nm)
    :param cal_threshold: startup entry for the autocalibration threshold (in nm)
    :param cal_interval: startup entry for the autocalibration interval (in s)
    :param cal_retry_interval: startup entry for the autocalibration retry interval (in s)
    :param start_autocal: set to start autocalibration on startup
    """
    def __init__(self,
                 host: str = "::1",
                 rpc_target: str = "wavemeter_server",
                 rpc_port: int = 3280,
                 notifier_port: int = 3281,
                 title: str = None,
                 window_x: int = 0,
                 window_y: int = 0,
                 cal_channel: int = 1,
                 cal_wl: float = 633.,
                 cal_threshold: float = 0.00005,
                 cal_interval: int = 600,
                 cal_retry_interval: int = 10,
                 start_autocal: bool = False):

        self._app = QApplication(sys.argv)
        self._loop = QEventLoop(self._app)
        asyncio.set_event_loop(self._loop)

        self.wm_status_dict = {
            "autocal_countdown": 0,
            "calibration_timestamp": -1
        }

        self._subscriber = Subscriber("status", self._subscriber_init_cb,
                                      self._subscriber_mod_cb)
        logger.info(
            "Connecting to publisher at {}:{}, notifier name: status".format(
                host, notifier_port))
        self._loop.run_until_complete(
            self._subscriber.connect(host, notifier_port))

        logger.info(
            "Connecting to RPC server at {}:{}, target name: {}".format(
                host, rpc_port, rpc_target))
        self._rpc_client = BestEffortClient(host, rpc_port, rpc_target)
        self._loop.create_task(self.keepalive_loop())

        self.title = title if title is not None else "Wavemeter remote control ({} at {})".format(
            rpc_target, host)

        self._ui_input_elements = dict(
        )  # stores all input elements, used on function calls to retrieve their values
        self._build_ui(window_x, window_y, cal_channel, cal_wl, cal_threshold,
                       cal_interval, cal_retry_interval)

        if start_autocal and self._rpc_client is not None:
            self._rpc_client.start_autocalibration(cal_channel, cal_wl,
                                                   cal_threshold, cal_interval,
                                                   cal_retry_interval)

    def _build_ui(self, window_x, window_y, cal_channel, cal_wl, cal_threshold,
                  cal_interval, cal_retry_interval):
        self.window = QWidget()
        self.window.setWindowTitle(self.title)

        self.window.move(window_x, window_y)

        self.v_layout = QVBoxLayout()

        self.window.setLayout(self.v_layout)

        indicator_font = QFont()
        indicator_font.setPointSize(18)
        self.calibration_timestamp_display = QLabel()
        self.calibration_timestamp_display.setFont(indicator_font)
        self.calibration_countdown_display = QLabel()
        self.calibration_countdown_display.setFont(indicator_font)

        startstop_gb = QGroupBox("Measurement")
        startstop_gb_layout = QHBoxLayout()
        startstop_gb.setLayout(startstop_gb_layout)

        start_button = QPushButton("&Start measurement")

        def start():
            if self._rpc_client is not None:
                logger.info("sending RPC start_measurement()")
                self._rpc_client.start_measurement()

        start_button.clicked.connect(start)
        startstop_gb_layout.addWidget(start_button)

        stop_button = QPushButton("S&top measurement")

        def stop():
            if self._rpc_client is not None:
                logger.info("sending RPC stop_measurement()")
                self._rpc_client.stop_measurement()

        stop_button.clicked.connect(stop)
        startstop_gb_layout.addWidget(stop_button)

        self.v_layout.addWidget(startstop_gb)

        cal_gb = QGroupBox("Calibration")
        cal_gb_outer_layout = QVBoxLayout()
        cal_gb_outer_layout.addWidget(self.calibration_timestamp_display)
        cal_gb_outer_layout.addWidget(self.calibration_countdown_display)
        cal_gb_layout = QHBoxLayout()
        cal_gb_outer_layout.addLayout(cal_gb_layout)
        cal_gb.setLayout(cal_gb_outer_layout)

        calibrate_gb = QGroupBox("Calibrate")
        calibrate_gb_layout = QFormLayout()
        calibrate_gb.setLayout(calibrate_gb_layout)

        calibrate_button = QPushButton("&Calibrate")

        def calibrate():
            ch = int(self._ui_input_elements["cal_channel"].value())
            wl = self._ui_input_elements["cal_wl"].value()
            if self._rpc_client is not None:
                logger.info(
                    "sending RPC calibrate(channel={}, wavelength={})".format(
                        ch, wl))
                self._rpc_client.calibrate(ch, wl)

        calibrate_button.clicked.connect(calibrate)

        calibrate_gb_layout.addRow("", calibrate_button)

        self._ui_input_elements.update({"cal_channel": QDoubleSpinBox()})
        self._ui_input_elements["cal_channel"].setRange(1, 100)
        self._ui_input_elements["cal_channel"].setSingleStep(1)
        self._ui_input_elements["cal_channel"].setDecimals(0)
        self._ui_input_elements["cal_channel"].setValue(cal_channel)
        calibrate_gb_layout.addRow("Channel",
                                   self._ui_input_elements["cal_channel"])

        self._ui_input_elements.update({"cal_wl": QDoubleSpinBox()})
        self._ui_input_elements["cal_wl"].setRange(1., 10000.)
        self._ui_input_elements["cal_wl"].setSingleStep(1e-10)
        self._ui_input_elements["cal_wl"].setDecimals(10)
        self._ui_input_elements["cal_wl"].setSuffix(" nm")
        self._ui_input_elements["cal_wl"].setValue(cal_wl)
        calibrate_gb_layout.addRow("Wavelength",
                                   self._ui_input_elements["cal_wl"])

        cal_gb_layout.addWidget(calibrate_gb)

        autocalibration_gb = QGroupBox("Autocalibration")
        autocalibration_gb_layout = QFormLayout()
        autocalibration_gb.setLayout(autocalibration_gb_layout)

        start_autocalibration_button = QPushButton("Start &autocalibration")

        def start_autocalibration():
            ch = int(self._ui_input_elements["autocal_channel"].value())
            wl = self._ui_input_elements["autocal_wl"].value()
            thr = self._ui_input_elements["autocal_threshold"].value()
            interval = int(self._ui_input_elements["autocal_interval"].value())
            retry_interval = int(
                self._ui_input_elements["autocal_retry_interval"].value())
            if self._rpc_client is not None:
                logger.info(
                    "sending RPC start_autocalibration(channel={}, wavelength={}, threshold={}, "
                    "interval={}, retry_interval={})".format(
                        ch, wl, thr, interval, retry_interval))
                self._rpc_client.start_autocalibration(ch, wl, thr, interval,
                                                       retry_interval)

        start_autocalibration_button.clicked.connect(start_autocalibration)

        autocalibration_gb_layout.addRow("", start_autocalibration_button)

        self._ui_input_elements.update({"autocal_channel": QDoubleSpinBox()})
        self._ui_input_elements["autocal_channel"].setRange(1, 100)
        self._ui_input_elements["autocal_channel"].setSingleStep(1)
        self._ui_input_elements["autocal_channel"].setDecimals(0)
        self._ui_input_elements["autocal_channel"].setValue(cal_channel)
        autocalibration_gb_layout.addRow(
            "Channel", self._ui_input_elements["autocal_channel"])

        self._ui_input_elements.update({"autocal_wl": QDoubleSpinBox()})
        self._ui_input_elements["autocal_wl"].setRange(1., 10000.)
        self._ui_input_elements["autocal_wl"].setSingleStep(1e-10)
        self._ui_input_elements["autocal_wl"].setDecimals(10)
        self._ui_input_elements["autocal_wl"].setSuffix(" nm")
        self._ui_input_elements["autocal_wl"].setValue(cal_wl)
        autocalibration_gb_layout.addRow("Wavelength",
                                         self._ui_input_elements["autocal_wl"])

        self._ui_input_elements.update({"autocal_threshold": QDoubleSpinBox()})
        self._ui_input_elements["autocal_threshold"].setRange(1e-10, 10000.)
        self._ui_input_elements["autocal_threshold"].setSingleStep(1e-10)
        self._ui_input_elements["autocal_threshold"].setDecimals(10)
        self._ui_input_elements["autocal_threshold"].setSuffix(" nm")
        self._ui_input_elements["autocal_threshold"].setValue(cal_threshold)
        autocalibration_gb_layout.addRow(
            "Threshold", self._ui_input_elements["autocal_threshold"])

        self._ui_input_elements.update({"autocal_interval": QDoubleSpinBox()})
        self._ui_input_elements["autocal_interval"].setRange(1, 100000)
        self._ui_input_elements["autocal_interval"].setSingleStep(1)
        self._ui_input_elements["autocal_interval"].setDecimals(0)
        self._ui_input_elements["autocal_interval"].setSuffix(" s")
        self._ui_input_elements["autocal_interval"].setValue(cal_interval)
        autocalibration_gb_layout.addRow(
            "Interval", self._ui_input_elements["autocal_interval"])

        self._ui_input_elements.update(
            {"autocal_retry_interval": QDoubleSpinBox()})
        self._ui_input_elements["autocal_retry_interval"].setRange(1, 100000)
        self._ui_input_elements["autocal_retry_interval"].setSingleStep(1)
        self._ui_input_elements["autocal_retry_interval"].setDecimals(0)
        self._ui_input_elements["autocal_retry_interval"].setSuffix(" s")
        self._ui_input_elements["autocal_retry_interval"].setValue(
            cal_retry_interval)
        autocalibration_gb_layout.addRow(
            "Retry interval",
            self._ui_input_elements["autocal_retry_interval"])

        stop_autocalibration_button = QPushButton("St&op autocalibration")

        def stop_autocalibration():
            if self._rpc_client is not None:
                logger.info("sending RPC stop_autocalibration()")
                self._rpc_client.stop_autocalibration()

        stop_autocalibration_button.clicked.connect(stop_autocalibration)

        autocalibration_gb_layout.addRow("", stop_autocalibration_button)

        cal_gb_layout.addWidget(autocalibration_gb)

        self.v_layout.addWidget(cal_gb)

        exposure_gb = QGroupBox("Exposure")
        exposure_gb_layout = QHBoxLayout()
        exposure_gb.setLayout(exposure_gb_layout)

        control_form = QFormLayout()
        self._ui_input_elements.update({"exp_channel": QDoubleSpinBox()})
        self._ui_input_elements["exp_channel"].setRange(1, 100)
        self._ui_input_elements["exp_channel"].setSingleStep(1)
        self._ui_input_elements["exp_channel"].setDecimals(0)
        self._ui_input_elements["exp_channel"].setValue(1)
        control_form.addRow("Channel", self._ui_input_elements["exp_channel"])

        self._ui_input_elements.update({"exp_time1": QDoubleSpinBox()})
        self._ui_input_elements["exp_time1"].setRange(1, 2000)
        self._ui_input_elements["exp_time1"].setSingleStep(1)
        self._ui_input_elements["exp_time1"].setDecimals(0)
        self._ui_input_elements["exp_time1"].setSuffix(" ms")
        self._ui_input_elements["exp_time1"].setValue(1)
        control_form.addRow("Time 1", self._ui_input_elements["exp_time1"])

        self._ui_input_elements.update({"exp_time2": QDoubleSpinBox()})
        self._ui_input_elements["exp_time2"].setRange(0, 2000)
        self._ui_input_elements["exp_time2"].setSingleStep(1)
        self._ui_input_elements["exp_time2"].setDecimals(0)
        self._ui_input_elements["exp_time2"].setSuffix(" ms")
        self._ui_input_elements["exp_time2"].setValue(1)
        control_form.addRow("Time 2", self._ui_input_elements["exp_time2"])

        self._ui_input_elements.update({"exp_auto": QCheckBox()})
        control_form.addRow("Auto adjust", self._ui_input_elements["exp_auto"])

        exposure_gb_layout.addLayout(control_form)

        exposure_button_layout = QVBoxLayout()

        exposure_get_button = QPushButton("&Get")

        def exposure_get():
            channel = int(self._ui_input_elements["exp_channel"].value())
            if self._rpc_client is not None:
                logger.info(
                    "sending RPC get_exposure_time({}, 1)".format(channel))
                time1 = self._rpc_client.get_exposure_time(channel, 1)
                logger.info(
                    "sending RPC get_exposure_time({}, 2)".format(channel))
                time2 = self._rpc_client.get_exposure_time(channel, 2)
                logger.info(
                    "sending RPC get_exposure_auto_adjust({})".format(channel))
                auto = self._rpc_client.get_exposure_auto_adjust(channel)
                self._ui_input_elements["exp_time1"].setValue(time1)
                self._ui_input_elements["exp_time2"].setValue(time2)
                self._ui_input_elements["exp_auto"].setChecked(auto)

        exposure_get_button.clicked.connect(exposure_get)

        exposure_button_layout.addWidget(exposure_get_button)

        exposure_set_button = QPushButton("S&et")

        def exposure_set():
            channel = int(self._ui_input_elements["exp_channel"].value())
            time1 = int(self._ui_input_elements["exp_time1"].value())
            time2 = int(self._ui_input_elements["exp_time2"].value())
            auto = bool(self._ui_input_elements["exp_auto"].isChecked())
            if self._rpc_client is not None:
                logger.info("sending RPC set_exposure_time({}, 1, {})".format(
                    channel, time1))
                self._rpc_client.set_exposure_time(channel, 1, time1)
                logger.info("sending RPC set_exposure_time({}, 2, {})".format(
                    channel, time2))
                self._rpc_client.set_exposure_time(channel, 2, time2)
                logger.info(
                    "sending RPC set_exposure_auto_adjust({}, {})".format(
                        channel, auto))
                self._rpc_client.set_exposure_auto_adjust(channel, auto)

        exposure_set_button.clicked.connect(exposure_set)

        exposure_button_layout.addWidget(exposure_set_button)

        exposure_gb_layout.addLayout(exposure_button_layout)

        self.v_layout.addWidget(exposure_gb)

    def _update_cal_timestamp(self):
        self.calibration_timestamp_display.setText(
            "Last successful"
            " calibration:\n{}".format(
                time.asctime(
                    time.localtime(
                        self.wm_status_dict["calibration_timestamp"]))))

    def _update_cal_countdown(self):
        self.calibration_countdown_display.setText(
            "Next autocalibration attempt in {:.0f} s".format(
                self.wm_status_dict["autocal_countdown"]))

    def _subscriber_init_cb(self, data):
        self.wm_status_dict = data
        self._update_cal_timestamp()
        self._update_cal_countdown()
        return data

    def _subscriber_mod_cb(self, mod):
        try:
            if mod["key"] == "calibration_timestamp":
                self._update_cal_timestamp()
            elif mod["key"] == "autocal_countdown":
                self._update_cal_countdown()
        except KeyError:
            pass

    keepalive_interval = 3600.  # ping RPC server after this interval to keep the connection alive

    async def keepalive_loop(self):
        """Keep the RPC connection alive"""
        while True:
            logger.info("Pinging the RPC server to keep the connection alive")
            self._rpc_client.ping()
            await asyncio.sleep(self.keepalive_interval)

    def run(self):
        self.window.show()
        self._loop.run_forever()
        if self._subscriber is not None:
            self._loop.run_until_complete(self._subscriber.close())
        if self._rpc_client is not None:
            self._rpc_client.close_rpc()
Exemplo n.º 4
0
class WavemeterClient:
    """
    Base class for wavemeter clients. Installs a subscriber for one channel (wavelength, temperature or pressure).
    Values are accessible via `self.value`, corresponding timestamps from the wavemeter software via `self.timestamp`.

    Wavelengths are vacuum wavelengths in nm, temperature is in degrees C, pressure is in mbar.

    :meth:`_new_value_callback` gets called on every update (`value` and `timestamp` already return the new
    values at the time of the call).

    :param channel: wavemeter channel to subscribe to (can be either <n>, '<n>', 'ch<n>', 'T' or 'p')
    :param host: host running the publisher
    :param port: port of the publisher
    :param event_loop: asyncio event loop for starting the subscriber (defaults to asyncio.get_event_loop())

    """
    def __init__(self,
                 channel: str = "ch1",
                 host: str = "::1",
                 port: int = 3281,
                 event_loop: Any = None):
        try:  # accept integer (or string lacking the "ch" prefix) as channel argument
            self.channel = "ch{}".format(int(channel))
        except ValueError:
            self.channel = channel
        self.host = host
        self.port = port

        self.data = [(0, -1.)]
        self._subscriber = Subscriber(self.channel, self._subscriber_init_cb,
                                      self._subscriber_mod_cb)

        self._event_loop = event_loop if event_loop is not None else asyncio.get_event_loop(
        )

        self._event_loop.create_task(
            self._subscriber.connect(self.host, self.port))

    def _subscriber_init_cb(self, data):
        self.data = data
        self._init_callback()
        return data

    def _subscriber_mod_cb(self, _):
        self._new_value_callback()

    @property
    def value(self) -> float:
        """
        Latest value.

        :return: latest known value (vacuum wavelength in nm, temperature in degrees C, or pressure in mbar)
        """
        return self.data[0][1]

    @property
    def timestamp(self) -> int:
        """
        Latest time stamp.

        :return: timestamp (from the wavemeter software) for latest known value in ms
        """
        return self.data[0][0]

    def _init_callback(self):
        pass

    def _new_value_callback(self):
        pass

    def get_channel(self) -> str:
        """The wavemeter channel to which this client has subscribed.

        :return: channel name
        """
        return self.channel

    def close_subscriber(self):
        self._event_loop.run_until_complete(self._subscriber.close())