Skip to content

Writing Servers

ejeffrey edited this page Apr 11, 2016 · 13 revisions

Basics of pylabrad servers

You can write a server simply by subclassing the LabradServer class and using the @setting decorator

from labrad.server import LabradServer, setting
from twisted.internet.defer import inlineCallbacks, returnValue

class MyServer(LabradServer):
    name = "My Server"    # Will be labrad name of server
    
    @inlineCallbacks
    def initServer(self):  # Do initialization here
        pass

    @setting(10, data='?', returns='b')
    def is_true(self, c, data):
        return bool(data)

__server__ = MyServer()

if __name__ == '__main__':
    from labrad import util
    util.runServer(__server__)

Setting decorator

The @setting decorator takes several arguments:

  • The first positional argument is the setting ID number. These must be unique across all settings in a single server. The actual numbers don't matter, they're there so that requests coming in can be routed to the right setting.

  • An optional second argument which is a string specifying this setting's name. If this argument is omitted then the name of the setting is inferred from the python function name.

  • Any number of keyword arguments whose names match the parameter names of the method being decorated. The values passed to these keyword arguments can be a string or list of strings specifying the labrad types that are allowed for that parameter. For example, if parameter foo must be a string, then you would pass foo='s' to the @setting decorator. If the parameter foo can accept multiple types, say a string or an int, then you would pass a list of type tags: foo=['s', 'i']. If no types are specified in this way, then we will accept any labrad type for that parameter.

  • A keyword argument returns which specifies the type tag (or list of possible type tags) of the returned data.

  • A keyword argument unflatten which, if False, suppresses the unflattening of the data received over the network. Instead, the method will be passed FlatData objects. These can be passed along to other servers without incurring the expense of unflattening and flattening the data.

Here is an example of a cd (change directory) command for the registry:

    @setting(10, 'cd', path=['s', '*s', 'w'], returns='*s')
    def chdir(self, c, path=None):  # Path can also be omitted.
        '''Code goes here'''

This setting will be called cd on the LabRAD system. The path can be given as a string, list of strings, or unsigned integer. The setting returns a list of strings.

Parameters with default values

Note that in the previous example, the path argument to the chdir defaults to None. This means that the argument can actually be omitted, in which case it will take the default value None in the python code. In this latter, we might be tempted to add '_' to the list of accepted types for the path parameter, but it is best not to do this and just let pylabrad recognize that the param can be omitted because it has a default value.

The reason for not including '_' in the list of accepted types for a parameter becomes more clear when we consider a setting with more than one parameter. In labrad as it currently stands, multiple arguments are acutally passed as a cluster, but clusters cannot contain None as members. So, suppose you had three string arguments to a setting, the latter two of which have default values:

@setting(1, a='s', b='s', c='s')
def get(self, ctx, a, b=None, c='test'):
    pass

In principle we'd like to be able to omit any combination of parameters with default values, for example by passing data of type (ss_) or (s_s), but those are not valid data, so it is not really correct to say that b or c accept '_' as input. However, because there are default values defined in python, the decorator will detect that and allow (ss) or just s as accepted types for the entire setting. When it gets one of these it unpacks them to the appropriate number of params and the rest get the python defaults.

So, if you have default values, don't add '_' as an accepted labrad type for that parameter; the @setting decorator will generate accepted types with various numbers of trailing params omitted (while this would work for a single parameter, we suggest not doing it, just to keep things consistent). When you call a setting with default args, you can drop the last param, or both of the last two, or in general all of the last k, but you can't pick and choose like with regular python named args and default values.

(This restriction on not allowing None inside a cluster may be relaxed in labrad 2, which would make the calling conventions more flexible. In particular, it would be possible to specify any subset of params with default values.)

Setting Metadata

Based on the information included in the setting decorator, pylabrad assembles metadata to send to the manager when registering the setting. To make this metadata as informative as possible, it includes comments in the registered accepted type patterns which include the parameter names as well as information about parameters which will get default values. As an example, if we have a setting like:

@setting(1, foo='s', bar='i', baz='t')
def blah(self, c, foo, bar, baz=None):
    """Blah blah blah"""
    ...

Then pylabrad will include the docstring "Blah blah blah" as the setting doc and will tell the manager that the accepted types are (s{foo}, i{bar}, t{baz}) and (s{foo}, i{bar}): default: baz=None. pylabrad tries to make these accepted types as informative as possible without our having to do too much beyond just writing a python method and decorating it with the @setting decorator, but it helps to know a bit about how this works to make the documentation sensible.

Note that in the setting decorator we give a tag for each parameter, but the full accepted type combines all the parameter types into a cluster. This means that if we include comments in the individual tags we specify for the parameters, the comments may be lost when pylabrad tries to assemble the full accepted type for the setting. If there is only one parameter, we don't have to combine these tags, so the comments can get through:

# bad; when we make the combined tag (i{a}, s{b}) we lose the trailing comments
@setting(1, a='i: a really important integer', b='s: an important string')
def not_good(self, c, a, b):
    """my setting"""

# meh; since there's only one parameter, the tag we give for accepted types is all there is
@setting(2, a='i: a really important integer)
def ok_i_guess(self, c, a):
    """another setting"""

Keeping this straight is tricky. The recommended way to handle this is a follows:

  • In the setting decorator just write bare type tags. If there are multiple parameters, pylabrad will add comments with the parameter names to the accepted types.

  • Explain different parameter types in detail in the function docstring. This text will be visible to other labrad clients, and you can talk about what the parameters mean without worrying about this text getting lost due to some magic.

  • Note that docstrings on remotely-accessible settings are visible to the world, so you should describe the interface to the method and what it does, but it doesn't make much sense to discuss internal implementation details. Those can go in comments after the docstring. This applies to other python code as well, but it's worth remembering here in particular, because presumably the clients of your server really don't care how it does its job, just that it does so.

Servers acting as clients

Many servers need to make requests to other servers. Each server has a 'client' object for this purpose:

    @setting(15, key='s', returns='?')
    def get_registry_key(self, c, key):
        p = self.client.registry.packet()
        p.get(key)
        result = yield p.send(unflatten=False)  # Always wait=False
        returnValue(result['get'])

Notice that servers always make asynchronous requests, so we must use yield to get the value of the Deferred. We then must use returnValue to send the result back, just as if this were an inlineCallbacks method. unflatten=False tells pylabrad to not unflatten the data, which will just be returned as a FlatData object. Since the result of the get() call is just sent back on the wire, this avoids unflattning/flattening the data entirely.

Contexts

The second argument to every setting function (after self) is the context, usually called c. This allows the server to store state on a per-client basis. It acts like a dictionary which the server implementation is allowed to store arbitrary keys. It also has the attribute c.ID containing the ID of the client making the request. There are two special methods that a server can override: initContext(self, c) and expireContext(self, c). These are called the first time a client uses a specific context, and when the context expires (usually because the client disconnected from the labrad manager).

Signals

LabRAD support signals. These are messages sent by servers triggered by an external event, rather than as a response to a specific client request. For instance, the data vault sends a signal to every listening client when a new file is created. This allows e.g. the grapher to update its display without polling the server. Signals are declared in pylabrad servers like so:

from labrad.server import LabradServer, Signal, setting

class SignalTestServer(LabradServer):
    onNotification = Signal(1234, 'signal: test', 's')
    @setting(10, message='s')
    def notify_clients(self, c, message):
        self.onNotification(message)  # send the message to all listening clients