forked from M-Labs/artiq
1
0
Fork 0

clarify controller terminology

This commit is contained in:
Sebastien Bourdeauducq 2015-02-16 08:57:15 -07:00
parent 61dc177bce
commit 4558fb3e33
7 changed files with 269 additions and 232 deletions

View File

@ -11,7 +11,7 @@ class HidError(Exception):
class Ldasim:
"""Lab Brick Digital Attenuator simulation controller.
"""Lab Brick Digital Attenuator simulation driver.
"""
def __init__(self):
@ -56,9 +56,9 @@ class Ldasim:
class Lda:
"""Lab Brick Digital Attenuator controller.
"""Lab Brick Digital Attenuator driver.
This controller depends on the hidapi library.
This driver depends on the hidapi library.
On Linux you should install hidapi-libusb shared library in a directory
listed in your LD_LIBRARY_PATH or in the conventional places (/usr/lib,

View File

@ -1,143 +1 @@
from artiq.language.core import *
from artiq.language.db import *
from artiq.language.units import *
from artiq.coredevice import rtio
frame_setup = 20*ns
trigger_duration = 50*ns
frame_wait = 20*ns
sample_period = 10*us # FIXME: check this
class SegmentSequenceError(Exception):
pass
class FrameActiveError(Exception):
pass
class FrameCloseError(Exception):
pass
class _Segment:
def __init__(self, frame, sn, duration, host_data):
self.core = frame.core
self.frame = frame
self.sn = sn
self.duration = duration
self.host_data = host_data
@kernel
def advance(self):
if self.frame.pdq.current_frame != self.frame.fn:
raise FrameActiveError
if self.frame.pdq.next_sn != self.sn:
raise SegmentSequenceError
self.frame.pdq.next_sn += 1
t = time_to_cycles(now())
self.frame.pdq.trigger.on(t)
self.frame.pdq.trigger.off(t + time_to_cycles(trigger_duration))
delay(self.duration)
class _Frame:
def __init__(self, core):
self.core = core
self.segment_count = 0
self.closed = False
def append(self, t, u, trigger=False, name=None):
if self.closed:
raise FrameCloseError
sn = self.segment_count
duration = (t[-1] - t[0])*sample_period
segment = _Segment(self, sn, duration, (t, u, trigger))
if name is None:
# TODO
raise NotImplementedError("Anonymous segments are not supported yet")
else:
if hasattr(self, name):
raise NameError("Segment name already exists")
setattr(self, name, segment)
self.segment_count += 1
def close(self):
if self.closed:
raise FrameCloseError
self.closed = True
@kernel
def begin(self):
if self.pdq.current_frame >= 0:
raise FrameActiveError
self.pdq.current_frame = self.fn
self.pdq.next_sn = 0
t = (time_to_cycles(now())
- time_to_cycles(frame_setup + trigger_duration + frame_wait))
self.pdq.frame0.set_value(t, self.fn & 1)
self.pdq.frame1.set_value(t, (self.fn & 2) >> 1)
self.pdq.frame2.set_value(t, (self.fn & 4) >> 2)
t += time_to_cycles(frame_setup)
self.pdq.trigger.on(t)
self.pdq.trigger.off(t + time_to_cycles(trigger_duration))
@kernel
def advance(self):
# TODO
raise NotImplementedError
@kernel
def finish(self):
if self.pdq.current_frame != self.fn:
raise FrameActiveError
if self.pdq.next_sn != self.segment_count:
raise FrameActiveError
self.pdq.current_frame = -1
self.pdq.next_sn = -1
def _prepare(self, pdq, fn):
if not self.closed:
raise FrameCloseError
self.pdq = pdq
self.fn = fn
def _invalidate(self):
del self.pdq
del self.fn
class CompoundPDQ2(AutoDB):
class DBKeys:
ids = Argument()
rtio_trigger = Argument()
rtio_frame = Argument()
def build(self):
self.trigger = rtio.LLRTIOOut(core=self.core, channel=self.rtio_trigger)
self.frame0 = rtio.LLRTIOOut(core=self.core, channel=self.rtio_frame[0])
self.frame1 = rtio.LLRTIOOut(core=self.core, channel=self.rtio_frame[1])
self.frame2 = rtio.LLRTIOOut(core=self.core, channel=self.rtio_frame[2])
self.frames = []
self.current_frame = -1
self.next_sn = -1
def create_frame(self):
return _Frame(self.core)
def prepare(self, *frames):
# prevent previous frames and their segments from
# being (incorrectly) used again
for frame in self.frames:
frame._invalidate()
self.frames = list(frames)
for fn, frame in enumerate(frames):
frame._prepare(self, fn)
# TODO: upload to PDQ2 devices
from artiq.devices.pdq2.mediator import *

View File

@ -0,0 +1,143 @@
from artiq.language.core import *
from artiq.language.db import *
from artiq.language.units import *
from artiq.coredevice import rtio
frame_setup = 20*ns
trigger_duration = 50*ns
frame_wait = 20*ns
sample_period = 10*us # FIXME: check this
class SegmentSequenceError(Exception):
pass
class FrameActiveError(Exception):
pass
class FrameCloseError(Exception):
pass
class _Segment:
def __init__(self, frame, sn, duration, host_data):
self.core = frame.core
self.frame = frame
self.sn = sn
self.duration = duration
self.host_data = host_data
@kernel
def advance(self):
if self.frame.pdq.current_frame != self.frame.fn:
raise FrameActiveError
if self.frame.pdq.next_sn != self.sn:
raise SegmentSequenceError
self.frame.pdq.next_sn += 1
t = time_to_cycles(now())
self.frame.pdq.trigger.on(t)
self.frame.pdq.trigger.off(t + time_to_cycles(trigger_duration))
delay(self.duration)
class _Frame:
def __init__(self, core):
self.core = core
self.segment_count = 0
self.closed = False
def append(self, t, u, trigger=False, name=None):
if self.closed:
raise FrameCloseError
sn = self.segment_count
duration = (t[-1] - t[0])*sample_period
segment = _Segment(self, sn, duration, (t, u, trigger))
if name is None:
# TODO
raise NotImplementedError("Anonymous segments are not supported yet")
else:
if hasattr(self, name):
raise NameError("Segment name already exists")
setattr(self, name, segment)
self.segment_count += 1
def close(self):
if self.closed:
raise FrameCloseError
self.closed = True
@kernel
def begin(self):
if self.pdq.current_frame >= 0:
raise FrameActiveError
self.pdq.current_frame = self.fn
self.pdq.next_sn = 0
t = (time_to_cycles(now())
- time_to_cycles(frame_setup + trigger_duration + frame_wait))
self.pdq.frame0.set_value(t, self.fn & 1)
self.pdq.frame1.set_value(t, (self.fn & 2) >> 1)
self.pdq.frame2.set_value(t, (self.fn & 4) >> 2)
t += time_to_cycles(frame_setup)
self.pdq.trigger.on(t)
self.pdq.trigger.off(t + time_to_cycles(trigger_duration))
@kernel
def advance(self):
# TODO
raise NotImplementedError
@kernel
def finish(self):
if self.pdq.current_frame != self.fn:
raise FrameActiveError
if self.pdq.next_sn != self.segment_count:
raise FrameActiveError
self.pdq.current_frame = -1
self.pdq.next_sn = -1
def _prepare(self, pdq, fn):
if not self.closed:
raise FrameCloseError
self.pdq = pdq
self.fn = fn
def _invalidate(self):
del self.pdq
del self.fn
class CompoundPDQ2(AutoDB):
class DBKeys:
ids = Argument()
rtio_trigger = Argument()
rtio_frame = Argument()
def build(self):
self.trigger = rtio.LLRTIOOut(core=self.core, channel=self.rtio_trigger)
self.frame0 = rtio.LLRTIOOut(core=self.core, channel=self.rtio_frame[0])
self.frame1 = rtio.LLRTIOOut(core=self.core, channel=self.rtio_frame[1])
self.frame2 = rtio.LLRTIOOut(core=self.core, channel=self.rtio_frame[2])
self.frames = []
self.current_frame = -1
self.next_sn = -1
def create_frame(self):
return _Frame(self.core)
def prepare(self, *frames):
# prevent previous frames and their segments from
# being (incorrectly) used again
for frame in self.frames:
frame._invalidate()
self.frames = list(frames)
for fn, frame in enumerate(frames):
frame._prepare(self, fn)
# TODO: upload to PDQ2 devices

View File

@ -1,10 +1,25 @@
Writing a driver
================
Developing a network device support package
===========================================
These instructions cover writing a simple driver for a "slow" device, that uses the controller mechanism.
Most ARTIQ devices are interfaced through "controllers" that expose RPC interfaces to the network (based on :class:`artiq.protocols.pc_rpc`). The master never does direct I/O to the devices, but issues RPCs to the controllers when needed. As opposed to running everything on the master, this architecture has those main advantages:
The controller
--------------
* Each driver can be run on a different machine, which alleviates cabling issues and OS compatibility problems.
* Reduces the impact of driver crashes.
* Reduces the impact of driver memory leaks.
This mechanism is for "slow" devices that are directly controlled by a PC, typically over a non-realtime channel such as USB.
Certain devices (such as the PDQ2) may still perform real-time operations by having certain controls physically connected to the core device (for example, the trigger and frame selection signals on the PDQ2). For handling such cases, parts of the NDSPs may be kernels executed on the core device.
A network device support package (NDSP) is composed of several parts:
1. The `driver`, which contains the Python API functions to be called over the network, and performs the I/O to the device. The top-level module of the driver is called ``artiq.devices.XXX.driver``.
2. The `controller`, which instantiates, initializes and terminates the driver, and sets up the RPC server. The controller is a front-end command-line tool to the user and is called ``artiq.frontend.XXX_controller``. A ``setup.py`` entry must also be created to install it.
3. An optional `client`, which connects to the controller and exposes the functions of the driver as a command-line interface. Clients are front-end tools (called ``artiq.frontend.XXX_client``) that have ``setup.py`` entries. In most cases, a custom client is not needed and the generic ``artiq_rpctool`` utility can be used instead. Custom clients are only required when large amounts of data must be transferred over the network API, that would be unwieldy to pass as ``artiq_rpctool`` command-line parameters.
4. An optional `mediator`, which is code executed on the client that supplements the network API. A mediator may contain kernels that control real-time signals such as TTL lines connected to the device. Simple devices use the network API directly and do not have a mediator. Mediator modules are called ``artiq.devices.XXX.mediator`` and their public classes are exported at the ``artiq.devices.XXX`` level (via ``__init__.py``) for direct import and use by the experiments.
The driver and controller
-------------------------
A controller is a piece of software that receives commands from a client over the network (or the ``localhost`` interface), drives a device, and returns information about the device to the client. The mechanism used is remote procedure calls (RPCs) using :class:`artiq.protocols.pc_rpc`, which makes the network layers transparent for the driver's user.
@ -16,6 +31,8 @@ For using RPC, the functions that a driver provides must be the methods of a sin
def message(self, msg):
print("message: " + msg)
For a more complex driver, you would put this class definition into a separate Python module called ``driver``.
To turn it into a server, we use :class:`artiq.protocols.pc_rpc`. Import the function we will use: ::
from artiq.protocols.pc_rpc import simple_server_loop
@ -51,17 +68,20 @@ and verify that you can connect to the TCP port: ::
:tip: Use the key combination Ctrl-AltGr-9 to get the ``telnet>`` prompt, and enter ``close`` to quit Telnet. Quit the controller with Ctrl-C.
Also verify that a target (service) named "hello" (as passed in the first argument to ``simple_server_loop``) exists using the ``artiq_rpctool.py`` program from the ARTIQ front-end tools: ::
Also verify that a target (service) named "hello" (as passed in the first argument to ``simple_server_loop``) exists using the ``artiq_rpctool`` program from the ARTIQ front-end tools: ::
$ artiq_rpctool.py ::1 3249 list-targets
$ artiq_rpctool ::1 3249 list-targets
Target(s): hello
The client
----------
Controller clients are small command-line utilities that expose certain functionalities of the drivers. They are optional, and not used very often - typically for debugging and testing.
Clients are small command-line utilities that expose certain functionalities of the drivers. The ``artiq_rpctool`` utility contains a generic client that can be used in most cases, and developing a custom client is not required. Try these commands ::
Create a ``hello_client.py`` file with the following contents: ::
$ artiq_rpctool ::1 3249 list-methods
$ artiq_rpctool ::1 3249 call message test
In case you are developing a NDSP that is complex enough to need a custom client, we will see how to develop one. Create a ``hello_client.py`` file with the following contents: ::
#!/usr/bin/env python3
@ -92,16 +112,15 @@ Command-line arguments
Use the Python ``argparse`` module to make the bind address and port configurable on the controller, and the server address, port and message configurable on the client.
We suggest naming the controller parameters ``--bind`` and ``--port`` so that those parameters stay consistent across controller, and use ``-s/--server`` and ``--port`` on the client.
We suggest naming the controller parameters ``--bind`` and ``--port`` so that those parameters stay consistent across controller, and use ``-s/--server`` and ``--port`` on the client. The ``artiq.tools.simple_network_args`` library function adds such arguments for the controller.
The controller's code would contain something similar to this: ::
from artiq.tools import simple_network_args
def get_argparser():
parser = argparse.ArgumentParser(description="Hello world controller")
parser.add_argument("--bind", default="::1",
help="hostname or IP address to bind to")
parser.add_argument("--port", default=3249, type=int,
help="TCP port to listen to")
simple_network_args(parser, 3249) # 3249 is the default TCP port
return parser
def main():
@ -110,10 +129,8 @@ The controller's code would contain something similar to this: ::
We suggest that you define a function ``get_argparser`` that returns the argument parser, so that it can be used to document the command line parameters using sphinx-argparse.
Logging and error handling in controllers
-----------------------------------------
Unrecoverable errors (such as the hardware being unplugged) should cause timely termination of the controller, in order to notify the controller manager which may try to restart the controller later according to its policy. Throwing an exception and letting it propagate is the preferred way of reporting an unrecoverable error.
Logging
-------
For the debug, information and warning messages, use the ``logging`` Python module and print the log on the standard error output (the default setting). The logging level is by default "WARNING", meaning that only warning messages and more critical messages will get printed (and no debug nor information messages). By calling the ``verbosity_args()`` with the parser as argument, you add support for the ``--verbose`` (``-v``) and ``--quiet`` (``-q``) arguments in the parser. Each occurence of ``-v`` (resp. ``-q``) in the arguments will increase (resp. decrease) the log level of the logging module. For instance, if only one ``-v`` is present in the arguments, then more messages (info, warning and above) will get printed. If only one ``-q`` is present in the arguments, then only errors and critical messages will get printed. If ``-qq`` is present in the arguments, then only critical messages will get printed, but no debug/info/warning/error.
@ -159,6 +176,7 @@ General guidelines
* Use new-style formatting (``str.format``) except for logging where it is not well supported, and double quotes for strings.
* The device identification (e.g. serial number) to attach to must be passed as a command-line parameter to the controller. We suggest using ``-s`` and ``--serial`` as parameter name.
* Controllers must be able to operate in "simulation" mode, where they behave properly even if the associated hardware is not connected. For example, they can print the data to the console instead of sending it to the device, or dump it into a file.
* We suggest that the simulation mode is entered by using "sim" in place of the serial number.
* We suggest that the simulation mode is entered by using "sim" in place of the serial number or device name.
* Keep command line parameters consistent across clients/controllers. When adding new command line options, look for a client/controller that does a similar thing and follow its use of ``argparse``. If the original client/controller could use ``argparse`` in a better way, improve it.
* Use docstrings for all public methods of the driver (note that those will be retrieved by ``artiq_rpctool``).
* Choose a free default TCP port and add it to the default port list in this manual.

View File

@ -1,65 +0,0 @@
Drivers reference
=================
These drivers are for "slow" devices that are directly controlled by a PC, typically over a non-realtime channel such as USB.
Certain devices (such as the PDQ2) may still perform real-time operations by having certain controls physically connected to the core device (for example, the trigger and frame selection signals on the PDQ2). For handling such cases, parts of the drivers may be kernels executed on the core device.
Each driver is run in a separate "controller" that exposes a RPC interface (based on :class:`artiq.protocols.pc_rpc`) to its functions. The master never does direct I/O to the devices, but issues RPCs to the controllers when needed. As opposed to running everything on the master, this architecture has those main advantages:
* Each driver can be run on a different machine, which alleviates cabling issues and OS compatibility problems.
* Reduces the impact of driver crashes.
* Reduces the impact of driver memory leaks.
:mod:`artiq.devices.pdq2` module
----------------------------------
.. automodule:: artiq.devices.pdq2
:members:
.. argparse::
:ref: artiq.frontend.pdq2_controller.get_argparser
:prog: pdq2_controller
.. argparse::
:ref: artiq.frontend.pdq2_client.get_argparser
:prog: pdq2_client
:mod:`artiq.devices.lda` module
---------------------------------
.. automodule:: artiq.devices.lda.driver
:members:
.. argparse::
:ref: artiq.frontend.lda_controller.get_argparser
:prog: lda_controller
:mod:`artiq.devices.novatech409b` module
----------------------------------------
.. automodule:: artiq.devices.novatech409b.driver
:members:
.. argparse::
:ref: artiq.frontend.novatech409b_controller.get_argparser
:prog: novatech409b_controller
Default TCP port list
---------------------
When writing a new driver, choose a free TCP port and add it to this list.
+--------------------------+--------------+
| Component | Default port |
+==========================+==============+
| Master (notifications) | 3250 |
+--------------------------+--------------+
| Master (control) | 3251 |
+--------------------------+--------------+
| PDQ2 | 3252 |
+--------------------------+--------------+
| LDA | 3253 |
+--------------------------+--------------+
| Novatech 409B | 3254 |
+--------------------------+--------------+

View File

@ -8,11 +8,11 @@ Contents:
installing
getting_started
writing_a_driver
developing_a_ndsp
management_system
core_language_reference
core_drivers_reference
protocols_reference
drivers_reference
ndsp_reference
utilities
fpga_board_ports

View File

@ -0,0 +1,83 @@
Network device support packages reference
=========================================
PDQ2
----
Driver
++++++
.. automodule:: artiq.devices.pdq2.driver
:members:
Mediator
++++++++
.. automodule:: artiq.devices.pdq2.mediator
:members:
Controller
++++++++++
.. argparse::
:ref: artiq.frontend.pdq2_controller.get_argparser
:prog: pdq2_controller
Client
++++++
.. argparse::
:ref: artiq.frontend.pdq2_client.get_argparser
:prog: pdq2_client
Lab Brick Digital Attenuator (LDA)
----------------------------------
Driver
++++++
.. automodule:: artiq.devices.lda.driver
:members:
Controller
++++++++++
.. argparse::
:ref: artiq.frontend.lda_controller.get_argparser
:prog: lda_controller
Novatech 409B
-------------
Driver
++++++
.. automodule:: artiq.devices.novatech409b.driver
:members:
Controller
++++++++++
.. argparse::
:ref: artiq.frontend.novatech409b_controller.get_argparser
:prog: novatech409b_controller
Default TCP port list
---------------------
When writing a new NDSP, choose a free TCP port and add it to this list.
+--------------------------+--------------+
| Component | Default port |
+==========================+==============+
| Master (notifications) | 3250 |
+--------------------------+--------------+
| Master (control) | 3251 |
+--------------------------+--------------+
| PDQ2 | 3252 |
+--------------------------+--------------+
| LDA | 3253 |
+--------------------------+--------------+
| Novatech 409B | 3254 |
+--------------------------+--------------+