add example_artiq_device controller and doc

This commit is contained in:
Joe Britton 2015-02-02 19:00:42 -07:00
parent c259c4f46f
commit c699f5e704
7 changed files with 494 additions and 23 deletions

View File

@ -0,0 +1 @@
from .driver import ExampleARTIQDevice

View File

@ -0,0 +1,215 @@
import logging
import ctypes
import struct
import numpy.random
from artiq.language.units import dB, check_unit, Quantity
class ExampleARTIQDevice:
"""Example code demonstrating how to write an device controller for ARTIQ.
What the class ought demonstrate was discussed
on the ARTIQ mailing list Dec 2014. References to this discussion are in double parenthesis eg ((1a))
https://ssl.serverraum.org/lists-archive/artiq/2014-December/000253.html
"""
self._timeout_config = 1.0 # timeout in seconds for device to respond upon configuration
self._timeout_interaction = 0.1 # timeout in seconds for device to respond post configuration
def __init__(self, simulate_hw, serial_port):
"""Initialization steps require to create an instance of the device interface.
Examples:
* initiate communication with device hardware over serial port
* locate on-disk configuration files
* how should system respond if device can't be found? What's default timeout?
"""
# TODO handle problem that /dev/ttyUSBx is not unique ((2g))
# default logging level is 30, controller or client can change this subsequently
self.__setup_logging(logging_level=30)
# setup serial interface to device
self.__setup_serial(serial_port)
def __del__(self):
"""De-initialization steps required to safely shutdown an instance of the
device interface go here.
Examples:
* close serial port
* close any open files
"""
def __setup_logging(self, logging_level):
"""Do whatever is needed to configure ARTIQ logging.
For more on the python logging tool see https://docs.python.org/2/library/logging.html
:param int logging_level: to what degree are messages generated by this driver reported to the ARTIQ ecosystem?
50 is CRITICAL, 40 is ERROR, 30 is WARNING (default), 20 is INFO, 10 is DEBUG
:return None:
"""
logging_format_string = "%(asctime)-15s %(message)s"
logging.basicConfig(format=logging_format_string)
self.logger = logging.getLogger(self.__class__.__name__)
self.logger.setLevel(logging_level)
# TODO: Should there be a default class from which devices inherit? Such a class could include
# proper setup of things like the logger. This would also make it easier to add additional functionality
# across the driver ecosystem without modifying the code of each driver individually.
# TODO: how is the logger connected to the client and/or GUI? Either or both might need to be notified
# of errors. Nobody commented on this...
def set_logging_level(self, logging_level):
"""Change logging level.
:param int logging_level: to what degree are messages generated by this driver reported to the ARTIQ ecosystem?
50 is CRITICAL, 40 is ERROR, 30 is WARNING (default), 20 is INFO, 10 is DEBUG
:return None:
"""
self.logger.setLevel(logging_level)
def __setup_connection_to_parameter_database(self):
"""Do whatever is needed to communicate with the parameter database.
:return None:
"""
def __setup_serial(self, serial_port):
"""Do whatever is needed to communicate with a serial device.
TODO: For the serial ports adduser user dialout and serial.serial_for_url() is much simpler,
more powerful, and also works under windows. RJ
:param str serial port: on Windows an integer in a string (e.g. "1"); on Linux a device path (e.g. "/dev/ttyUSB0")
:return None:
"""
def echo(self, s):
"""Demonstration of a simple device subroutine that echoes back whatever string is passed to it.
:param str s: a string that will be echo'd back
:return str: return s
"""
replys = "echo: {}".format(s)
self.logger.info(replys)
return s
def sphynx_documentation_example(self, arg1, arg2, arg3=True):
"""Example of how to properly label arguments for automatic parsing by the ARTIQ documentation system.
ARTIQ uses the Sphinx documentation system to automatically generate help files for devices. The first line of
the comment is a short synopsis of what the function does: "Example of how to...". Separated from the first
line by an empty carriage return is a more verbose description of what the function does. Finally, the
documentation section is concluded by a description of the function arguments and return value in a
particular format. See "Info field lists" http://sphinx-doc.org/domains.html#info-field-lists
:param str arg1: arg1 is a string argument (no default value)
:param int arg2: arg2 is an integer argument (no default value)
:param bool arg3: arg3 is a boolean argument (true default value)
:return int: the return value is an integer
"""
return 0
def example_using_logger(self):
"""Shows how to use the logger.
The details on the python logger are here : https://docs.python.org/2/library/logging.html
In the context of ARTIQ the logger is the mechanism by which a driver can communicate status information
to the front-end Graphical User Interface or put critical errors into the experiment log.
:return None:
"""
self.logger.info("logs a message with level INFO")
self.logger.warning("logs a message with level WARNING")
self.logger.error("logs a message with level ERROR")
self.logger.log("logs a message with level CRITICAL")
def example_using_quantity_class(self, qvar1, qvar2):
"""Example of how to properly use write a function that takes arguments of the Quantity class.
ARTIQ includes a special class for passing around Quantities that have specific types or ranges. For example,
a device number might only be an integer, a phase (in rounds) should lie between 0 and 1, and a
frequency can't be negative. Other advanced Quantities might be arrays or python classes.
:param Quantity qvar1: is a ....
:param Quantity qvar2: is a ....
:return None:
"""
# TODO: please include some code showing how to use this
# show how to raise an exception if the wrong Quantity is passed or a variable that is not of type
# Quantity
def demo_exception_handling(self, myvar):
"""Example code that tells the device to do something specific. And throw an exception if it goes bad.
:param int myvar: variable that modifies device behavior
:return None:
"""
try:
my_random_num = numpy.random.rand(1)[0]
if myvar < 0:
self.logger.error("argument must be greater than zero")
elif my_random_num > myvar:
# alert GUI that this has happened
# raise an ARTIQ specific exception here
self.logger.error("you're unlucky")
else:
r = myvar/0
except ZeroDivisionError:
# caught a divide by zero error; if it can be handled locally do that
# if it can't be handeled locally throw it for another
self.logger.error("divide by zero")
raise
except:
self.logger.error("unhandled exception")
raise
# TODO: Is this right? I don't know how artiq handles exceptions.
def example_interface_with_c(self):
"""(1) example of how to interface with some generic C code
Generic code in C with functions passing a representative sample of types
e.g. (char[10], int[10], double[10], my_struct[10]). In a subfolder of example_artiq_device
is the example C code along with a suitable makefile.
:return None:
"""
# TODO: implement this
def program_device_with_vector_argument(self, my_vec):
"""get a vector to modify device behavior
This could be a vector describing the waveform to be generated by an ADC
:return None:
"""
# TODO: implement this
def get_from_parameter_database(self):
"""get some parameters from the parameter database
Cases to consider:
1) what if the requested parameter is not in the database
2) parameter is a vector or class object
:return None:
"""
# TODO: implement this
def set_variable_to_parameter_database(self):
"""update some parameters in the parameter database
Cases to consider:
1) requested parameter is not in the database
2) parameter is a vector or class object
:return None:
"""
# TODO: implement this
def gpib_communication_example(self):
"""per ((3a))
:return None:
"""
# TODO: implement this

View File

@ -0,0 +1,2 @@
# put code framework and example here for simulating a piece of hardware in the right way.
# I don't know how to do this.

View File

@ -0,0 +1,95 @@
#!/usr/bin/env python3
import argparse
from functools import partial
from artiq.protocols.pc_rpc import Client
# Question: shouldn't this restricted_float() be part of the Quantity class?
# Question: Shouldn't all the parameters passed to drivers be of the Quantity class?
def restricted_float(val_min, val_max, x):
"""do range checking on a variable
"""
x = float(x)
if x < val_min or x > val_max:
raise argparse.ArgumentTypeError(
"{:f} not in range [{:f}, {:f}]".format(x, val_min, val_max))
return x
def define_parser():
parser = argparse.ArgumentParser(
description="example_artiq_device_client",
epilog="This is a m-labs.com ARTIQ "
"client that serves as a template for interaction with ARTIQ devices. == "
"The hardware interface is a serial port.")
# following are default arguments that should be common to any ARTIQ device client.
parser.add_argument("--bind", default="::1",
help="hostname or IP address to bind to (::1 is localhost)")
parser.add_argument("--port", default=3254, type=int,
help="TCP port to listen to 3254")
parser.add_argument("--verbose", action="store_true",
help="increase output verbosity")
# Following are command line options for interacting with a specific device.
# Roughly, each member function of the driver Class, here Example_ARTIQ_Device
# has its own entry below.
subparsers = parser.add_subparsers(dest="subparser_name")
# Here, a python feature called a partial is used to check the parameter range
# of some passed arguments.
# https://docs.python.org/2/library/argparse.html#partial-parsing
restricted_myvar = partial(restricted_float, 0.0, 1.0)
parser_demo_exception_handling = subparsers.add_parser("demo_exception_handling",
help="demonstration of exception handling")
parser_demo_exception_handling.add_argument("myvar", type=restricted_myvar,
help="a number in the range"
"[0.0,1.0]")
parser_demo_exception_handling.add_argument("--optional_argument", default=-1, type=int,
choices=range(0, 4),
help="an optional argument to pass to demo_exception_handling")
# All the other member functions in Example_ARTIQ_Device would be parameterized
# in a similar fashion.
return parser
def _get_args():
p = define_parser()
return p.parse_args()
def main():
args = _get_args()
remote = Client(args.bind, args.port, "novatech409b")
try:
if args.verbose:
print(args)
if args.echo:
r = remote.echo(args.echo[0])
print(r)
elif args.subparser_name:
if args.subparser_name == "phase":
remote.set_phase_all(args.p)
elif args.subparser_name == "freq":
if args.channel == -1:
remote.set_freq_all_phase_continuous(args.f)
else:
remote.set_freq(args.channel, args.f)
elif args.subparser_name == "sweep-freq":
remote.freq_sweep_all_phase_continuous(
args.f0, args.f1, args.t)
elif args.subparser_name == "gain":
if args.channel == -1:
remote.output_scale_all(args.g)
else:
remote.output_scale(args.channel, args.g)
elif args.subparser_name == "reset":
remote.reset()
elif args.args.subparser_eeprom == "save-to-eeprom":
remote.save_state_to_eeprom()
finally:
remote.close_rpc()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,61 @@
#!/usr/bin/python3
# Copyright (c) 2014 Joe Britton, Sebastien Bourdeauducq
import argparse
from artiq.protocols.pc_rpc import simple_server_loop
import importlib
import logging
import artiq.devices.ExampleARTIQDevice
importlib.reload(artiq.devices.ExampleARTIQDevice)
def get_argparser():
parser = argparse.ArgumentParser(
description="example_artiq_device_controller",
epilog="This is a sample m-labs.hk ARTIQ "
"device controller.")
parser.add_argument("--bind", default="::1",
help="hostname or IP address to bind to;"
"::1 is localhost")
parser.add_argument("--port", default=3254, type=int,
help="TCP port to listen to")
parser.add_argument("--simulate_hw", action="store_true",
help="simulate hardware so ARTIQ can be used"
"outside the lab")
parser.add_argument(
"--serial_port",
default="/dev/ttyUSB0", type=str,
help="serial port: on Windows an integer (e.g. 1),"
"on Linux a device path (e.g. \"/dev/ttyUSB0\")")
parser.add_argument("--verbosity", type=int, default=1)
parser.add_argument("--log", type=int, default=30,
help="set log level by verbosity: 50 is CRITICAL, 40 is ERROR, 30 is WARNING, 20 is INFO, 10 is DEBUG")
# add additional commandline arguments here that might be needed to configure the device
parser.add_argument("--myvar", type=int, default=0,
help="example user-defined parameter")
return parser
def main():
"""
primary steps:
1) Create an instance of the device driver class Example_ARTIQ_Device.
2) Start driver event loop using simple_server_loop()
"""
# get command line arguments using the standard python argparser library
args = get_argparser().parse_args()
# start event loop
simple_server_loop(
{"example_artiq_device":
artiq.devices.example_artiq_device.ExampleARTIQDevice(
logging_level=args.verbosity,
simulate_hw=args.simulate_hw,
serial_port=args.port)},
host=args.bind,
port=args.port )
if __name__ == "__main__":
main()

View File

@ -1,16 +1,52 @@
Writing a driver
================
Writing an ARTIQ Device Driver
==============================
These instructions cover writing a simple driver for a "slow" device, that uses the controller mechanism.
These instructions cover writing a simple driver for a "slow" peripheral device.
A device driver consists primarily of three parts.
The controller
--------------
1. driver.py :: This file is where the low-level implementation details of the driver go. Things like
opening a serial connection to a device and passing it arguments. Choose a name for
your driver with alphanumeric characters and underscore. For example,
widget_233A.
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.
2. widget_233A_client.py :: This file provides a command line interface to the device. Its
an easy way to interact with the device without using the ARTIQ GUI.
The controller we will develop is for a "device" that is very easy to work with: the console from which the controller is run. The operation that the driver will implement is writing a message to that console.
3. widget_233A_controller.py :: The controller is a piece of software that receives
commands destined for driver.py from a client. The client could be my_driver_client.py
or it could be the ARTIQ GUI on another machine. The controller runs continuously in the
background waiting for requests from clients.
For using RPC, the functions that a driver provides must be the methods of a single object. We will thus define a class that provides our message-printing method: ::
driver.py
---------
The low-level implementation of the driver goes here. To create a driver.py for your device
perform the following steps. By convention the name of this file is driver.py (and nothing
else).
1. Copy everything in artiq/devices/example_artiq_device to a new directory:
artiq/devices/widget_233A/.
3. Following the template in artiq/devices/widget_233A/driver.py fill out the details of your driver. Note that the
driver functionality is encapsulated in a class, e.g. Widget233A.
4. Edit artiq/devices/widget_233A/__init__.py to reflect the path to your class.
Controller Overview
-------------------
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.
The controller we will develop is for a "device" that is very easy to work with: the
console from which the controller is run. The operation that the driver will implement
is writing a message to that console.
For using RPC, the functions that a driver provides must be the methods of a single
object. We will thus define a class that provides our message-printing method: ::
class Hello:
def message(self, msg):
@ -34,13 +70,13 @@ The parameters ``::1`` and ``3249`` are respectively the address to bind the ser
#!/usr/bin/env python3
at the beginning of the file, save it to ``hello_controller.py`` and set its execution permissions: ::
at the beginning of the file, save it to ``controller.py`` and set its execution permissions: ::
$ chmod 755 hello_controller.py
$ chmod 755 controller.py
Run it as: ::
$ ./hello_controller.py
$ ./controller.py
and verify that you can connect to the TCP port: ::
@ -56,12 +92,23 @@ Also verify that a target (service) named "hello" (as passed in the first argume
$ artiq_ctlid.py ::1 3249
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.
widget_233A_controller.py
-------------------------
Use the following steps to create a controller that works with the ARTIQ ecosystem.
1. Copy artiq/frontend/example_artiq_device_controller.py to
artiq/frontend/widget_233A_controller.py.
2. With the Controller Overview in mind follow the instructions in the example code
to flesh out the behavior of a controller for your device.
Create a ``hello_client.py`` file with the following contents: ::
Client Overview
---------------
Controller clients are small command-line utilities that expose certain
functionalities of the drivers. It's an easy way to interact with the device
without using the ARTIQ GUI.
Create a ``client.py`` file with the following contents: ::
#!/usr/bin/env python3
@ -78,21 +125,30 @@ Create a ``hello_client.py`` file with the following contents: ::
if __name__ == "__main__":
main()
Run it as before, while the controller is running. You should see the message appearing on the controller's terminal: ::
Run it as before, while the controller is running. You should see the message appearing
on the controller's terminal: ::
$ ./hello_controller.py
$ ./controller.py
message: Hello World!
When using the driver in an experiment, for simple cases the ``Client`` instance can be returned by the :class:`artiq.language.db.AutoDB` mechanism and used normally as a device.
When using the driver in an experiment, for simple cases the ``Client`` instance can
be returned by the :class:`artiq.language.db.AutoDB` mechanism and used normally as
a device.
:warning: RPC servers operate on copies of objects provided by the client, and modifications to mutable types are not written back. For example, if the client passes a list as a parameter of an RPC method, and that method ``append()s`` an element to the list, the element is not appended to the client's list.
:warning: RPC servers operate on copies of objects provided by the client, and
modifications to mutable types are not written back. For example, if the
client passes a list as a parameter of an RPC method, and that method
``append()s`` an element to the list, the element is not appended to the
client's list.
Command-line arguments
----------------------
The driver's controller should be saved in
~/artiq-dev/artiq/artiq/devices/driver_name/client.py.
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.
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 controller's code would contain something similar to this: ::
@ -146,6 +202,45 @@ The program below exemplifies how to use logging: ::
if __name__ == "__main__":
main()
The driver's server should be saved in ~/artiq-dev/artiq/artiq/devices/driver_name/server.py.
widget_233A_client.py
---------------------
Use the following steps to create a client that works with the ARTIQ ecosystem.
1. Copy artiq/frontend/example_artiq_device_client.py to
artiq/frontend/widget_233A_client.py.
2. With the Client Overview in mind follow the instructions in the example code
to flesh out the behavior of a client for your device.
setup.py
--------
Edit the entry_points section of setup.py to point to your client and controller following
the model set by example_artiq_device_client and example_artiq_device_controller.
Device Manager Udev Rules
-------------------------
On Linux systems udev is the device manager. It manages device nodes in /dev. ARTIQ drivers
often interface with instruments that have USB-Serial interfaces. Normally these devices
are assigned device nodes on an ad hock basis by the kernel (e.g. /dev/ttyUSB3); the
assignment is not deterministic. It is convenient
for device node names to be consistent for each device (e.g. /dev/artiq_ppro). This can be accomplished
by creating a udev rule (https://wiki.archlinux.org/index.php/udev).
* Get a list of device attributes. ::
$udevadm info -a /dev/ttyUSB2
*force reloading of udev
$ sudo udevadm control --reload; udevadm trigger
$ sudo vim /etc/udev/rules.d/30-usb-papilio-pro.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", ATTRS{manufacturer}=="FTDI",
ATTRS{manufacturer}=="FTDI", GROUP="plugdev", SYMLINK+="artiq_ppro"
* To confirm that everything is wroking, force reloading of udev. Then disconnect and reconnect
the serial device. ::
$ sudo udevadm control --reload; udevadm trigger
General guidelines
------------------

View File

@ -37,6 +37,8 @@ setup(
"lda_controller=artiq.frontend.lda_controller:main",
"pdq2_client=artiq.frontend.pdq2_client:main",
"pdq2_controller=artiq.frontend.pdq2_controller:main",
"example_artiq_device_client=artiq.frontend.example_artiq_device_client:main",
"example_artiq_device_controller=example_artiq_device_controller:main"
],
}
)