diff --git a/artiq/dashboard/applets_ccb.py b/artiq/dashboard/applets_ccb.py new file mode 100644 index 000000000..7fd984cae --- /dev/null +++ b/artiq/dashboard/applets_ccb.py @@ -0,0 +1,89 @@ +import logging + +from PyQt5 import QtCore, QtWidgets + +from artiq.gui import applets + + +logger = logging.getLogger(__name__) + + +class AppletsCCBDock(applets.AppletsDock): + def __init__(self, *args, **kwargs): + applets.AppletsDock.__init__(self, *args, **kwargs) + + sep = QtWidgets.QAction(self.table) + sep.setSeparator(True) + self.table.addAction(sep) + self.listen_action = QtWidgets.QAction( + "Listen to client control broadcasts", self.table) + self.listen_action.setCheckable(True) + self.table.addAction(self.listen_action) + + def locate_applet(self, name, group, create_groups): + if group is None: + group = [] + elif isinstance(group, str): + group = [group] + + parent = self.table.invisibleRootItem() + for g in group: + new_parent = None + for i in range(parent.childCount()): + child = parent.child(i) + if child.ty == "group" and child.text(1) == g: + new_parent = child + break + if new_parent is None: + if create_groups: + new_parent = self.new_group(g, parent) + else: + return None, None + parent = new_parent + + applet = None + for i in range(parent.childCount()): + child = parent.child(i) + if child.ty == "applet" and child.text(1) == name: + applet = child + break + return parent, applet + + def ccb_create_applet(self, name, command_or_code, group=None, is_code=False): + if not self.listen_action.isChecked(): + return + parent, applet = self.locate_applet(name, group, True) + if applet is None: + applet = self.new(name=name, command=command_or_code, parent=parent) + else: + applet.setText(2, command_or_code) + applet.setCheckState(0, QtCore.Qt.Checked) + + def ccb_disable_applet(self, name, group=None): + if not self.listen_action.isChecked(): + return + parent, applet = self.locate_applet(name, group, False) + if applet is not None: + applet.setCheckState(0, QtCore.Qt.Unchecked) + + def ccb_notify(self, message): + try: + service = message["service"] + args = message["args"] + kwargs = message["kwargs"] + if service == "create_applet": + self.ccb_create_applet(*args, **kwargs) + elif service == "disable_applet": + self.ccb_disable_applet(*args, **kwargs) + except: + logger.error("failed to process CCB", exc_info=True) + + def save_state(self): + return { + "applets": applets.AppletsDock.save_state(self), + "listen": self.listen_action.isChecked() + } + + def restore_state(self, state): + applets.AppletsDock.restore_state(self, state["applets"]) + self.listen_action.setChecked(state["listen"]) diff --git a/artiq/examples/master/repository/flopping_f_simulation.py b/artiq/examples/master/repository/flopping_f_simulation.py index 2d802ad99..d6e875be7 100644 --- a/artiq/examples/master/repository/flopping_f_simulation.py +++ b/artiq/examples/master/repository/flopping_f_simulation.py @@ -29,6 +29,7 @@ class FloppingF(EnvExperiment): 0.1, min=0, max=100, step=0.01)) self.setattr_device("scheduler") + self.setattr_device("ccb") def run(self): l = len(self.frequency_scan) @@ -41,6 +42,11 @@ class FloppingF(EnvExperiment): self.set_dataset("flopping_f_fit", np.full(l, np.nan), broadcast=True, save=False) + self.ccb.issue("create_applet", "flopping_f", + "${artiq_applet}plot_xy " + "flopping_f_brightness --x flopping_f_frequency " + "--fit flopping_f_fit") + for i, f in enumerate(self.frequency_scan): m_brightness = model(f, self.F0) + self.noise_amplitude*random.random() self.mutate_dataset("flopping_f_frequency", i, f) diff --git a/artiq/frontend/artiq_dashboard.py b/artiq/frontend/artiq_dashboard.py index 9fa2956e1..d5d64e835 100755 --- a/artiq/frontend/artiq_dashboard.py +++ b/artiq/frontend/artiq_dashboard.py @@ -15,9 +15,9 @@ from artiq.tools import (atexit_register_coroutine, verbosity_args, from artiq.protocols.pc_rpc import AsyncioClient from artiq.protocols.broadcast import Receiver from artiq.gui.models import ModelSubscriber -from artiq.gui import state, applets, log +from artiq.gui import state, log from artiq.dashboard import (experiments, shortcuts, explorer, - moninj, datasets, schedule) + moninj, datasets, schedule, applets_ccb) def get_argparser(): @@ -119,10 +119,13 @@ def main(): atexit_register_coroutine(subscriber.close) sub_clients[notifier_name] = subscriber - log_receiver = Receiver("log", []) - loop.run_until_complete(log_receiver.connect( - args.server, args.port_broadcast)) - atexit_register_coroutine(log_receiver.close) + broadcast_clients = dict() + for target in "log", "ccb": + client = Receiver(target, []) + loop.run_until_complete(client.connect( + args.server, args.port_broadcast)) + atexit_register_coroutine(client.close) + broadcast_clients[target] = client # initialize main window main_window = MainWindow(args.server) @@ -152,9 +155,10 @@ def main(): rpc_clients["dataset_db"]) smgr.register(d_datasets) - d_applets = applets.AppletsDock(main_window, sub_clients["datasets"]) + d_applets = applets_ccb.AppletsCCBDock(main_window, sub_clients["datasets"]) atexit_register_coroutine(d_applets.stop) smgr.register(d_applets) + broadcast_clients["ccb"].notify_cbs.append(d_applets.ccb_notify) d_ttl_dds = moninj.MonInj() loop.run_until_complete(d_ttl_dds.start(args.server, args.port_notify)) @@ -166,7 +170,7 @@ def main(): logmgr = log.LogDockManager(main_window) smgr.register(logmgr) - log_receiver.notify_cbs.append(logmgr.append_message) + broadcast_clients["log"].notify_cbs.append(logmgr.append_message) widget_log_handler.callback = logmgr.append_message # lay out docks diff --git a/artiq/frontend/artiq_master.py b/artiq/frontend/artiq_master.py index 105ca612c..ae1e5820e 100755 --- a/artiq/frontend/artiq_master.py +++ b/artiq/frontend/artiq_master.py @@ -69,6 +69,13 @@ def main(): log_forwarder.callback = (lambda msg: server_broadcast.broadcast("log", msg)) + def ccb_issue(service, *args, **kwargs): + msg = { + "service": service, + "args": args, + "kwargs": kwargs + } + server_broadcast.broadcast("ccb", msg) device_db = DeviceDB(args.device_db) dataset_db = DatasetDB(args.dataset_db) @@ -96,7 +103,8 @@ def main(): "scheduler_delete": scheduler.delete, "scheduler_request_termination": scheduler.request_termination, "scheduler_get_status": scheduler.get_status, - "scheduler_check_pause": scheduler.check_pause + "scheduler_check_pause": scheduler.check_pause, + "ccb_issue": ccb_issue, }) experiment_db.scan_repository_async() diff --git a/artiq/frontend/artiq_run.py b/artiq/frontend/artiq_run.py index 79bb4dcb7..94e853edc 100755 --- a/artiq/frontend/artiq_run.py +++ b/artiq/frontend/artiq_run.py @@ -118,6 +118,12 @@ class DummyScheduler: pass +class DummyCCB: + def issue(self, service, *args, **kwargs): + logger.info("CCB for service '%s' (args %s, kwargs %s)", + service, args, kwargs) + + def get_argparser(with_file=True): parser = argparse.ArgumentParser( description="Local experiment running tool") @@ -183,7 +189,8 @@ def run(with_file=False): init_logger(args) device_mgr = DeviceManager(DeviceDB(args.device_db), - virtual_devices={"scheduler": DummyScheduler()}) + virtual_devices={"scheduler": DummyScheduler(), + "ccb": DummyCCB()}) dataset_db = DatasetDB(args.dataset_db) dataset_mgr = DatasetManager(dataset_db) diff --git a/artiq/master/worker_impl.py b/artiq/master/worker_impl.py index c52f09226..3c4003a21 100644 --- a/artiq/master/worker_impl.py +++ b/artiq/master/worker_impl.py @@ -106,6 +106,10 @@ class Scheduler: return self._check_pause(rid) +class CCB: + issue = staticmethod(make_parent_action("ccb_issue")) + + def get_exp(file, class_name): module = file_import(file, prefix="artiq_worker_") if class_name is None: @@ -189,7 +193,8 @@ def main(): repository_path = None device_mgr = DeviceManager(ParentDeviceDB, - virtual_devices={"scheduler": Scheduler()}) + virtual_devices={"scheduler": Scheduler(), + "ccb": CCB()}) dataset_mgr = DatasetManager(ParentDatasetDB) import_cache.install_hook()