-
Notifications
You must be signed in to change notification settings - Fork 0
ARTIQ Applets
For writing applets, the best examples are dds (each channel is the same, repeated for a few times), and pmt (a GUI that doesn't have repeated elements). They represent two typical categories of applets.
This section describes how to write a general ARTIQ applet that connects to a LabRAD server example_server
. Some familiarity with LabRAD servers are expected before proceeding to read the section. The example_server
has a get_value()
setting to get a value, and a set_value()
setting to set a value. When the value is changed by another LabRAD client, the server sends out a value_changed
signal. We want to write an applet to show a QDoubleSpinBox
which can show and change this value.
An ARTIQ applet should inherit on both QWidget
and jax.JaxApplet
. QWidget
enables building an applet GUI, and JaxApplet
contains functions to make LabRAD connections. The module also needs to import artiq.applets.simple.SimpleApplet
, which is a wrapper on the applet to connect to ARTIQ, process command line arguments, and enable embedding in a dashboard.
from PyQt5 import QtWidgets, QtCore, QtGui
from artiq.applets.simple import SimpleApplet
from jax import JaxApplet
class ExampleApplet(QtWidgets.QWidget, JaxApplet):
"""Connects to a `example_server` and controls an example device."""
The __init__
method takes in an extra positional argument args
that correspond to command line arguments supplied by the user when running the applet. A typical command line argument is --ip
which can be used to define the LabRAD manager IP address to connect to. More details about command line arguments will be introduced later. The other keyword arguments are sent to the parent classes using super().__init__
.
After __init__
method of the parent classes are called, we build the GUI, which is typically disabled until the server is connected and the initial values are populated correctly. We also make a connection to LabRAD.
def __init__(self, args, **kwds):
super().__init__(**kwds)
self.setDisabled(True) # disable the whole GUI
self.init_ui()
self.setup_gui_listeners()
self.connect_to_labrad(args.ip) # connects to LabRAD, defined in JaxApplet
def init_ui(self):
layout = QtWidgets.QGridLayout()
label = "Value:"
layout.addWidget(label, 0, 0)
self.spinbox = QtWidgets.QDoubleSpinBox()
layout.addWidget(self.spinbox, 1, 0)
self.setLayout(layout)
def setup_gui_listeners(self):
self.spinbox.valueChanged.connect(self.spinbox_value_changed) # will be defined later
connect_to_labrad
is a function defined in the JaxApplet
class that connects to LabRAD to the IP address specified in the command line argument (default is the local computer) in another thread. We cannot connect to LabRAD in the main thread because twisted
(which LabRAD requires) cannot work in the same event loop as qasync
(which PyQt5 with asyncio requires).
Because connect_to_labrad
runs in a different thread, self.connect_to_labrad
is not a blocking function (it will not wait for LabRAD to connect). If you add any code immediately after connect_to_labrad
in the __init__
function, there is no guarantee that the LabRAD connection will be made before the following code is ran. Instead, at the end of connect_to_labrad
, it will call the labrad_connected
method, which we need to reimplement in this class. In labrad_connected
, we connect to the server, populates the initial values, and listens to signals. Note the async
and await
keywords in the code below.
async def labrad_connected(self):
"""self.cxn is defined in connect_to_labrad."""
await self.connect_server()
self.setup_cxn_listeners() # will be defined later
async def server_connected(self):
"""Connects to the server and populates the value in the GUI."""
self.server = self.cxn.get_server("example_server")
value = await self.server.get_value()
self.set_spinbox_value(value)
self.setDisabled(False) # the GUI can be enabled after the initialization.
await self.connect_to_signals() # connects to signals to update the GUI.
def set_spinbox_value(self, value):
self.spinbox.blockSignal(True) # avoids triggering the valueChanged event.
self.spinbox.setValue(value)
self.spinbox.blockSignal(False)
async def connect_to_signals(self):
# random integer. If the same client listens to two signals, they must use different IDs.
SIGNALID = 129484
await self.server.value_changed(SIGNALID)
self.server.addListener(listener=self.server_value_changed, # will be defined later
source=None, ID=SIGNALID)
We also set up listeners for server connection and disconnection. These listeners enable us to disable the GUI when the server disconnects, and enable the GUI and initializes the values again when the server connects. They are not necessary, but they make the GUI more robust against server restarting.
def setup_cxn_listeners(self):
"""Listens to when the server is connected or disconnected."""
# To run any async function using LabRAD from a sync function,
# we need to wrap the function with `self.run_in_labrad_loop`, which is defined in JaxApplet.
self.cxn.add_on_connect("example_server", self.run_in_labrad_loop(self.server_connected))
# since `server_disconnected` is a sync function, we don't need to wrap it.
self.cxn.add_on_disconnect("example_server", self.server_disconnected)
def server_disconnected(self):
self.setDisabled(True) # disable the applet when the server is disconnected.
We write functions to handle when the value is changed in the GUI, or when the value is changed in the server:
def spinbox_value_changed(self, value):
# we need to handle LabRAD (async) in this sync function.
# so we need to write an async worker, and wrap it with `self.run_in_labrad_loop`
async def worker():
await self.server.set_value(value)
self.run_in_labrad_loop(worker)()
def server_value_changed(self, signal, value):
self.set_spinbox_value(value)
Finally, we write instructions of how to run this applet
def main():
"""Handles `python -m __module_path__` calls."""
applet = SimpleApplet(ExampleApplet)
# adds IP address as an argument.
# If your client uses a config to get the IP address, the following line should be removed.
ExampleApplet.add_labrad_ip_argument(applet)
applet.run()
if __name__ == "__main__":
"""Handles `python __file_path__` calls."""
main()
The entire file is attached. I did not spend time testing it, so there might be bugs. The structure should be correct though.
from PyQt5 import QtWidgets, QtCore, QtGui
from artiq.applets.simple import SimpleApplet
from jax import JaxApplet
class ExampleApplet(QtWidgets.QWidget, JaxApplet):
"""Connects to a `example_server` and controls an example device."""
def __init__(self, args, **kwds):
super().__init__(**kwds)
self.setDisabled(True) # disable the whole GUI
self.init_ui()
self.setup_gui_listeners()
self.connect_to_labrad(args.ip) # connects to LabRAD, defined in JaxApplet
def init_ui(self):
layout = QtWidgets.QGridLayout()
label = "Value:"
layout.addWidget(label, 0, 0)
self.spinbox = QtWidgets.QDoubleSpinBox()
layout.addWidget(self.spinbox, 1, 0)
self.setLayout(layout)
def setup_gui_listeners(self):
self.spinbox.valueChanged.connect(self.spinbox_value_changed) # will be defined later
async def labrad_connected(self):
"""self.cxn is defined in connect_to_labrad."""
await self.connect_server()
self.setup_cxn_listeners() # will be defined later
async def server_connected(self):
"""Connects to the server and populates the value in the GUI."""
self.server = self.cxn.get_server("example_server")
value = await self.server.get_value()
self.set_spinbox_value(value)
self.setDisabled(False) # the GUI can be enabled after the initialization.
await self.connect_to_signals() # connects to signals to update the GUI.
def set_spinbox_value(self, value):
self.spinbox.blockSignal(True) # avoids triggering the valueChanged event.
self.spinbox.setValue(value)
self.spinbox.blockSignal(False)
async def connect_to_signals(self):
# random integer. If the same client listens to two signals, they must use different IDs.
SIGNALID = 129484
await self.server.value_changed(SIGNALID)
self.server.addListener(listener=self.server_value_changed, # will be defined later
source=None, ID=SIGNALID)
def setup_cxn_listeners(self):
"""Listens to when the server is connected or disconnected."""
# To run any async function using LabRAD from a sync function,
# we need to wrap the function with `self.run_in_labrad_loop`, which is defined in JaxApplet.
self.cxn.add_on_connect("example_server", self.run_in_labrad_loop(self.server_connected))
# since `server_disconnected` is a sync function, we don't need to wrap it.
self.cxn.add_on_disconnect("example_server", self.server_disconnected)
def server_disconnected(self):
self.setDisabled(True) # disable the applet when the server is disconnected.
def spinbox_value_changed(self, value):
# we need to handle LabRAD (async) in this sync function.
# so we need to write an async worker, and wrap it with `self.run_in_labrad_loop`
async def worker():
await self.server.set_value(value)
self.run_in_labrad_loop(worker)()
def server_value_changed(self, signal, value):
self.set_spinbox_value(value)
def main():
"""Handles `python -m __module_path__` calls."""
applet = SimpleApplet(ExampleApplet)
# adds IP address as an argument.
# If your client uses a config to get the IP address, the following line should be removed.
ExampleApplet.add_labrad_ip_argument(applet)
applet.run()
if __name__ == "__main__":
"""Handles `python __file_path__` calls."""
main()
To run this file (saved as $PYTHONPATH/repo/applets/example_applet.py
), either run
python -m repo.applets.example_applets
Or run
python __PYTHONPATH__/repo/applets/example_applet.py
The first command is recommended as the first command is shorter if you include the full file path in the second one. They will try to connect to the example_server
on the local computer. To connect to another computer, you need to add --ip __ip_address__
after the command, e.g.
python -m repo.applets.example_applets 192.168.1.101
### LabRAD imports
We cannot import labrad
or any component of it (e.g. labrad.units.WithUnit
) before connect_to_labrad
finishes. They can be imported in the labrad_connected
function if needed. You can assign an instance variable to it to keep a reference to the imported module.
You can add more command line arguments if you want to. See https://github.com/m-labs/artiq/blob/master/artiq/applets/big_number.py#L25-L26 for an example.
WinError
is typically due to failure to connect to ARTIQ master. If ARTIQ master is not running on the local computer, you will see this error. To avoid this, you need to add a --server
command line argument pointing to the computer that ARTIQ master is running on.
These are due to threading errors. For a simple GUI this should not be an issue. See the wavemeter applets in pydux if this is an issue for you.