Device Access Library: User's Guide and Reference (draft)

Claude W. Saunders

APS Controls Group, Argonne National Laboratory

1.0 Introduction

This document is a comprehensive reference and user's guide to the device access layer. The device access layer is a software library, along with a device server and assorted utility programs. The main purpose of the device access layer is to change the manner in which equipment is referenced in an EPICS control system implementation. In addition, this layer simplifies the coding of procedural (i.e. not event driven) programs which access EPICS process variables. The device access layer resides above channel access. As such, all the capabilities of channel access are available should the programmer wish to use them.

The notion of a "device" is fundamentally different from that of a process variable. A process variable is typically one-to-one with an i/o point such as a binary-in or analog-out. A device is a higher level construct which corresponds more closely with how operators/physicists view a facility. Examples would be a power supply, or a beam-position monitor. These devices generally have a set of process variables associated with them. In the case of a power supply, there is the current setpoint, current readback, voltage readback, magnet temperature, and numerous interlocks, etc.

The device access layer considers a device to be an object which responds to a set of messages. For those familiar with object oriented programming, the semantics of devices have been designed to correspond as closely as possible with this paradigm. Another analogy would be GPIB "devices". These too respond to a set of messages, some of which entail reading the device, others which set the device. Some messages require additional arguments, such as setpoint, and others do not, such as on/off.

Standards may be adopted which state that, for example, all devices must respond to a "read" and "status" message. Writable devices must respond to a "set" message. Other standard messages such as "on" and "off" may also be adopted. Device orientation, along with these standards, provides numerous benefits. For example, the protocol for a given machine control procedure is more readily characterized as a sequence of messages to devices. The operator/physicist need not be concerned with how the device handles messages, only which messages it handles. This, in turn, frees the EPICS application developer to change the underlying implementation without affecting existing applications. More specifically, the EPICS database may be altered without affecting the fact that power supply device S1AQ1 always responds to a "set" message.

The devices discussed thusfar are referred to as atomic devices in that they are the basic unit of monitoring and control. In addition to abstracting the unit of monitoring and control from the process variable to the atomic device, the notion of a composite device is also supported. A composite device is a (possibly) heterogeneous vector of atomic devices. Messages may also be sent to a composite device. The primary difference is that a composite device, for example, will return an array of values when sent a "read" message.

Devices are not intended to replace the underlying notion of a database of process variables. When a device status is "bad", the engineer must have access to the complete set of i/o points associated with the device. In a diagnosis situation, the process variable is an appropriate level of access to the machine.

2.0 Design

A device server along with a client device library are used to implement device-oriented control. In particular, a device server was deemed necessary to allow for wildcard references to device names, and dynamic definition of composite devices.

  • 2.1 The Server

    The server is RPC (Remote Procedure Call) based. The collection of remote procedure calls (or services) is utilized by the client device library. One instance of the server runs on each workstation. Upon invocation, the server reads common text files which define the set of atomic device classes and atomic device instances for operations. These definitions are parsed using lex/yacc. Once read, no changes to the atomic device definitions may be made without restarting the server. After invocation, any number of composite devices may be defined and/or re-defined on the fly. A utility program is provided to instruct the server to scan a text file of composite device definitions. All definitions are maintained in a hash table.

  • 2.2 The Device Access Library

    A client program links with this library and may use a number of calls to control the set of defined devices. When a device is first referenced in a call, the device server is contacted to acquire all relevant information, particularly the process variables associated with that device. This information is collected in a client side hash table, so all subsequent references do not require interaction with the server. Channel access connections are established from the client only, the server is essentially just a device-to-process-variable name resolver.

    3.0 Defining Devices

    Devices are defined via text files using a simple syntax. Files containing atomic device definitions are parsed by the server when it is first invoked. Files containing composite device definitions may be parsed at any time during operations. The server at start-up time searches a default directory for files ending with the extension .cl (class definition) or .at (atomic device def). Subsequently, the scandefs utility may be used to instruct the server to read .co (composite device def) files at any time.

    3.1 Atomic Device Class Definition (.cl files)

    Defining a set of atomic devices begins by first defining the classes of devices in your control system. An atomic device class merely defines the set of messages to which an instance of that class will respond. An example definition of the DC power supply class follows. Note that any number of these definitions may be in one file.

    class dcps {

    msg set {dir: w args: 1 type: DBR_DOUBLE}

    msg read {dir: r args: 1 type: DBR_DOUBLE}

    msg status {dir: r args: 1 type: DBR_DOUBLE}

    msg readcurr {dir: r args: 1 type: DBR_DOUBLE}

    msg on {dir: w args: 0 default: 1.0 type: DBR_DOUBLE}

    msg off {dir: w args: 0 default: 0.0 type: DBR_DOUBLE}

    }

    The class name given is dcps for dc power supply. A collection of messages is then defined. Each message to which a device will respond corresponds to reading or writing a single process variable. The information following the message name (i.e. set or on) defines the action to be taken on the process variable associated with that message. Since this is just a class definition, we do not know what specific process variable will be accessed until an instance of this class is defined.

    Take the read message for example. A read message results in a read (dir: r) from the associated process variable. One value will be returned (args: 1) and will be of type DOUBLE (type: DBR_DOUBLE).

    The on message is slightly different since it takes no additional arguments. An on message results in a write (dir: w) to the PV. No arguments are supplied with this message (args: 0). A default value of 1.0 (default: 1.0) is to be written and is of the type DOUBLE (type: DBR_DOUBLE). In other words, when you tell a DC power supply "on", a 1.0 will be written to the associated PV.

    The following restrictions apply to the various attributes of messages: dir is either `r' or `w', args is either 0 or 1, type is either DBR_DOUBLE or DBR_STRING, and if default is used, it must be a number or a string depending on the type attribute.

    It is good policy to adopt a standard subset of messages for all atomic device classes. In other words, all devices should respond to a common subset of messages. It is recommended that readable devices have the "read", and "status" messages defined. Writable devices should have, in addition, the "set" message defined. Specific classes of devices will have other messages defined, however this basic subset goes a long way towards standardizing control.

    3.2 Atomic Device Instance Definition (.at files)

    Once a set of classes is defined, you then define instances of those classes. For example, a definition of a storage ring quadrupole power supply follows. Any number of these may appear in a file.

    inst S1AQ1 : dcps {

    msg set {pv: S1AQ1:CurrentAO}

    msg read {pv: S1AQ1:DacAI}

    msg status {pv: S1AQ1:StatusCALC}

    msg readcurr {pv: S1AQ1:CurrentAI}

    msg on {pv: S1AQ1:ResetSEQ}

    msg off {pv: S1AQ1:ClampBO}

    }

    The device is S1AQ1, and is of the class dcps. A process variable is given for each message. This PV in conjunction with the information given in the class definition fully specifies what operation should be performed when S1AQ1 receives a message. The process variable given for a message must correspond to the data type defined for the message (ie. DBR_DOUBLE or DBR_STRING).

    .3.3 Composite Device Definition (.co files)

    A composite device is a uniquely named device which is a vector of atomic devices. For example, a quadrupole family composite device follows. Once again, any number of these may appear in a file.

    inst QUADFAM1 : composite {

    S1AQ1

    S1AQ3

    S1BQ2

    }

    The instance QUADFAM1 is of the class composite (which is an intrinsic, predefined class). This particular composite device is homogeneous in that all of its constituents are of the dcps class. A composite device does not have to be homogeneous. For example, take the definition of an undulator-gap device which is composed of an atomic device which handles the undulator gap, followed by a set of corrector magnet devices.

    inst UNDGAP1 : composite {

    S1GAP

    S1BH3

    S1BH1

    S2AH1

    }

    The definition and use of heterogeneous composite devices requires some care. For example, if a composite device is sent a read message, each constituent atomic device must return the same data type for the read message. In other words, if we were to send UNDGAP1 a read message, we can't have S1GAP return a DBR_STRING, while S1BH3 returns a DBR_DOUBLE. Similarly, the constituents of a composite device should not execute a mix of reads and writes for a given message. When defining a composite device, the above must be taken into consideration.

    4.0 API

    The collection of functions presented here are available in libdev.a. Client programs must also include the header file "libdev.h". Section 5.0 addresses how to compile a client to make use of the library. Section 4.1 covers the machine access functions which are the core of any application. Section 4.2 covers the file utility functions which support reading data files into arrays and writing from arrays out to data files.

    Machine"4.1 Machine Access Functions

    4.1.1 Basic

    The following four functions support writing simple, procedural control code. devSend is the core function for accessing devices. devSendBlock behaves the same as devSend, however it guarantees that the associated device has completed acting on the command when it returns. This allows for precise sequencing of actions without the use of arbitrary pauses. devError checks return codes from all library functions. devCount is a utility function which returns the number and names of atomic devices in the named device.

    NAME

    devSend - send a message to a device, no blocking is provided

    SYNOPSIS

    #include "libdev.h"

    int devSend(char *dname, char *msg, void *args);

    DESCRIPTION

    Sends indicated message (msg) to named device (dname). If additional arguments are required, they are supplied via a void pointer (args). If the message implies a write operation to a double, for example, args should point to a double value. If the message implies a read operation from a double, args should point to sufficient memory in which to store the result. If the data type for the given message is DBR_STRING, the user must supply a pointer to a pointer to a char. For a read operation, devSend will take care of allocating memory for the returned string. See EXAMPLES below.

    RETURNS

    DEV_NORMAL success, msg was sent to dname along with possible args

    DEV_NOSERVER unable to contact device server to resolve dname

    DEV_CALLFAIL RPC call failed

    DEV_NODEV dname is not a defined device name

    DEV_NOMSG msg is not defined for dname

    DEV_BADSEARCH unable to ca_search on PV associated with dname

    DEV_BADCHID unable to ca_get or ca_put to PV

    DEV_TIMEOUT ca_pend_io operation timed out

    EXAMPLES

    double val = 120.4;

    status = devSend("S1AQ1","set",&val);

    double readback;

    status = devSend("S1AQ1","read",&readback);

    double array[3];

    status = devSend("QUADFAM1","read",array);

    char *strings[3];

    strings[0] = strdup("quad1");

    strings[1] = strdup("quad2");

    strings[2] = strdup("quad3");

    status = devSend("QUADFAM1","setdescription",strings);

    char *strarray[3];

    status = devSend("QUADFAM1","getdescription",strarray);

    NAME

    devSendBlock - send a message to a device and block until operation completed

    SYNOPSIS

    #include "libdev.h"

    int devSendBlock(char *dname, char *msg, void *args);

    DESCRIPTION

    Operates identically to devSend except it blocks until the EPICS record associated with the device and message has completed processing. If dname is a composite device, the call blocks until all records have completed processing. By "completed processing" it is meant that either a synchronous record has completed, or the second pass of an asynchronous record has completed. The call does not block on completion of forward link processing, only the record which is explicitly activated by the call.

    NOTE

    until blocking is supported in EPICS, this function behaves exactly as devSend does

    RETURNS

    same as devSend

    NAME

    devError - print error message to stderr

    SYNOPSIS

    #include <libdev.h>

    int devError(int status);

    DESCRIPTION

    Prints an error message to stderr according to the code supplied in status.

    RETURNS

    0 if status is DEV_NORMAL

    -1 if status != DEV_NORMAL

    EXAMPLE

    int status;

    status = devSend("S1AQ1","on");

    if (devError(status))

    exit(1);

    NAME

    devCount - return number and names of devices in dname

    SYNOPSIS

    #include <libdev.h>

    int devCount(char *dname, int *cnt, devnamearr *names);

    DESCRIPTION

    Determines the number of atomic devices in dname and places the number in cnt. Generally one only uses this function for composite devices. If dname is an atomic device, cnt will always contain 1. Also returned is an array containing the names of the atomic devices of which dname is composed. Again, if dname is atomic, only one name will be in the array. Devnamearr is an array of pointers to characters. A null pointer indicates no more elements. All space for devnamearr is allocated automatically by the call. Note: the previous contents of devnamearr will be freed up on the next call.

    RETURNS

    DEV_NORMAL success, cnt is valid

    DEV_NOSERVER unable to contact device server to resolve dname

    DEV_CALLFAIL RPC call failed

    DEV_NODEV dname is not a defined device name

    EXAMPLE

    int count, status, i;

    devnamearr mynames;

    status = devCount("QUADFAM1", &count, &mynames);

    for (i=0 ; i < count ; i++)

    printf("device %d is %s\n",i, mynames[i]);

    4.1.2 Advanced

    The advanced functions allow more complex, general purpose control applications to be written. In general, these functions assume an understanding of structures in C. devMatch returns an array of device names which match a given wildcard pattern. devInfo returns the complete set of information the device server has on the given device (ie. supported messages and the associated PV names). devDirType works in conjunction with devInfo and returns the direction and data type for a given message to a device. devErrorString is the same as devError only it returns an error string instead of printing it. devChid returns a channel id (chid) structure for the given device and message. This provides a hook to the channel access library and all its support for event-driven applications (ie. installation of callback functions). devTimeout allows one to adjust the waiting period for pending io operations.

    NAME

    devMatch - returns an array of device names which match wildcard pattern

    SYNOPSIS

    #include <libdev.h>

    int devMatch(char *pattern, devnamearr *matches);

    DESCRIPTION

    A device name with the wildcard characters `*' and `?' is given as the pattern. The pattern has the same semantics as filename completion in UNIX (ie. * matches anything and ? matches any single character). Returned is an array of all atomic and composite device names which match the pattern. Devnamearr is an array of pointers to characters. A null pointer indicates no more matches. All space for devnamearr is allocated automatically by the call. Note: The previous contents of devnamearr will be freed up on the next call.

    RETURNS

    DEV_NORMAL success, array of matches is valid

    DEV_NOSERVER unable to contact device server to look for matches

    DEV_CALLFAIL RPC call failed

    EXAMPLE

    devnamearr mynames;

    int status, i;

    status = devMatch("S*AQ?",&mynames);

    i = 0;

    while (mynames[i] != NULL) {

    printf("matched %s\n",mynames[i]);

    i++;

    }

    NAME

    devChid - returns the channel id structure associated with given atomic device and message

    SYNOPSIS

    #include <libdev.h>

    int devChid(char *dname, char *msg, chid **pchid);

    DESCRIPTION

    Provides a hook to the Channel Access library by returning an initialized chid for the named device and message. Dname must be an atomic device. Any of the Channel Access calls which require a chid may now be used. Should the chid become "disconnected", devChid may be called again to re-retrieve a valid connection.

    RETURNS

    DEV_NORMAL success, initialized chid is returned

    DEV_NOSERVER unable to contact device server to get neccessary information

    DEV_CALLFAIL RPC call failed

    DEV_NODEV dname is not a defined device name

    DEV_NOMSG msg is not defined for dname

    DEV_BADSEARCH unable to ca_search on PV associated with dname

    DEV_BADCHID unable to ca_get or ca_put to PV

    DEV_TIMEOUT ca_pend_io operation timed out

    DEV_ATOMICONLY call accepts atomic device names only

    EXAMPLES

    XXX

    NAME

    devInfo - returns an array of device structures containing information about given device

    SYNOPSIS

    #include <libdev.h>

    int devInfo(char *dname, devstructarr *devinfo);

    DESCRIPTION

    An atomic or composite device name is given in dname. Returned is an array of device structures containing all information the server has about that device. If dname is an atomic device, there is an element for each message the atomic device supports. If dname is a composite device, there is an element for each message of each constituent atomic device. A NULL pointer indicates no more elements. All space for devstructarr is allocated automatically by the call. Note: the previous contents of devstructarr will be freed up on the next call.

    RETURNS

    DEV_NORMAL success, devstructarr contains results

    DEV_NOSERVER unable to contact device server to get information

    DEV_CALLFAIL RPC call failed

    DEV_NODEV dname is not a defined device name

    EXAMPLES

    devstructarr myinfo;

    int i, status;

    status = devInfo("S1AQ1",&myinfo);

    i = 0;

    while (myinfo[i] != NULL) {

    printf("%s %s %s %d\n",

    myinfo[i]->dname, myinfo[i]->msg, myinfo[i]->pvname, myinfo[i]->dir);

    i++;

    }

    NAME

    devDirType - given a message and the result of devInfo, returns direction and data type.

    SYNOPSIS

    #include <libdev.h>

    int devDirType(char *msg, devstructarr devinfo, int *dir, int *type);

    DESCRIPTION

    This function works in conjunction with devInfo() and is used as follows: The devInfo() function returns devinfo for a given device. This is then passed to devDirType along with a particular message. devDirType then returns the direction of io (READ or WRITE) and the data type (DBR_DOUBLE or DBR_STRING) that would result if the given message were sent to the device. This information is neccessary when coding a general purpose utility program in which the user may arbitrarily send any message to any device.

    RETURNS

    DEV_NORMAL always

    EXAMPLE

    devstructarr myinfo;

    int i, status, dir, type;

    status = devInfo("S1AQ1",&myinfo);

    status = devDirType("status", myinfo, &dir, &type);

    printf("a status message results in a %s to a process variable of type %s\n",

    ((dir == READ)?"read":"write"), ((type == DBR_DOUBLE)?"double":"string"));

    NAME

    devErrorString - copy error string to buffer

    SYNOPSIS

    #include <libdev.h>

    int devErrorString(int status, char *buf);

    DESCRIPTION

    Copies an error message to buf which corresponds to the error code supplied in status. Error string will be 80 characters or less in length.

    RETURNS

    0 if status is DEV_NORMAL

    -1 if status != DEV_NORMAL

    NAME

    devTimeout - sets timeout in seconds for pending input/output operations

    SYNOPSIS

    #include <libdev.h>

    int devTimeout(float timeout);

    DESCRIPTION

    Accumulated input/output operations are flushed onto the network using the channel access function ca_pend_io(). For example, a devSend() call results in a channel access put or get, which is subsequently flushed out on the network with the ca_pend_io() function. Should the flushing be delayed for any reason, a time-out period is given. By default, this period is 5 seconds for all input/output in the device access layer. This timeout may be globally adjusted with the devTimeout call.

    RETURNS

    DEV_NORMAL always

    5.0 Compiling Client Programs

    The device access library utilizes the channel access library. As such, you must add to your include and library paths in order to access the needed header files and libraries. For EPICS R3.11, the following makefile is suggested:

    CC = /usr/lang/acc

    INCLUDES = /net/phebos/epics/R3.11/share/epicsH

    LIBS = -L/net/phebos/epics/R3.11/Unix/sun4/bin -L.

    CFLAGS = -DUNIX -USUN4 -I$(INCLUDES)

    myapp: myapp.c

    $(CC) -o myapp $(LIBS) myapp.c -lrpcsvc -ldev -lca -lUnix

    Note that CC is the Sun ANSI C compiler. This is recommended over the default K&R C compiler. The INCLUDES and LIBS paths are installation dependent, however they should point to the latest epicsH and sun4/bin directories. This may be direct, as shown, or via a privately installed release of EPICS.

    Note that the order of library inclusion is important. Rpcsvc is the SunOS Remote Procedure Call library, neccessary to access the RPC based server. Dev is the actual device access library. Ca and Unix are the channel access libraries.

    Naturally, execution of your application presumes that you have defined the devices for your installation, and have invoked the device server on your workstation (see next section).

    6.0 Managing the Device Server

    The device server may be started manually from a shell, or added to the workstation boot file. The server utilizes one environment variable, DEV_DEF_PATH. This variable tells the server what directory to scan for atomic device class (.cl files) and instance definitions (.at files) required at startup. Any composite definitions (.co files) in this directory will also be scanned. If DEV_DEF_PATH is not set, the current working directory in which the server is invoked is searched. The server backgrounds itself and detatches from the terminal session. As such, you must use "ps -aux" to find the process.

    You must be on the console of the machine on which you are starting the server. The only exception to this is if you are logged in as root. Errors encountered in startup are reported on the console, as well as the system log file /var/adm/messages.

    Invoking from a shell (sh) or a bootfile is simply:

    (export DEV_DEF_PATH ; DEV_DEF_PATH=/usr/local/lib ; idev_svc)

    The system log file or console should be checked to make sure no errors occurred during startup. The format of error and log messages has yet to be finalized, but at present it is fairly self explanatory.

    If multiple instances of the device server are invoked, client programs will contact the most recently started.

    7.0 Client Applications

    The following applications are supplied with the device server. The first, scandefs, is used to define and/or redefine composite devices. devSend is a command line utility which will send messages to devices. It is really just a simple C program which in turn calls the devSend() library function. This is useful for testing purposes. devBackup and devRestore allow a snapshot of device setpoints to be taken and later restored.

    7.1 scandefs

    NAME

    scandefs - instructs device server to scan a file of composite device definitions

    SYNOPSIS

    scandefs file

    DESCRIPTION

    Contacts device server on host workstation and requests that the given file (which must end in .co) be scanned. The file may contain any number of composite device definitions (see Section 3.3). New definitions are added, and old ones are redefined. Note that redefinition does not imply that currently executing clients are notified of a change. Client applications may need to be restarted to use the new definitions.

    7.2 devSend

    NAME

    devSend - send a message to a device, possibly with arguments

    SYNOPSIS

    devSend devname msg [arg0...argn]

    DESCRIPTION

    Sends msg to devname. If msg implies a write operation, then the values are supplied via arg0...argn. If msg implies a read operation, then the device's response is printed to stdout. The device may be atomic or composite. When a read occurs, the response to stdout will be one of the following four:

    devname valid-value

    devname NOCONNECT

    devname NOHANDLE

    devname BADSTATUS

    If everything succeeds, the normal response is to print the device name followed by the value read from the device. If devSend was unable to contact the device, the keyword NOCONNECT is printed in place of the value. If the given message is not defined for the device, the keyword NOHANDLE is printed. Lastly, if a status message is defined for devname, and the device responds with a non-zero status, the keyword BADSTATUS is printed.

    EXAMPLES

    devSend S1AQ1 set 120.3

    devSend S1AQ1 read

    S1AQ1 120.30000

    devSend S1AQ1 read

    S1AQ1 BADSTATUS

    devSend S1AQ1 readcurr

    S1AQ1 NOCONNECT

    devSend QUADFAM1 set 120.2 109.3 134.2

    devSend QUADFAM1 read

    S1AQ1 120.20000

    S1AQ3 109.30000

    S1BQ1 134.70000

    devSend S1AQ1 off

    devSend S1AQ1 getdescription

    S1AQ1 sector 1 subsector A quadrupole 1

    S1AQ3 sector 1 subsector A quadrupole 3

    S1BQ1 sector 1 subsector B quadrupole 1

    7.3 devBackup and devRestore

    NAME

    devBackup - using a specification file, generate a snapshot file of machine data

    SYNOPSIS

    devBackup [-m message] < spec-file > snap-file

    DESCRIPTION

    Takes a spec-file (text file with list of device names) and performs a read on each device. The results are output to snap-file (text file with device name/value pairs). By default, each device is sent the "read"message unless otherwise specified by the -m message option.

    The spec-file may contain the "#include" directive to incorporate other specification files. In addition, the devices listed in the spec-file may contain the wildcard characters `*' and `?'. Any devices which match the wilcard device name will be read.

    Note that the keywords NOCONNECT, BADSTATUS, and NOHANDLE may be in the snapshot. See section 7.2 (devSend) for a detailed description.

    EXAMPLE

    Given the following specification file (myspec):

    S1AQ3 /* atomic device */

    S1AQ2 /* atomic device */

    LBUMP /* composite device */

    devBackup < myspec > mysnap

    The snapshot file mysnap now contains:

    S1AQ3 345.32

    S1AQ2 352.94

    S1AH1 12.231 /* note: composite device resolved into its components */

    S1AH2 -3.458

    S1A:H3 BADSTATUS

    OR, to get a snapshot of the device current transductor value instead of the DAC setpoint,

    devBackup -m readcurr < myspec > mysnap

    NAME

    devRestore - set the machine using the given snapshot file

    SYNOPSIS

    devRestore [-m message] < snap-file

    DESCRIPTION

    Takes a snap-file (text file with device name/value pairs) and writes the data to the devices. By default, each device is set by sending the "set" message. The -m message option may be used to write the data using a different message.

    EXAMPLE

    Using the mysnap file shown in the examples for devBackup, the machine may be set as follows:

    devRestore < mysnap

    8.0 Library Development Plans

    Development of the device access library is ongoing. Most work will not affect the current API, however this is not quaranteed presently.

    * Consider replacing use of SunRPC with DCE RPC.

    * Add support for arbitrary hierarchies of composite devices (should be there anyway).

    * More strictly specify and enforce syntax of device definitions.

    * Support a single-server option, and add broadcast "maintenance" services to existing server.

    * Consider use of CORBA IDL syntax for defining devices.

    * Consider use of CORBA dynamic invocation interface as a model for device access API.

    * Implement devSendBlock when channel access blocking becomes available.

    * Remove devBackup/devRestore from release, since new BURT will cover devices.

    * Consider extending functionality of device definitions. Provide inheritance mechanism. Provide a mechanism to attach a C function or application to a device's message.