From 4558fb3e338171610deaad57e0a7ced97fbda21e Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 16 Feb 2015 08:57:15 -0700 Subject: [PATCH] clarify controller terminology --- artiq/devices/lda/driver.py | 6 +- artiq/devices/pdq2/__init__.py | 144 +----------------- artiq/devices/pdq2/mediator.py | 143 +++++++++++++++++ ...ing_a_driver.rst => developing_a_ndsp.rst} | 56 ++++--- doc/manual/drivers_reference.rst | 65 -------- doc/manual/index.rst | 4 +- doc/manual/ndsp_reference.rst | 83 ++++++++++ 7 files changed, 269 insertions(+), 232 deletions(-) create mode 100644 artiq/devices/pdq2/mediator.py rename doc/manual/{writing_a_driver.rst => developing_a_ndsp.rst} (64%) delete mode 100644 doc/manual/drivers_reference.rst create mode 100644 doc/manual/ndsp_reference.rst diff --git a/artiq/devices/lda/driver.py b/artiq/devices/lda/driver.py index 2abf52b3f..2cd849640 100644 --- a/artiq/devices/lda/driver.py +++ b/artiq/devices/lda/driver.py @@ -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, diff --git a/artiq/devices/pdq2/__init__.py b/artiq/devices/pdq2/__init__.py index 5ad2180c0..10896c0fd 100644 --- a/artiq/devices/pdq2/__init__.py +++ b/artiq/devices/pdq2/__init__.py @@ -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 * diff --git a/artiq/devices/pdq2/mediator.py b/artiq/devices/pdq2/mediator.py new file mode 100644 index 000000000..5ad2180c0 --- /dev/null +++ b/artiq/devices/pdq2/mediator.py @@ -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 diff --git a/doc/manual/writing_a_driver.rst b/doc/manual/developing_a_ndsp.rst similarity index 64% rename from doc/manual/writing_a_driver.rst rename to doc/manual/developing_a_ndsp.rst index 6aa3d4d51..07c9c6273 100644 --- a/doc/manual/writing_a_driver.rst +++ b/doc/manual/developing_a_ndsp.rst @@ -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. diff --git a/doc/manual/drivers_reference.rst b/doc/manual/drivers_reference.rst deleted file mode 100644 index 80bcc6f5d..000000000 --- a/doc/manual/drivers_reference.rst +++ /dev/null @@ -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 | -+--------------------------+--------------+ diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 75ded1ff5..b1705a42d 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -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 diff --git a/doc/manual/ndsp_reference.rst b/doc/manual/ndsp_reference.rst new file mode 100644 index 000000000..2eb98f48c --- /dev/null +++ b/doc/manual/ndsp_reference.rst @@ -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 | ++--------------------------+--------------+