From 741b11c26d59632e3efdaed47cdd894a78fd0461 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 09:59:15 +0100 Subject: [PATCH 01/11] applets: basic embedding OK --- artiq/applets/simple.py | 127 ++++++++++++++++------ artiq/frontend/artiq_gui.py | 21 ++-- artiq/gui/applets.py | 211 ++++++++++++++++-------------------- 3 files changed, 197 insertions(+), 162 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 933485c89..8fc5ceafc 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -4,7 +4,25 @@ import asyncio from quamash import QEventLoop, QtWidgets, QtGui, QtCore from artiq.protocols.sync_struct import Subscriber -from artiq.protocols.pc_rpc import Client +from artiq.protocols import pyon +from artiq.protocols.pipe_ipc import AsyncioChildComm + + +class AppletIPCClient(AsyncioChildComm): + def write_pyon(self, obj): + self.write(pyon.encode(obj).encode() + b"\n") + + async def read_pyon(self): + line = await self.readline() + return pyon.decode(line.decode()) + + async def embed(self, win_id): + self.write_pyon({"action": "embed", + "win_id": win_id}) + reply = await self.read_pyon() + if reply["action"] != "embed_done": + raise ValueError("Got erroneous reply to embed request", + reply) class SimpleApplet: @@ -13,27 +31,31 @@ class SimpleApplet: 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("data server") - group.add_argument( - "--server-notify", default="::1", - help="hostname or IP to connect to for dataset notifications") - group.add_argument( - "--port-notify", default=3250, type=int, - help="TCP port to connect to for dataset notifications") - group = self.argparser.add_argument_group("GUI server") - group.add_argument( - "--server-gui", default="::1", - help="hostname or IP to connect to for GUI control") - group.add_argument( - "--port-gui", default=6501, type=int, - help="TCP port to connect to for GUI control") - group.add_argument("--embed", default=None, type=int, - help="embed main widget into existing window") + self._arggroup_datasets = self.argparser.add_argument_group("datasets") + + subparsers = self.argparser.add_subparsers(dest="mode") + subparsers.required = True + + parser_sa = subparsers.add_parser("standalone", + help="run standalone, connect to master directly") + parser_sa.add_argument( + "--server", default="::1", + help="hostname or IP to connect to") + parser_sa.add_argument( + "--port", default=3250, type=int, + help="TCP port to connect to") + + parser_em = subparsers.add_parser("embedded", + help="embed into GUI") + parser_em.add_argument("ipc_address", + help="address for pipe_ipc") + self.dataset_args = set() def add_dataset(self, name, help=None, required=True): @@ -56,6 +78,25 @@ class SimpleApplet: self.loop = QEventLoop(app) asyncio.set_event_loop(self.loop) + def ipc_init(self): + if self.args.mode == "standalone": + # nothing to do + pass + elif self.args.mode == "embedded": + self.ipc = AppletIPCClient(self.args.ipc_address) + self.loop.run_until_complete(self.ipc.connect()) + else: + raise NotImplementedError + + def ipc_close(self): + if self.args.mode == "standalone": + # nothing to do + pass + elif self.args.mode == "embedded": + self.ipc.close() + else: + raise NotImplementedError + def create_main_widget(self): self.main_widget = self.main_widget_class(self.args) # Qt window embedding is ridiculously buggy, and empirical testing @@ -65,15 +106,10 @@ class SimpleApplet: # 3. applet sends the ID to host, host embeds the widget # 4. applet shows the widget # Doing embedding the other way around (using QWindow.setParent in the - # applet) breaks resizing; furthermore the host needs to know our - # window ID to request graceful termination by closing the window. - if self.args.embed is not None: + # applet) breaks resizing. + if self.args.mode == "embedded": win_id = int(self.main_widget.winId()) - remote = Client(self.args.server_gui, self.args.port_gui, "applets") - try: - remote.embed(self.args.embed, win_id) - finally: - remote.close_rpc() + self.loop.run_until_complete(self.ipc.embed(win_id)) self.main_widget.show() def sub_init(self, data): @@ -81,6 +117,10 @@ class SimpleApplet: return data def filter_mod(self, mod): + if self.args.mode == "embedded": + # the parent already filters for us + return True + if mod["action"] == "init": return True if mod["path"]: @@ -108,21 +148,40 @@ class SimpleApplet: else: self.main_widget.data_changed(self.data, [mod]) - def create_subscriber(self): - self.subscriber = Subscriber("datasets", - self.sub_init, self.sub_mod) - self.loop.run_until_complete(self.subscriber.connect( - self.args.server_notify, self.args.port_notify)) + def subscribe(self): + if self.args.mode == "standalone": + self.subscriber = Subscriber("datasets", + self.sub_init, self.sub_mod) + self.loop.run_until_complete(self.subscriber.connect( + self.args.server_notify, self.args.port_notify)) + elif self.args.mode == "embedded": + # TODO + pass + else: + raise NotImplementedError + + def unsubscribe(self): + if self.args.mode == "standalone": + self.loop.run_until_complete(self.subscriber.close()) + elif self.args.mode == "embedded": + # nothing to do + pass + else: + raise NotImplementedError def run(self): self.args_init() self.quamash_init() try: - self.create_main_widget() - self.create_subscriber() + self.ipc_init() try: - self.loop.run_forever() + self.create_main_widget() + self.subscribe() + try: + self.loop.run_forever() + finally: + self.unsubscribe() finally: - self.loop.run_until_complete(self.subscriber.close()) + self.ipc_close() finally: self.loop.close() diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 06de89683..d4e36445b 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -12,7 +12,7 @@ from pyqtgraph import dockarea from artiq import __artiq_dir__ as artiq_dir from artiq.tools import * -from artiq.protocols.pc_rpc import AsyncioClient, Server +from artiq.protocols.pc_rpc import AsyncioClient from artiq.gui.models import ModelSubscriber from artiq.gui import (state, experiments, shortcuts, explorer, moninj, datasets, applets, schedule, log, console) @@ -111,9 +111,9 @@ def main(): d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) - appletmgr = applets.AppletManager(dock_area) - atexit_register_coroutine(appletmgr.stop) - smgr.register(appletmgr) + d_applets = applets.AppletsDock(dock_area) + atexit_register_coroutine(d_applets.stop) + smgr.register(d_applets) if os.name != "nt": d_ttl_dds = moninj.MonInj() @@ -133,11 +133,11 @@ def main(): if os.name != "nt": dock_area.addDock(d_ttl_dds.dds_dock, "top") dock_area.addDock(d_ttl_dds.ttl_dock, "above", d_ttl_dds.dds_dock) - dock_area.addDock(appletmgr.main_dock, "above", d_ttl_dds.ttl_dock) - dock_area.addDock(d_datasets, "above", appletmgr.main_dock) + dock_area.addDock(d_applets, "above", d_ttl_dds.ttl_dock) + dock_area.addDock(d_datasets, "above", d_applets) else: - dock_area.addDock(appletmgr.main_dock, "top") - dock_area.addDock(d_datasets, "above", appletmgr.main_dock) + dock_area.addDock(d_applets, "top") + dock_area.addDock(d_datasets, "above", d_applets) dock_area.addDock(d_shortcuts, "above", d_datasets) dock_area.addDock(d_explorer, "above", d_shortcuts) dock_area.addDock(d_console, "bottom") @@ -153,11 +153,6 @@ def main(): if d_log0 is not None: dock_area.addDock(d_log0, "right", d_explorer) - # start RPC server - rpc_server = Server({"applets": appletmgr.rpc}) - loop.run_until_complete(rpc_server.start("::1", 6501)) - atexit_register_coroutine(rpc_server.stop) - # run win.show() loop.run_until_complete(win.exit_request.wait()) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 87a40516b..755491f17 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -7,17 +7,47 @@ from functools import partial from quamash import QtCore, QtGui, QtWidgets from pyqtgraph import dockarea +from artiq.protocols import pyon +from artiq.protocols.pipe_ipc import AsyncioParentComm + logger = logging.getLogger(__name__) +class AppletIPCServer(AsyncioParentComm): + def __init__(self, capture_cb): + AsyncioParentComm.__init__(self) + self.capture_cb = capture_cb + + def write_pyon(self, obj): + self.write(pyon.encode(obj).encode() + b"\n") + + async def read_pyon(self): + line = await self.readline() + return pyon.decode(line.decode()) + + async def serve(self): + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "embed": + self.capture_cb(obj["win_id"]) + self.write_pyon({"action": "embed_done"}) + else: + raise ValueError("unknown action in applet request") + except: + logger.warning("error processing applet request", + exc_info=True) + self.write_pyon({"action": "error"}) + + class AppletDock(dockarea.Dock): - def __init__(self, token, name, command): - dockarea.Dock.__init__(self, "applet" + str(token), + def __init__(self, name, command): + dockarea.Dock.__init__(self, "applet" + str(id(self)), # XXX label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) - self.token = token self.applet_name = name self.command = command @@ -26,41 +56,38 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): + self.ipc = AppletIPCServer(self.capture) command = self.command.format(python=sys.executable, - embed_token=self.token) + ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) try: - self.process = await asyncio.create_subprocess_exec( - *shlex.split(command)) + await self.ipc.create_subprocess(*shlex.split(command)) except: logger.warning("Applet %s failed to start", self.applet_name, exc_info=True) + asyncio.ensure_future(self.ipc.serve()) def capture(self, win_id): logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) - self.captured_window = QtGui.QWindow.fromWinId(win_id) - self.captured_widget = QtWidgets.QWidget.createWindowContainer( - self.captured_window) - self.addWidget(self.captured_widget) + captured_window = QtGui.QWindow.fromWinId(win_id) + captured_widget = QtWidgets.QWidget.createWindowContainer( + captured_window) + self.addWidget(captured_widget) async def terminate(self): - if hasattr(self, "captured_window"): - self.captured_window.close() - self.captured_widget.deleteLater() - del self.captured_window - del self.captured_widget if hasattr(self, "process"): + # TODO: send IPC termination request try: - await asyncio.wait_for(self.process.wait(), 2.0) + await asyncio.wait_for(self.ipc.process.wait(), 2.0) except: logger.warning("Applet %s failed to exit, killing", self.applet_name) try: - self.process.kill() + self.ipc.process.kill() except ProcessLookupError: pass - await self.process.wait() - del self.process + await self.ipc.process.wait() + del self.ipc async def restart(self): await self.terminate() @@ -69,24 +96,25 @@ class AppletDock(dockarea.Dock): _templates = [ ("Big number", "{python} -m artiq.applets.big_number " - "--embed {embed_token} NUMBER_DATASET"), + "embedded {ipc_address} NUMBER_DATASET"), ("Histogram", "{python} -m artiq.applets.plot_hist " - "--embed {embed_token} COUNTS_DATASET " + "embedded {ipc_address} COUNTS_DATASET " "--x BIN_BOUNDARIES_DATASET"), ("XY", "{python} -m artiq.applets.plot_xy " - "--embed {embed_token} Y_DATASET --x X_DATASET " + "embedded {ipc_address} Y_DATASET --x X_DATASET " "--error ERROR_DATASET --fit FIT_DATASET"), ("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist " - "--embed {embed_token} X_DATASET " + "embedded {ipc_address} X_DATASET " "HIST_BIN_BOUNDARIES_DATASET " "HISTS_COUNTS_DATASET"), ] class AppletsDock(dockarea.Dock): - def __init__(self, manager): - self.manager = manager - self.token_to_checkbox = dict() + def __init__(self, dock_area): + self.dock_area = dock_area + self.dock_to_checkbox = dict() + self.workaround_pyqtgraph_bug = False dockarea.Dock.__init__(self, "Applets") self.setMinimumSize(QtCore.QSize(850, 450)) @@ -129,6 +157,18 @@ class AppletsDock(dockarea.Dock): self.table.cellChanged.connect(self.cell_changed) + def create(self, name, command): + dock = AppletDock(name, command) + # If a dock is floated and then dock state is restored, pyqtgraph + # leaves a "phantom" window open. + if self.workaround_pyqtgraph_bug: + self.dock_area.addDock(dock) + else: + self.dock_area.floatDock(dock) + asyncio.ensure_future(dock.start()) + dock.sigClosed.connect(partial(self.on_dock_closed, dock)) + return dock + def cell_changed(self, row, column): if column == 0: item = self.table.item(row, column) @@ -141,27 +181,28 @@ class AppletsDock(dockarea.Dock): name = "" else: name = name.text() - token = self.manager.create(name, command) - item.applet_token = token - self.token_to_checkbox[token] = item + dock = self.create(name, command) + item.applet_dock = dock + self.dock_to_checkbox[dock] = item else: - token = getattr(item, "applet_token", None) - if token is not None: - # cell_changed is emitted at row creation - self.manager.delete(token) + dock = getattr(item, "applet_dock", None) + if dock is not None: + # This calls self.on_dock_closed + dock.close() elif column == 1 or column == 2: new_value = self.table.item(row, column).text() - token = getattr(self.table.item(row, 0), "applet_token", None) - if token is not None: + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: if column == 1: - self.manager.rename(token, new_value) + dock.rename(new_value) else: - self.manager.set_command(token, new_value) + dock.command = new_value - def disable_token(self, token): - checkbox_item = self.token_to_checkbox[token] - checkbox_item.applet_token = None - del self.token_to_checkbox[token] + def on_dock_closed(self, dock): + asyncio.ensure_future(dock.terminate()) + checkbox_item = self.dock_to_checkbox[dock] + checkbox_item.applet_dock = None + del self.dock_to_checkbox[dock] checkbox_item.setCheckState(QtCore.Qt.Unchecked) def new(self): @@ -185,19 +226,26 @@ class AppletsDock(dockarea.Dock): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - token = getattr(self.table.item(row, 0), "applet_token", None) - if token is not None: - asyncio.ensure_future(self.manager.restart(token)) + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: + asyncio.ensure_future(dock.restart()) def delete(self): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - token = getattr(self.table.item(row, 0), "applet_token", None) - if token is not None: - self.manager.delete(token) + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: + # This calls self.on_dock_closed + dock.close() self.table.removeRow(row) + async def stop(self): + for row in range(self.table.rowCount()): + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: + await dock.terminate() + def save_state(self): state = [] for row in range(self.table.rowCount()): @@ -208,6 +256,7 @@ class AppletsDock(dockarea.Dock): return state def restore_state(self, state): + self.workaround_pyqtgraph_bug = True for enabled, name, command in state: row = self.new() item = QtWidgets.QTableWidgetItem() @@ -218,72 +267,4 @@ class AppletsDock(dockarea.Dock): self.table.setItem(row, 2, item) if enabled: self.table.item(row, 0).setCheckState(QtCore.Qt.Checked) - - -class AppletManagerRPC: - def __init__(self, parent): - self.parent = parent - - def embed(self, token, win_id): - self.parent.embed(token, win_id) - - -class AppletManager: - def __init__(self, dock_area): - self.dock_area = dock_area - self.main_dock = AppletsDock(self) - self.rpc = AppletManagerRPC(self) - self.applet_docks = dict() - self.workaround_pyqtgraph_bug = False - - def embed(self, token, win_id): - if token not in self.applet_docks: - logger.warning("Ignored incorrect embed token %d for winid 0x%x", - token, win_id) - return - self.applet_docks[token].capture(win_id) - - def create(self, name, command): - token = next(iter(set(range(len(self.applet_docks) + 1)) - - self.applet_docks.keys())) - dock = AppletDock(token, name, command) - self.applet_docks[token] = dock - # If a dock is floated and then dock state is restored, pyqtgraph - # leaves a "phantom" window open. - if self.workaround_pyqtgraph_bug: - self.dock_area.addDock(dock) - else: - self.dock_area.floatDock(dock) - asyncio.ensure_future(dock.start()) - dock.sigClosed.connect(partial(self.on_dock_closed, token)) - return token - - def on_dock_closed(self, token): - asyncio.ensure_future(self.applet_docks[token].terminate()) - self.main_dock.disable_token(token) - del self.applet_docks[token] - - def delete(self, token): - # This in turns calls on_dock_closed and main_dock.disable_token - self.applet_docks[token].close() - - def rename(self, token, name): - self.applet_docks[token].rename(name) - - def set_command(self, token, command): - self.applet_docks[token].command = command - - async def restart(self, token): - await self.applet_docks[token].restart() - - async def stop(self): - for dock in self.applet_docks.values(): - await dock.terminate() - - def save_state(self): - return self.main_dock.save_state() - - def restore_state(self, state): - self.workaround_pyqtgraph_bug = True - self.main_dock.restore_state(state) self.workaround_pyqtgraph_bug = False From 8a912105cb2441d0f6a5602e2c2e279d3bc20610 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 14:05:00 +0100 Subject: [PATCH 02/11] protocols/pyon: remove FlatFileDB --- artiq/protocols/pyon.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/artiq/protocols/pyon.py b/artiq/protocols/pyon.py index 238ff2198..b7abfa93a 100644 --- a/artiq/protocols/pyon.py +++ b/artiq/protocols/pyon.py @@ -202,23 +202,3 @@ def load_file(filename): """Parses the specified file and returns the decoded Python object.""" with open(filename, "r") as f: return decode(f.read()) - - -class FlatFileDB: - def __init__(self, filename): - self.filename = filename - self.data = pyon.load_file(self.filename) - - def save(self): - pyon.store_file(self.filename, self.data) - - def get(self, key): - return self.data[key] - - def set(self, key, value): - self.data[key] = value - self.save() - - def delete(self, key): - del self.data[key] - self.save() From 8be0696b39cb68d404be59d36f5ef5fa519b47bc Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 14:08:14 +0100 Subject: [PATCH 03/11] protocols/pyon: set support --- artiq/protocols/pyon.py | 15 +++++++++------ artiq/test/serialization.py | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/artiq/protocols/pyon.py b/artiq/protocols/pyon.py index b7abfa93a..10b5412aa 100644 --- a/artiq/protocols/pyon.py +++ b/artiq/protocols/pyon.py @@ -35,6 +35,7 @@ _encode_map = { bytes: "bytes", tuple: "tuple", list: "list", + set: "set", dict: "dict", wrapping_int: "number", Fraction: "fraction", @@ -98,6 +99,12 @@ class _Encoder: r += "]" return r + def encode_set(self, x): + r = "{" + r += ", ".join([self.encode(item) for item in x]) + r += "}" + return r + def encode_dict(self, x): r = "{" if not self.pretty or len(x) < 2: @@ -149,9 +156,7 @@ class _Encoder: def encode(x, pretty=False): """Serializes a Python object and returns the corresponding string in - Python syntax. - - """ + Python syntax.""" return _Encoder(pretty).encode(x) @@ -181,9 +186,7 @@ _eval_dict = { def decode(s): """Parses a string in the Python syntax, reconstructs the corresponding - object, and returns it. - - """ + object, and returns it.""" return eval(s, _eval_dict, {}) diff --git a/artiq/test/serialization.py b/artiq/test/serialization.py index 88cf9b18e..9c23481b9 100644 --- a/artiq/test/serialization.py +++ b/artiq/test/serialization.py @@ -10,6 +10,7 @@ from artiq.protocols import pyon _pyon_test_object = { (1, 2): [(3, 4.2), (2, )], Fraction(3, 4): np.linspace(5, 10, 1), + {"testing", "sets"}, "a": np.int8(9), "b": np.int16(-98), "c": np.int32(42), "d": np.int64(-5), "e": np.uint8(8), "f": np.uint16(5), "g": np.uint32(4), "h": np.uint64(9), "x": np.float16(9.0), "y": np.float32(9.0), "z": np.float64(9.0), From 8844fba4c98b5ed518a1beed64dc3bd7229e7af1 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 14:35:37 +0100 Subject: [PATCH 04/11] applets: clean shutdown --- artiq/applets/simple.py | 42 ++++++++++++++++++++++--- artiq/gui/applets.py | 69 ++++++++++++++++++++++++----------------- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 8fc5ceafc..24a76e464 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -1,3 +1,4 @@ +import logging import argparse import asyncio @@ -8,7 +9,13 @@ from artiq.protocols import pyon from artiq.protocols.pipe_ipc import AsyncioChildComm +logger = logging.getLogger(__name__) + + class AppletIPCClient(AsyncioChildComm): + def set_close_cb(self, close_cb): + self.close_cb = close_cb + def write_pyon(self, obj): self.write(pyon.encode(obj).encode() + b"\n") @@ -17,12 +24,37 @@ class AppletIPCClient(AsyncioChildComm): return pyon.decode(line.decode()) async def embed(self, win_id): + # This function is only called when not subscribed to anything, + # so the only normal replies are embed_done and terminate. self.write_pyon({"action": "embed", "win_id": win_id}) reply = await self.read_pyon() - if reply["action"] != "embed_done": - raise ValueError("Got erroneous reply to embed request", - reply) + if reply["action"] == "terminate": + self.close_cb() + elif reply["action"] != "embed_done": + logger.error("unexpected action reply to embed request: %s", + action) + self.close_cb() + + async def listen(self): + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "terminate": + self.close_cb() + return + else: + raise ValueError("unknown action in applet request") + except: + logger.error("error processing applet request", + exc_info=True) + self.close_cb() + + def subscribe(self, datasets): + self.write_pyon({"action": "subscribe", + "datasets": datasets}) + asyncio.ensure_future(self.listen()) class SimpleApplet: @@ -108,6 +140,7 @@ class SimpleApplet: # Doing embedding the other way around (using QWindow.setParent in the # applet) breaks resizing. if self.args.mode == "embedded": + self.ipc.set_close_cb(self.main_widget.close) win_id = int(self.main_widget.winId()) self.loop.run_until_complete(self.ipc.embed(win_id)) self.main_widget.show() @@ -155,8 +188,7 @@ class SimpleApplet: self.loop.run_until_complete(self.subscriber.connect( self.args.server_notify, self.args.port_notify)) elif self.args.mode == "embedded": - # TODO - pass + self.ipc.subscribe(self.datasets) else: raise NotImplementedError diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 755491f17..909069796 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -7,18 +7,14 @@ from functools import partial from quamash import QtCore, QtGui, QtWidgets from pyqtgraph import dockarea -from artiq.protocols import pyon from artiq.protocols.pipe_ipc import AsyncioParentComm +from artiq.protocols import pyon logger = logging.getLogger(__name__) class AppletIPCServer(AsyncioParentComm): - def __init__(self, capture_cb): - AsyncioParentComm.__init__(self) - self.capture_cb = capture_cb - def write_pyon(self, obj): self.write(pyon.encode(obj).encode() + b"\n") @@ -26,25 +22,40 @@ class AppletIPCServer(AsyncioParentComm): line = await self.readline() return pyon.decode(line.decode()) - async def serve(self): - while True: - obj = await self.read_pyon() - try: - action = obj["action"] - if action == "embed": - self.capture_cb(obj["win_id"]) - self.write_pyon({"action": "embed_done"}) - else: - raise ValueError("unknown action in applet request") - except: - logger.warning("error processing applet request", - exc_info=True) - self.write_pyon({"action": "error"}) + async def serve(self, embed_cb): + try: + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "embed": + embed_cb(obj["win_id"]) + self.write_pyon({"action": "embed_done"}) + elif action == "subscribe": + print("applet subscribed: ", obj["datasets"]) + else: + raise ValueError("unknown action in applet request") + except: + logger.warning("error processing applet request", + exc_info=True) + self.write_pyon({"action": "error"}) + except asyncio.CancelledError: + pass + except: + logger.error("error processing data from applet, " + "server stopped", exc_info=True) + + def start(self, embed_cb): + self.server_task = asyncio.ensure_future(self.serve(embed_cb)) + + async def stop(self): + self.server_task.cancel() + await asyncio.wait([self.server_task]) class AppletDock(dockarea.Dock): def __init__(self, name, command): - dockarea.Dock.__init__(self, "applet" + str(id(self)), # XXX + dockarea.Dock.__init__(self, "applet" + str(id(self)), # TODO label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) @@ -56,7 +67,7 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): - self.ipc = AppletIPCServer(self.capture) + self.ipc = AppletIPCServer() command = self.command.format(python=sys.executable, ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) @@ -65,18 +76,18 @@ class AppletDock(dockarea.Dock): except: logger.warning("Applet %s failed to start", self.applet_name, exc_info=True) - asyncio.ensure_future(self.ipc.serve()) + self.ipc.start(self.embed) - def capture(self, win_id): + def embed(self, win_id): logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) - captured_window = QtGui.QWindow.fromWinId(win_id) - captured_widget = QtWidgets.QWidget.createWindowContainer( - captured_window) - self.addWidget(captured_widget) + embed_window = QtGui.QWindow.fromWinId(win_id) + embed_widget = QtWidgets.QWidget.createWindowContainer(embed_window) + self.addWidget(embed_widget) async def terminate(self): - if hasattr(self, "process"): - # TODO: send IPC termination request + if hasattr(self, "ipc"): + await self.ipc.stop() + self.ipc.write_pyon({"action": "terminate"}) try: await asyncio.wait_for(self.ipc.process.wait(), 2.0) except: From f25b5442e76cb3f0f0eadc6145ae1f679db64472 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 16:26:02 +0100 Subject: [PATCH 05/11] applets: properly name docks to support state save/restore --- artiq/gui/applets.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 909069796..626195d87 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -54,8 +54,8 @@ class AppletIPCServer(AsyncioParentComm): class AppletDock(dockarea.Dock): - def __init__(self, name, command): - dockarea.Dock.__init__(self, "applet" + str(id(self)), # TODO + def __init__(self, uid, name, command): + dockarea.Dock.__init__(self, "applet" + str(uid), label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) @@ -125,6 +125,7 @@ class AppletsDock(dockarea.Dock): def __init__(self, dock_area): self.dock_area = dock_area self.dock_to_checkbox = dict() + self.applet_uids = set() self.workaround_pyqtgraph_bug = False dockarea.Dock.__init__(self, "Applets") @@ -168,8 +169,8 @@ class AppletsDock(dockarea.Dock): self.table.cellChanged.connect(self.cell_changed) - def create(self, name, command): - dock = AppletDock(name, command) + def create(self, uid, name, command): + dock = AppletDock(uid, name, command) # If a dock is floated and then dock state is restored, pyqtgraph # leaves a "phantom" window open. if self.workaround_pyqtgraph_bug: @@ -192,17 +193,17 @@ class AppletsDock(dockarea.Dock): name = "" else: name = name.text() - dock = self.create(name, command) + dock = self.create(item.applet_uid, name, command) item.applet_dock = dock self.dock_to_checkbox[dock] = item else: - dock = getattr(item, "applet_dock", None) + dock = item.applet_dock if dock is not None: # This calls self.on_dock_closed dock.close() elif column == 1 or column == 2: new_value = self.table.item(row, column).text() - dock = getattr(self.table.item(row, 0), "applet_dock", None) + dock = self.table.item(row, 0).applet_dock if dock is not None: if column == 1: dock.rename(new_value) @@ -217,6 +218,10 @@ class AppletsDock(dockarea.Dock): checkbox_item.setCheckState(QtCore.Qt.Unchecked) def new(self): + uid = next(iter(set(range(len(self.applet_uids) + 1)) + - self.applet_uids)) + self.applet_uids.add(uid) + row = self.table.rowCount() self.table.insertRow(row) checkbox = QtWidgets.QTableWidgetItem() @@ -224,6 +229,8 @@ class AppletsDock(dockarea.Dock): QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled) checkbox.setCheckState(QtCore.Qt.Unchecked) + checkbox.applet_uid = uid + checkbox.applet_dock = None self.table.setItem(row, 0, checkbox) self.table.setItem(row, 1, QtWidgets.QTableWidgetItem()) self.table.setItem(row, 2, QtWidgets.QTableWidgetItem()) @@ -237,7 +244,7 @@ class AppletsDock(dockarea.Dock): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - dock = getattr(self.table.item(row, 0), "applet_dock", None) + dock = self.table.item(row, 0).applet_dock if dock is not None: asyncio.ensure_future(dock.restart()) @@ -245,15 +252,18 @@ class AppletsDock(dockarea.Dock): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - dock = getattr(self.table.item(row, 0), "applet_dock", None) + item = self.table.item(row, 0) + dock = item.applet_dock if dock is not None: # This calls self.on_dock_closed dock.close() + self.applet_uids.remove(item.applet_uid) self.table.removeRow(row) + async def stop(self): for row in range(self.table.rowCount()): - dock = getattr(self.table.item(row, 0), "applet_dock", None) + dock = self.table.item(row, 0).applet_dock if dock is not None: await dock.terminate() From de99e7f830f9e1cc9a2c4dea5f5cd898559e42de Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:20:07 +0100 Subject: [PATCH 06/11] applets: handle dataset mutations --- artiq/applets/simple.py | 16 ++++++++++++--- artiq/frontend/artiq_gui.py | 2 +- artiq/gui/applets.py | 41 ++++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 24a76e464..7da0c0a31 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -4,7 +4,7 @@ import asyncio from quamash import QEventLoop, QtWidgets, QtGui, QtCore -from artiq.protocols.sync_struct import Subscriber +from artiq.protocols.sync_struct import Subscriber, process_mod from artiq.protocols import pyon from artiq.protocols.pipe_ipc import AsyncioChildComm @@ -37,6 +37,7 @@ class AppletIPCClient(AsyncioChildComm): self.close_cb() async def listen(self): + data = None while True: obj = await self.read_pyon() try: @@ -44,6 +45,13 @@ class AppletIPCClient(AsyncioChildComm): if action == "terminate": self.close_cb() return + elif action == "mod": + mod = obj["mod"] + if mod["action"] == "init": + data = self.init_cb(mod["struct"]) + else: + process_mod(data, mod) + self.mod_cb(mod) else: raise ValueError("unknown action in applet request") except: @@ -51,9 +59,11 @@ class AppletIPCClient(AsyncioChildComm): exc_info=True) self.close_cb() - def subscribe(self, datasets): + def subscribe(self, datasets, init_cb, mod_cb): self.write_pyon({"action": "subscribe", "datasets": datasets}) + self.init_cb = init_cb + self.mod_cb = mod_cb asyncio.ensure_future(self.listen()) @@ -188,7 +198,7 @@ class SimpleApplet: self.loop.run_until_complete(self.subscriber.connect( self.args.server_notify, self.args.port_notify)) elif self.args.mode == "embedded": - self.ipc.subscribe(self.datasets) + self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) else: raise NotImplementedError diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index d4e36445b..ddbcc0c48 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -111,7 +111,7 @@ def main(): d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) - d_applets = applets.AppletsDock(dock_area) + d_applets = applets.AppletsDock(dock_area, sub_clients["datasets"]) atexit_register_coroutine(d_applets.stop) smgr.register(d_applets) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 626195d87..6df993804 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -15,6 +15,11 @@ logger = logging.getLogger(__name__) class AppletIPCServer(AsyncioParentComm): + def __init__(self, datasets_sub): + AsyncioParentComm.__init__(self) + self.datasets_sub = datasets_sub + self.datasets = set() + def write_pyon(self, obj): self.write(pyon.encode(obj).encode() + b"\n") @@ -22,7 +27,25 @@ class AppletIPCServer(AsyncioParentComm): line = await self.readline() return pyon.decode(line.decode()) + def _synthesize_init(self, data): + struct = {k: v for k, v in data.items() if k in self.datasets} + return {"action": "init", + "struct": struct} + + def _on_mod(self, mod): + if mod["action"] == "init": + mod = self._synthesize_init(mod["struct"]) + else: + if mod["path"]: + if mod["path"][0] not in self.datasets: + return + elif mod["action"] in {"setitem", "delitem"}: + if mod["key"] not in self.datasets: + return + self.write_pyon({"action": "mod", "mod": mod}) + async def serve(self, embed_cb): + self.datasets_sub.notify_cbs.append(self._on_mod) try: while True: obj = await self.read_pyon() @@ -32,7 +55,11 @@ class AppletIPCServer(AsyncioParentComm): embed_cb(obj["win_id"]) self.write_pyon({"action": "embed_done"}) elif action == "subscribe": - print("applet subscribed: ", obj["datasets"]) + self.datasets = obj["datasets"] + if self.datasets_sub.model is not None: + mod = self._synthesize_init( + self.datasets_sub.model.backing_store) + self.write_pyon({"action": "mod", "mod": mod}) else: raise ValueError("unknown action in applet request") except: @@ -44,6 +71,8 @@ class AppletIPCServer(AsyncioParentComm): except: logger.error("error processing data from applet, " "server stopped", exc_info=True) + finally: + self.datasets_sub.notify_cbs.remove(self._on_mod) def start(self, embed_cb): self.server_task = asyncio.ensure_future(self.serve(embed_cb)) @@ -54,11 +83,12 @@ class AppletIPCServer(AsyncioParentComm): class AppletDock(dockarea.Dock): - def __init__(self, uid, name, command): + def __init__(self, datasets_sub, uid, name, command): dockarea.Dock.__init__(self, "applet" + str(uid), label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) + self.datasets_sub = datasets_sub self.applet_name = name self.command = command @@ -67,7 +97,7 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): - self.ipc = AppletIPCServer() + self.ipc = AppletIPCServer(self.datasets_sub) command = self.command.format(python=sys.executable, ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) @@ -122,8 +152,9 @@ _templates = [ class AppletsDock(dockarea.Dock): - def __init__(self, dock_area): + def __init__(self, dock_area, datasets_sub): self.dock_area = dock_area + self.datasets_sub = datasets_sub self.dock_to_checkbox = dict() self.applet_uids = set() self.workaround_pyqtgraph_bug = False @@ -170,7 +201,7 @@ class AppletsDock(dockarea.Dock): self.table.cellChanged.connect(self.cell_changed) def create(self, uid, name, command): - dock = AppletDock(uid, name, command) + dock = AppletDock(self.datasets_sub, uid, name, command) # If a dock is floated and then dock state is restored, pyqtgraph # leaves a "phantom" window open. if self.workaround_pyqtgraph_bug: From 7584b02d6638065384a4c652b9d377594bba1609 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:23:50 +0100 Subject: [PATCH 07/11] applets: fix error message text --- artiq/applets/simple.py | 4 ++-- artiq/gui/applets.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 7da0c0a31..0acc18360 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -53,9 +53,9 @@ class AppletIPCClient(AsyncioChildComm): process_mod(data, mod) self.mod_cb(mod) else: - raise ValueError("unknown action in applet request") + raise ValueError("unknown action in parent message") except: - logger.error("error processing applet request", + logger.error("error processing parent message", exc_info=True) self.close_cb() diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 6df993804..f69c7d0f1 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -61,9 +61,9 @@ class AppletIPCServer(AsyncioParentComm): self.datasets_sub.model.backing_store) self.write_pyon({"action": "mod", "mod": mod}) else: - raise ValueError("unknown action in applet request") + raise ValueError("unknown action in applet message") except: - logger.warning("error processing applet request", + logger.warning("error processing applet message", exc_info=True) self.write_pyon({"action": "error"}) except asyncio.CancelledError: From 4733c4ba1e70db26d9b86756a238d02b9a4bcf03 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 21:57:07 +0100 Subject: [PATCH 08/11] gui/applets: save dock UID in state --- artiq/gui/applets.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index f69c7d0f1..6281e70a4 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -248,9 +248,10 @@ class AppletsDock(dockarea.Dock): del self.dock_to_checkbox[dock] checkbox_item.setCheckState(QtCore.Qt.Unchecked) - def new(self): - uid = next(iter(set(range(len(self.applet_uids) + 1)) - - self.applet_uids)) + def new(self, uid=None): + if uid is None: + uid = next(iter(set(range(len(self.applet_uids) + 1)) + - self.applet_uids)) self.applet_uids.add(uid) row = self.table.rowCount() @@ -301,16 +302,17 @@ class AppletsDock(dockarea.Dock): def save_state(self): state = [] for row in range(self.table.rowCount()): + uid = self.table.item(row, 0).applet_uid enabled = self.table.item(row, 0).checkState() == QtCore.Qt.Checked name = self.table.item(row, 1).text() command = self.table.item(row, 2).text() - state.append((enabled, name, command)) + state.append((uid, enabled, name, command)) return state def restore_state(self, state): self.workaround_pyqtgraph_bug = True - for enabled, name, command in state: - row = self.new() + for uid, enabled, name, command in state: + row = self.new(uid) item = QtWidgets.QTableWidgetItem() item.setText(name) self.table.setItem(row, 1, item) From 1a2596de8e5e0371744ded237035038028563c34 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:24:23 +0100 Subject: [PATCH 09/11] examples/histogram: artiq -> artiq.experiment --- examples/master/repository/histograms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/master/repository/histograms.py b/examples/master/repository/histograms.py index c0c660708..b8c283712 100644 --- a/examples/master/repository/histograms.py +++ b/examples/master/repository/histograms.py @@ -2,7 +2,7 @@ from time import sleep import numpy as np -from artiq import * +from artiq.experiment import * class Histograms(EnvExperiment): From 67327b1e618c8ce4c59d146f5179a30fe0be5d1c Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:25:02 +0100 Subject: [PATCH 10/11] applets: avoid argparse subparser mess --- artiq/applets/simple.py | 59 +++++++++++++---------------------------- artiq/gui/applets.py | 8 +++--- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 0acc18360..75aec014e 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -79,24 +79,20 @@ class SimpleApplet: help="time to wait after a mod (buffering other mods) " "before updating (default: %(default).2f)") - self._arggroup_datasets = self.argparser.add_argument_group("datasets") - - subparsers = self.argparser.add_subparsers(dest="mode") - subparsers.required = True - - parser_sa = subparsers.add_parser("standalone", - help="run standalone, connect to master directly") - parser_sa.add_argument( + group = self.argparser.add_argument_group("standalone mode (default)") + group.add_argument( "--server", default="::1", - help="hostname or IP to connect to") - parser_sa.add_argument( + 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") - parser_em = subparsers.add_parser("embedded", - help="embed into GUI") - parser_em.add_argument("ipc_address", - help="address for pipe_ipc") + self.argparser.add_argument("--embed", default=None, + help="embed into GUI", metavar="IPC_ADDRESS") + + self._arggroup_datasets = self.argparser.add_argument_group("datasets") self.dataset_args = set() @@ -121,23 +117,13 @@ class SimpleApplet: asyncio.set_event_loop(self.loop) def ipc_init(self): - if self.args.mode == "standalone": - # nothing to do - pass - elif self.args.mode == "embedded": - self.ipc = AppletIPCClient(self.args.ipc_address) + if self.args.embed is not None: + self.ipc = AppletIPCClient(self.args.embed) self.loop.run_until_complete(self.ipc.connect()) - else: - raise NotImplementedError def ipc_close(self): - if self.args.mode == "standalone": - # nothing to do - pass - elif self.args.mode == "embedded": + if self.args.embed is not None: self.ipc.close() - else: - raise NotImplementedError def create_main_widget(self): self.main_widget = self.main_widget_class(self.args) @@ -149,7 +135,7 @@ class SimpleApplet: # 4. applet shows the widget # Doing embedding the other way around (using QWindow.setParent in the # applet) breaks resizing. - if self.args.mode == "embedded": + if self.args.embed is not None: self.ipc.set_close_cb(self.main_widget.close) win_id = int(self.main_widget.winId()) self.loop.run_until_complete(self.ipc.embed(win_id)) @@ -160,7 +146,7 @@ class SimpleApplet: return data def filter_mod(self, mod): - if self.args.mode == "embedded": + if self.args.embed is not None: # the parent already filters for us return True @@ -192,24 +178,17 @@ class SimpleApplet: self.main_widget.data_changed(self.data, [mod]) def subscribe(self): - if self.args.mode == "standalone": + if self.args.embed is None: self.subscriber = Subscriber("datasets", self.sub_init, self.sub_mod) self.loop.run_until_complete(self.subscriber.connect( - self.args.server_notify, self.args.port_notify)) - elif self.args.mode == "embedded": - self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) + self.args.server, self.args.port)) else: - raise NotImplementedError + self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) def unsubscribe(self): - if self.args.mode == "standalone": + if self.args.embed is None: self.loop.run_until_complete(self.subscriber.close()) - elif self.args.mode == "embedded": - # nothing to do - pass - else: - raise NotImplementedError def run(self): self.args_init() diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 6281e70a4..868842018 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -137,15 +137,15 @@ class AppletDock(dockarea.Dock): _templates = [ ("Big number", "{python} -m artiq.applets.big_number " - "embedded {ipc_address} NUMBER_DATASET"), + "--embed {ipc_address} NUMBER_DATASET"), ("Histogram", "{python} -m artiq.applets.plot_hist " - "embedded {ipc_address} COUNTS_DATASET " + "--embed {ipc_address} COUNTS_DATASET " "--x BIN_BOUNDARIES_DATASET"), ("XY", "{python} -m artiq.applets.plot_xy " - "embedded {ipc_address} Y_DATASET --x X_DATASET " + "--embed {ipc_address} Y_DATASET --x X_DATASET " "--error ERROR_DATASET --fit FIT_DATASET"), ("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist " - "embedded {ipc_address} X_DATASET " + "--embed {ipc_address} X_DATASET " "HIST_BIN_BOUNDARIES_DATASET " "HISTS_COUNTS_DATASET"), ] From 44a1efa601679d673c3a7e4aa8412276d6766622 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:25:20 +0100 Subject: [PATCH 11/11] applets: make sure pyqtgraph imports qt5 --- artiq/applets/big_number.py | 2 +- artiq/applets/plot_hist.py | 1 + artiq/applets/plot_xy.py | 1 + artiq/applets/plot_xy_hist.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/artiq/applets/big_number.py b/artiq/applets/big_number.py index 7eb60cbcd..8d13170b8 100755 --- a/artiq/applets/big_number.py +++ b/artiq/applets/big_number.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3.5 -from quamash import QtWidgets +from PyQt5 import QtWidgets from artiq.applets.simple import SimpleApplet diff --git a/artiq/applets/plot_hist.py b/artiq/applets/plot_hist.py index 54f459d91..ff77365fc 100755 --- a/artiq/applets/plot_hist.py +++ b/artiq/applets/plot_hist.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np +import PyQt5 # make sure pyqtgraph imports Qt5 import pyqtgraph from artiq.applets.simple import SimpleApplet diff --git a/artiq/applets/plot_xy.py b/artiq/applets/plot_xy.py index 4b8221f42..413d90cb2 100755 --- a/artiq/applets/plot_xy.py +++ b/artiq/applets/plot_xy.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np +import PyQt5 # make sure pyqtgraph imports Qt5 import pyqtgraph from artiq.applets.simple import SimpleApplet diff --git a/artiq/applets/plot_xy_hist.py b/artiq/applets/plot_xy_hist.py index 2a97a324c..0bf8987f3 100755 --- a/artiq/applets/plot_xy_hist.py +++ b/artiq/applets/plot_xy_hist.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np -from quamash import QtWidgets +from PyQt5 import QtWidgets import pyqtgraph from artiq.applets.simple import SimpleApplet