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
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()
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()
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())