artiq/artiq/applets/simple.py

267 lines
9.4 KiB
Python
Raw Normal View History

2016-02-08 21:35:37 +08:00
import logging
2016-01-03 00:46:15 +08:00
import argparse
import asyncio
import os
2016-04-06 17:02:57 +08:00
import string
2016-01-03 00:46:15 +08:00
2016-04-08 01:22:33 +08:00
from quamash import QEventLoop, QtWidgets, QtCore
2016-01-03 00:46:15 +08:00
2016-02-09 02:20:07 +08:00
from artiq.protocols.sync_struct import Subscriber, process_mod
2016-02-08 16:59:15 +08:00
from artiq.protocols import pyon
from artiq.protocols.pipe_ipc import AsyncioChildComm
2016-02-08 21:35:37 +08:00
logger = logging.getLogger(__name__)
2016-02-08 16:59:15 +08:00
class AppletIPCClient(AsyncioChildComm):
2016-02-08 21:35:37 +08:00
def set_close_cb(self, close_cb):
self.close_cb = close_cb
2016-02-08 16:59:15 +08:00
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):
2016-02-08 21:35:37 +08:00
# This function is only called when not subscribed to anything,
# so the only normal replies are embed_done and terminate.
2016-02-08 16:59:15 +08:00
self.write_pyon({"action": "embed",
"win_id": win_id})
reply = await self.read_pyon()
2016-02-08 21:35:37 +08:00
if reply["action"] == "terminate":
self.close_cb()
elif reply["action"] != "embed_done":
logger.error("unexpected action reply to embed request: %s",
2016-04-08 01:22:33 +08:00
reply["action"])
2016-02-08 21:35:37 +08:00
self.close_cb()
def fix_initial_size(self):
self.write_pyon({"action": "fix_initial_size"})
2016-02-08 21:35:37 +08:00
async def listen(self):
2016-02-09 02:20:07 +08:00
data = None
2016-02-08 21:35:37 +08:00
while True:
obj = await self.read_pyon()
try:
action = obj["action"]
if action == "terminate":
self.close_cb()
return
2016-02-09 02:20:07 +08:00
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)
2016-02-08 21:35:37 +08:00
else:
2016-02-09 02:23:50 +08:00
raise ValueError("unknown action in parent message")
2016-02-08 21:35:37 +08:00
except:
2016-02-09 02:23:50 +08:00
logger.error("error processing parent message",
2016-04-08 01:22:33 +08:00
exc_info=True)
2016-02-08 21:35:37 +08:00
self.close_cb()
2016-02-09 02:20:07 +08:00
def subscribe(self, datasets, init_cb, mod_cb):
2016-02-08 21:35:37 +08:00
self.write_pyon({"action": "subscribe",
"datasets": datasets})
2016-02-09 02:20:07 +08:00
self.init_cb = init_cb
self.mod_cb = mod_cb
2016-02-08 21:35:37 +08:00
asyncio.ensure_future(self.listen())
2016-01-03 00:46:15 +08:00
class SimpleApplet:
2016-01-10 22:12:00 +08:00
def __init__(self, main_widget_class, cmd_description=None,
default_update_delay=0.0):
2016-01-03 00:46:15 +08:00
self.main_widget_class = main_widget_class
self.argparser = argparse.ArgumentParser(description=cmd_description)
2016-02-08 16:59:15 +08:00
2016-04-08 01:22:33 +08:00
self.argparser.add_argument(
"--update-delay", type=float, default=default_update_delay,
2016-01-10 22:12:00 +08:00
help="time to wait after a mod (buffering other mods) "
2016-04-08 01:22:33 +08:00
"before updating (default: %(default).2f)")
2016-02-08 16:59:15 +08:00
2016-02-09 05:25:02 +08:00
group = self.argparser.add_argument_group("standalone mode (default)")
group.add_argument(
2016-02-08 16:59:15 +08:00
"--server", default="::1",
2016-02-09 05:25:02 +08:00
help="hostname or IP of the master to connect to "
"for dataset notifications "
"(ignored in embedded mode)")
group.add_argument(
2016-02-08 16:59:15 +08:00
"--port", default=3250, type=int,
help="TCP port to connect to")
2016-04-08 01:22:33 +08:00
self.argparser.add_argument(
"--embed", default=None, help="embed into GUI",
metavar="IPC_ADDRESS")
2016-02-09 05:25:02 +08:00
self._arggroup_datasets = self.argparser.add_argument_group("datasets")
2016-02-08 16:59:15 +08:00
2016-01-13 22:04:55 +08:00
self.dataset_args = set()
2016-01-03 00:46:15 +08:00
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)
2016-01-03 00:46:15 +08:00
else:
self._arggroup_datasets.add_argument("--" + name, **kwargs)
2016-01-13 22:04:55 +08:00
self.dataset_args.add(name)
2016-01-03 00:46:15 +08:00
def args_init(self):
self.args = self.argparser.parse_args()
2016-01-13 22:04:55 +08:00
self.datasets = {getattr(self.args, arg.replace("-", "_"))
for arg in self.dataset_args}
2016-01-03 00:46:15 +08:00
def quamash_init(self):
app = QtWidgets.QApplication([])
self.loop = QEventLoop(app)
asyncio.set_event_loop(self.loop)
2016-02-08 16:59:15 +08:00
def ipc_init(self):
2016-02-09 05:25:02 +08:00
if self.args.embed is not None:
self.ipc = AppletIPCClient(self.args.embed)
2016-02-08 16:59:15 +08:00
self.loop.run_until_complete(self.ipc.connect())
def ipc_close(self):
2016-02-09 05:25:02 +08:00
if self.args.embed is not None:
2016-02-08 16:59:15 +08:00
self.ipc.close()
2016-01-03 00:46:15 +08:00
def create_main_widget(self):
self.main_widget = self.main_widget_class(self.args)
2016-02-09 05:25:02 +08:00
if self.args.embed is not None:
2016-02-08 21:35:37 +08:00
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
2016-04-08 01:22:33 +08:00
# 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()
2016-01-03 00:46:15 +08:00
def sub_init(self, data):
self.data = data
return data
2016-01-13 22:04:55 +08:00
def filter_mod(self, mod):
2016-02-09 05:25:02 +08:00
if self.args.embed is not None:
2016-02-08 16:59:15 +08:00
# the parent already filters for us
return True
2016-01-13 22:04:55 +08:00
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
2016-04-06 17:02:57 +08:00
def emit_data_changed(self, data, mod_buffer):
self.main_widget.data_changed(data, mod_buffer)
2016-01-10 22:12:00 +08:00
def flush_mod_buffer(self):
2016-04-06 17:02:57 +08:00
self.emit_data_changed(self.data, self.mod_buffer)
2016-01-10 22:12:00 +08:00
del self.mod_buffer
2016-01-03 00:46:15 +08:00
def sub_mod(self, mod):
2016-01-13 22:04:55 +08:00
if not self.filter_mod(mod):
return
2016-01-10 22:12:00 +08:00
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:
2016-04-06 17:02:57 +08:00
self.emit_data_changed(self.data, [mod])
2016-01-03 00:46:15 +08:00
2016-02-08 16:59:15 +08:00
def subscribe(self):
2016-02-09 05:25:02 +08:00
if self.args.embed is None:
2016-02-08 16:59:15 +08:00
self.subscriber = Subscriber("datasets",
self.sub_init, self.sub_mod)
self.loop.run_until_complete(self.subscriber.connect(
2016-02-09 05:25:02 +08:00
self.args.server, self.args.port))
2016-02-08 16:59:15 +08:00
else:
2016-02-09 05:25:02 +08:00
self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod)
2016-02-08 16:59:15 +08:00
def unsubscribe(self):
2016-02-09 05:25:02 +08:00
if self.args.embed is None:
2016-02-08 16:59:15 +08:00
self.loop.run_until_complete(self.subscriber.close())
2016-01-03 00:46:15 +08:00
def run(self):
self.args_init()
self.quamash_init()
try:
2016-02-08 16:59:15 +08:00
self.ipc_init()
2016-01-03 00:46:15 +08:00
try:
2016-02-08 16:59:15 +08:00
self.create_main_widget()
self.subscribe()
try:
self.loop.run_forever()
finally:
self.unsubscribe()
2016-01-03 00:46:15 +08:00
finally:
2016-02-08 16:59:15 +08:00
self.ipc_close()
2016-01-03 00:46:15 +08:00
finally:
self.loop.close()
2016-04-06 17:02:57 +08:00
class TitleApplet(SimpleApplet):
def __init__(self, *args, **kwargs):
SimpleApplet.__init__(self, *args, **kwargs)
self.argparser.add_argument("--title", default=None,
help="set title (can be a Python format "
"string where field names are dataset "
"names, replace '.' with '/')")
2016-04-06 17:02:57 +08:00
def args_init(self):
SimpleApplet.args_init(self)
if self.args.title is not None:
self.dataset_title = set()
parsed = string.Formatter().parse(self.args.title)
for _, format_field, _, _ in parsed:
if format_field is None:
break
if not format_field:
raise ValueError("Invalid title format string")
self.dataset_title.add(format_field.replace("/", "."))
2016-04-06 17:02:57 +08:00
self.datasets |= self.dataset_title
def emit_data_changed(self, data, mod_buffer):
if self.args.title is not None:
title_values = {k.replace(".", "/"): data.get(k, (False, None))[1]
2016-04-06 17:02:57 +08:00
for k in self.dataset_title}
try:
title = self.args.title.format(**title_values)
except:
logger.warning("failed to format title", exc_info=True)
title = self.args.title
else:
title = None
self.main_widget.data_changed(data, mod_buffer, title)