This document describes how to write and integrate new equipment classes into AVE:
Note that these are generic guidelines. Equipment sometimes has exotic features or limitations that require more work for full and seamless integration.
Also see the Structural Guidelines for requirements on the new module’s file and directory structure.
Code examples are given for a hypothetical washing machine device that may be controlled over a USB connection.
Equipment fall into three broad categories:
The equipment must have a globally unique serial number (or similar attribute) that can be read without claiming the device. This is necessary to monitor the equipment’s basic connected/disconnected state in the broker after another process has claimed the device (i.e. a test job).
You can check if a piece of equipment passes this requirement by inspecting it with:
lsusb -v
This lists all USB equipment on the host. A SoMC handset entry will look something like this:
Bus 002 Device 009: ID 0fce:5189 Sony Ericsson Mobile Communications AB
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 0 (Defined at Interface level)
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x0fce Sony Ericsson Mobile Communications AB
idProduct 0x5189
bcdDevice 2.28
iManufacturer 1 Sony
iProduct 2 C6503
iSerial 3 CB5121X6KM
# 80 lines of output removed for clarity
The device descriptor field iSerial is set to "CB5121X6KM". This field may be read by any program at any time, without claiming the device.
Warning
If the equipment does not expose a unique serial number, then it may be very hard or impossible to support more than one device per host machine. The equipment vendor can tell you if it uses unique serial numbers.
The device descriptor’s idVendor and idProduct values should identify the equipment’s type. (Other options may exist depending on the equipment. See iManufacturer and iProduct in the previous example.) This is normally not a problem, but some equipment will be based on Arduino boards (or similar) and have the VID/PID of that product rather than the fully developed equipment. In such cases it may be very difficult to tell apart two units that are based on the same development board.
An implementation may perform very limited network host/port scans to detect this kind of equipment. However, it may be easier and safer to just record the identity statically in a configuration file under .ave/config. Networked equipment has its own set of problems because they are normally not allowed on the common intranet and must be walled off in special lab networks.
The equipment host normally runs some sort of server software that came with the equipment. It is very valuable if this server can answer questions about the current state of the equipment, without claiming it for the caller. This can then be used to improve the equipment listing in the broker. Often this kind of equipment can be used manually and the broker needs a way to determine this before allocating the equipment to a test job.
Example
The Spirent GPS simulator is handled this way. The lab owner records the host name of the simulator host in .ave/config/spirent.json. The equipment is listed based on that and a globally unique identifier (also selected by the lab owner).
The equipment host runs a server called TestDrive that controls the simulator. This server can not answer questions about equipment state without claiming the equipment for the caller. This has negative consequences:
Of course this simulator is so valuable that it is supported anyway, but with limitations on use. A TestDrive host that is linked into a DUST lab can never be used for manual work. Conversely a host that is used for manual work cannot be linked into a DUST lab.
Examples:
Implementing listers for this kind of resource needs special solutions. E.g. the broker does not list a workspace until it has been allocated/created. If a job asks for a workspace that already exists, the broker will check that the session actually owns the wanted workspace. A built in WLAN interface would have to be listed based on ifconfig output (or similar) rather than USB device descriptors (which can be used for WLAN dongles).
The broker deals only in resource profiles and has very little other knowledge about the equipment. All profiles must tell the broker what it is, which one it is (even when there can only be one device per host) and if the equipment is currently connected.
The minimum profile for the hypothetical USB Washer equipment could look like this:
{
"type" : "washer",
"uid" : "washer-1",
"power_state": "online"
}
Profiles may reveal other information about the equipment by adding more fields.
The profile class must inherit from ave.profile.Profile and implement this API:
from ave.profile import Profile
class WasherProfile(Profile):
def __init__(self, values):
if type(values) != dict or 'type' not in profile:
raise Exception('invalid profile: %s' % values)
if values['type'] != 'washer':
raise Exception('not a washer profile: %s' % values)
if ('uid' not in values
or type(values['uid']) not in [str,unicode]):
raise Exception('invalid washer profile: %s' % values)
Profile.__init__(self, values)
def __hash__(self):
return hash(self['uid'])
def __eq__(self, other):
return self['uid'] == other['uid']
def __ne__(self, other):
return not self.__eq__(other)
def minimize(self, profile=None):
# mandatory fields:
r = {
'type' : 'washer',
'uid' : self['uid']
}
# allocation profile fields:
if profile:
r.update(profile)
return HandsetProfile(r)
def match(self, profile):
Profile.match(self, profile)
Notes
When a client lists equipment, it passes a profile to match (the allocation profile) and the broker uses match() on all profiles it has received from any equipment lister. The returned result is a list of full profiles (not minimized).
When a client allocates equipment, the same matching happens again, but this time the selected equipment profile is minimized against the allocation profile and the result is added to the client’s session. An instance of the correct equipment class is created by the session which passes the instance profile to the class’ constructor.
Note
The lister, broker and session are all separate processes. This is a design chosen for address space safety, but adds the requirement that listers must be able to create correct profiles without claiming the equipment (because it may be claimed already by a session).
Small but important changes:
The mandatory API that supports broker integration is concentrated into the equipment class’ constructor. The constructor must accept a profile and a home directory path. If the home directory is not set, then the constructor must find it by calling ave.config.load_etc().
import ave.config
from ave.washer.profile import WasherProfile
class Washer(object):
profile = None
home = None
def __init__(self, profile, home=None):
if type(profile) != WasherProfile:
raise Exception('complaints, always complaints')
if not home:
home = ave.config.load_etc()['home']
self.profile = profile
self.home = home
def __del__(self):
# clean up. especially terminate any started processes.
Warning
The constructor must not perform other kinds of IO, start processes or do anything that adds uncertainty to its time complexity. The broker’s call to Session.add_resource() blocks until the constructor returns. This means no other client can make broker calls in the meantime.
Note
The lister may include fields in the profile that are needed by the equipment class to implement “behind the scenes” mechanisms. E.g. it may include the device node path in the profile so that the equipment class may know which device to actually open. Just make sure this field is included among the mandatory ones in your Profile.minimize() method.
There are also some rules that must be observed when implementing the rest of the equipment class:
Also see the Source Code Style Rules.
Common requirements on listers:
- Should be based on udev event handling when possible.
- Must be implemented as a full process. The class must then inherit from ave.network.process.Process. (Exception made for Spirent simulator lister which only reads a static configuration file. The equipment does not support any form of automated detection.)
- Must post created profiles to the broker using Broker.add_equipment().
- Must be startable from the broker.
- Must permit multiple parallel instances. No singleton patterns.
- Must not claim the equipment. If a unique identifier cannot be read without claiming the equipment, then please do not add support for it. Note that paths under /dev change when the device is unplugged and replugged, so these are useless for identification.
- Must handle SIGUSR1 for diagnostic and debugging purposes.
Note
Skip this chapter if your equipment is limited to static listing.
There is no common base class for listers. There should be one, but for now an existing implementation will have to do as example:
module: | ave/handset |
---|---|
file: | src/ave/handset/lister.py |
Listers must accept the port and authkey parameters. They will be passed by the broker and are used by the lister to “call back” to the broker with admin privileges. The port number and authentication key are not read from files in .ave/config/ because test cases must be able to set these to alternative values.
The handset lister also accepts paths for testing purposes. It allows a test case to create a lister that only looks at devices whose sysfs paths appear in the list of paths.
Use ctypes to load libudev.so. Python 2 has no standard library module to interface with udev so we implement a wrapper ourselves. The assignments found in this method declare the C types for parameters and return values of all functions we need to use in libudev. Python then handles translation of Python parameter types to their C equivalents automatically.
Set handlers for SIGTERM (terminate the process without running Python exit handlers) and SIGUSR1 (ignored, but you may dump whatever state you want, in any way you want).
Note the call to set_PDEATHSIG() which causes SIGTERM to be sent to the process when its parent dies. The broker should terminate listers gracefully, but in case of a crash this makes sure the lister does not live on. It will automatically reparent to the init process if this is not done.
First function to run in the new lister process. Set up signal handlers, create the ctypes wrapper and list all equipment once. Then enter a select loop that reacts on udev events.
Modify these as needed:
Modify this if your equipment is not a USB device:
Copy as is.
Note the handling of action=="remove". The lister must tell the broker which device went offline by posting a profile that contains { "power_state": "offline" } and the uniquely identifying attribute (the serial number). Unfortunately, the profile can’t be built because the device was just unplugged and can’t be inspected. Instead recover the serial number from a dictionary that maps sysfs paths to serial numbers. The sysfs path is determined entirely by the physical USB port the device was connected to. It only changes if the device is plugged into a different port, or if a USB hub is inserted or removed in between the host and the device.
Remember to delete the indexed serial number (or whatever state you stored) at the end of handling action=="remove".
The handset lister spawns a new process for each handset. Each process calls the broker with updates whenever an important attribute of the handset changes. The use of processes is not normally necessary but some handset attributes can take very long to settle (e.g which GSM network is signed on to). A lister that builds the profiles quickly can just make the call to Broker.add_equipment() from the lister’s main process and be done with it. See use of RemoteControl in report_to_broker() for how to perform the RPC.
Self tests are required for both the equipment class and the lister class. Test cases for the equipment class should of course cover all public API’s. Listers must also have tests that check for broker integration problems.
For concrete examples, please check the existing lister and broker tests for e.g. relays, power meters and handsets.
Note
The tests described here should of course all fail if no equipment is connected to the test host.
Automated tests:
Semi-automated tests:
These tests require a version of the broker that has already been patched to support the new equipment class. Implement the tests in the equipment class’ git tree, not in the broker’s.
Automated tests:
It is a hassle to have to make and install AVE and restart the broker if needed The developer can avoid all of this by replacing an RPC handle from Broker.get() with the real equipment class in the test cases. This trick is used in all of AVE’s equipment support modules. Consider this simplified handset example:
# runners.py
from ave.broker import Broker
from ave.handset.handset import Handset
from ave.handset.profile import HandsetProfile
def all_tests(local):
b = Broker()
h = b.get({'type':'handset'})
if local:
p = HandsetProfile(h.profile)
h = Handset(p)
result = True
result &= t1(h)
result &= t2(h)
result &= t3(h)
return result
You should have two versions of every test job: One that calls run_tests() with local set to False and one that sets it to True. The second version must also manipulate Python’s search paths to prefer your working tree over the system installed version of the package. Example executable:
#! /usr/bin/python2
# only import non-AVE modules here:
import os
import sys
if __name__ == '__main__':
# manipulate python's search paths:
path = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
path = os.path.join(path, 'src')
sys.path.insert(0, path)
# must not import runners or *any* module that imports an AVE module
# before the search paths have been manipulated. otherwise all system
# installed versions of all AVE packages are preferred over the local
# tree implementation.
import vcsjob
import runners
try:
result = runners.all_tests(local=True)
if result == True:
sys.exit(vcsjob.OK)
sys.exit(vcsjob.FAILURES)
except Exception, e:
print e
sys.exit(vcsjob.ERROR)
Check the notes about src/ave/__init__.py in the Structural Guidelines to complete this setup.
API’s must be documented in dedicated files under the module tree’s docs/ folder and must be written in reStructuredText. Do not generate doc strings from source code comments. There are several examples to download and take guidance from.
To include the new documents in the user’s guide, patch the module ave/documentation:
Create a symlink that points to the new module’s root directory (which must be “neighbours” with the documentation tree in a common superdirectory).
Include the new document in the user’s guide’s master index.rst. Use an include path that traverses the symlink.
Build everything by running make without arguments:
cd documentation
make
chromium-browser _build/html/index.html &