Claude W. Saunders
APS Controls Group, Argonne National Laboratory
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.
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.
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.
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.
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.
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.