[Next] [Previous] [Top] [Contents] [Index]

Channel Access Client Tutorial

2. Reading PV Values

A Simple Program

This first program is a simple program called ezee that reads the value of a PV and prints its value to standard output. When the program is run, the user should pass the name of a valid PV to the program from the command line as in

	%ezee myPV
Basically, the program establishes a connection to the PV, reads its value, prints its value, and then exits. Although simple, it should demonstrate some basic features of Channel Access. Here's the code:

/* 1. Include appropriate files. */
#include <stdio.h>
#include <cadef.h> /* EPICS specific */

#define USAGE "usage: ezee <PV name>"

int main(int argc, char **argv)
{
	chid chan;
	dbr_string string;
	int  status;

	if(argc != 2){
		printf("%s\n", USAGE);
		exit(-1);
	}

	/* 2. Initialize Channel Access. */
	status = ca_task_initialize();
	SEVCHK(status, "Unable to initialize");
	if (status != ECA_NORMAL)
		exit(-1);

	/* 3. Make search request. */
	status = ca_search(argv[1], &chan);
	if (status != ECA_NORMAL){
		printf("%s: problem establishing connection to %s.", camessage(status), argv[1]);
		exit(-1);
	}

	/* 4. Send the request and wait for channel to be found. */
	status = ca_pend_io(1.0);
	if (status != ECA_NORMAL){
		printf("%s: Not Found\n", argv[1]);
		exit(-1);
	}

	/* 5. Make get request. */
	status = ca_bget(chan, string);
	SEVCHK(status, "Error in call to ca_bget()" );

	status = ca_pend_io(1.0);
	if (status != ECA_NORMAL){
		printf("%s: Get Timed Out\n", argv[1]);
		exit(-1);
	}
	printf("%s\n", string);

	/* 6. Free resources and close channel access. */
	ca_task_exit();
	return(0);
}
Any client application uses one or more functions from the client library. Most client library calls follow the format ca_xxx() as in ca_task_exit(). An exception is SEVCHK(), which is really just a macro-envelope around another routine called ca_signal(). Like SEVCHK(), many client calls are macros or aliases of other calls. For instance, the call in this program that is used to read a PV's value is called ca_bget(), which is really an alias of ca_array_get().

There are six parts to our program, each numbered in the comment that precedes it. Each of these parts is discussed below along with some basic Channel Access concepts.

1. Include Files and Program Compilation

All programs that are going to use one or more functions from the Channel Access client library must include the cadef.h header file which can be found in the $EPICS/base/include directory. The header file cadef.h causes db_access.h, caerr.h, and caeventmask.h to be included also. These EPICS header files are necessary in order to use the client library.

	/* 1. Include appropriate files. */
	#include <stdio.h>
	#include <cadef.h> /* EPICS specific */
To compile a client program, it must be linked with the libraries libca.a and libCom.a. These libraries are in $EPICS/base/lib/$ARCH. Assuming our C compiler was cc, we could compile ezee with the following command:

	 cc ezee.c -o ezee -L$EPICS/base/lib/$ARCH -lca -lCom -I$EPICS/base/include
where $EPICS is a variable that indicates the top level of the EPICS build on the system and $ARCH is a variable representing the name of the target architecture, sun4, for instance.

2. Initializing Channel Access

Each client application should call ca_task_initialize() before calling any other functions in the client library. Calling ca_task_initialize() initializes the Channel Access library for a process, or a group of tasks in a multi-threaded OS. If a program or task group does not call this function, it is automatically invoked at the first call to any routine from the client library and is thus optional, but still considered good practice.

Our program calls ca_task_initialize() after it has checked to see if the proper number of arguments have been passed to main():

   	/* 2. Initialize Channel Access. */
	status = ca_task_initialize();
	SEVCHK(status, "Unable to initialize");
	if (status != ECA_NORMAL)
		exit(-1);
Notice that the variable status is used to hold the value returned from ca_task_initialize(). Nearly all client library routines return an integer that represents one of a number of possible status codes. For example, ca_task_initialize() returns one of two possible status codes, ECA_NORMAL and ECA_ALLOCMEM. If ca_task_initialize() returns ECA_NORMAL, then Channel Access client library was successfully initialized. If it returns the other status code, ECA_ALLOCMEM, then Channel Access wasn't successfully initialized due to lack of available memory.

All calls that return a status code will return ECA_NORMAL to indicate success. To indicate an error, each returns one of a number of possible status codes. The possible error codes for each call may differ. For example, the possible error indicators for ca_get() are ECA_BADCHID, ECA_BADTYPE, ECA_BADCOUNT, and ECA_GET_FAIL. Each error code has a message associated with it that describes the error. The message for ECA_NORMAL is

	Normal successful completion.
The header file caerr.h lists all status codes and their associated messages. In addition, the routine ca_message(), when passed a status code, will return a string containing the message associated with the code.

All status codes have an associated severity level, which can be one of five: SUCCESS, INFO, WARNING, ERROR, and FATAL. ECA_NORMAL is the only code which has SUCCESS as its severity. All other codes have one of the other severities, where INFO is the least severe and FATAL is the most.

A convenient way to check error status codes returned by client calls is the SEVCHK() macro. The first argument passed to SEVCHK() should be the status code returned by the client call, and the second argument should be a message to be printed when the code indicates an error. Our program passes the status code returned by ca_task_initialize() to SEVCHK() in addition to the string "Unable to initialize"; SEVCHK() checks whether or not Channel Access was successfully initialized:

	SEVCHK(status, "Unable to initialize");
If the status code is other than ECA_NORMAL, SEVCHK() will print the status code, its associated message, and the string passed as the second argument, and if the severity of the error is ERROR or FATAL, SEVCHK() will abort the program. Since ECA_ALLOCMEM has the associated severity WARNING, SEVCHK() will merely print a warning if ca_task_initialize() returns ECA_ALLOCMEM. The program exits if the returned status code is anything other than ECA_NORMAL.

For convenience, Appendix A presents a table of status codes, their severity level, and their associated messages. This information can also be gleaned from the caerr.h header file.

3. Establishing a Connection to a PV

After Channel Access is initialized, our program calls the routine ca_search() to establish a connection to the PV that was specified on the command line. A string identifier containing the name of the PV should be passed as the first argument to ca_search() and the address of a channel ID should be passed as the second argument. The channel ID should be declared to be of type chid. Our ID is called chan and is declared at the beginning of main():

	chid chan;
A chid is a defined type for pointer to a structure. Channel Access uses the structure to store information about the channel, such as the network address, the PV's name, etc. An application should never have to access the contents of a chid directly. if ca_search() finds a PV on the network with the specified name, it will write the necessary information into the chid structure and the program can then use the chid in all subsequent operations on that PV.

ca_search() finds a PV by broadcasting its name on the network. All Channel Access servers that "hear" the broadcast will search to see if the specified PV is located on their host. If a server finds a PV by the specified name, it will send a message back to the Channel Access client library indicating that the message was found. Before this occurs, however, the ca_search() request must be flushed from the request buffer, which is explained in the next section.

Our program checks the status returned by ca_search(); if it is not ECA_NORMAL, the program exits:

	/* 3. Make search request. */
	status = ca_search(argv[1], &chan);
	if(status != ECA_NORMAL){
		printf("%s: problem with search request for %s.", camessage(status), argv[1]);
		exit(-1);
	}
You might assume that if ca_search() returns ECA_NORMAL, it means the PV was found and the information about the channel connection has been written into chan. In most cases, this assumption is not correct! Operations such as ca_search() or ca_get() are requests, since they are requesting an action from a server. For example, when a client application calls ca_get(), it is requesting that the server get the value of a PV and send it back to the client. When a client application issues a request, the client library checks to see if the request is valid. If the request is valid, the routine returns ECA_NORMAL. If the request is invalid, the routine returns the appropriate error code. An instance of an invalid request would be a ca_get() request that is passed a chid for a PV which the client program has not yet connected to. Since the client library cannot identify the channel connection, it returns ECA_BADCHID.

For most client calls, when the request is valid, the client library places the request in a request buffer. The request buffer is flushed and all the requests in it are sent over the network to the servers when the buffer is full or when a client program specifically flushes the buffer using any one of several routines. However, this is not the case with ca_search(), since the client library must broadcast the request multiple times. Thus, when a client application issues a ca_search() request and the request is valid, instead of placing it in a request buffer, the client library immediately starts broadcasting the request over the network until a Channel Access server responds.

It should be noted that if the PV is located on the same host, the status code returned by the request indicates not only if the request was valid or not, but also if the request was successfully performed. For example, suppose a client application is located on the same host as an EPICS database that has a record called calc1. If a client makes a request to read the record's value using ca_get() and if ca_get() returns ECA_NORMAL, then that means the value of the record was successfully read. However, unless a client application knows beforehand which PVs it will connect to and that these PVs are going to be located on the same host as the client application, a client application should assume that any PVs will be located remotely. The success/failure of a local operation can be checked in the same way that the success/failure of a remote operation is checked. The next section explains one way in which a client application can check the success/failure of ca_search() when the PV is located remotely.

In the example program if the call to ca_search() returns ECA_NORMAL, then our program assumes that the request was valid. If the call returns an error code, our program assumes that the request is invalid and prints an error message. Note how the error message associated with the status code is printed by passing the routine ca_message() as an argument to printf().

4. Checking Success/Failure of Requests

The question then arises: how does a client application check the success/failure of a request such as ca_get()or ca_search(). First, it's important to note that not all client calls request the services of a Channel Access server; therefore they don't have to check for success/failure the way requests do. For example, when a client calls ca_message(), the client library simply returns the appropriate character string, so a Channel Access server is not involved. When a client application requests the services of a server, it should check to see if the server successfully performed the operation.

Our program checks the success/failure of ca_search() using the routine ca_pend_io(); that is, it checks whether or not a channel was established for the PV. The sole argument ca_pend_io() accepts is a floating-point number representing seconds.When ca_pend_io() is called, it first flushes any requests from the request buffer and sends them to the appropriate server; next, it waits until the specified seconds have elapsed or until any generated queries have been answered. Currently, calls that generate queries include ca_search() and ca_get() (and any aliases of ca_get()).

For each query, Channel Access increments a counter. The call to ca_pend_io() will wait until all queries have been answered or the specified number of seconds have elapsed, in which case it is said to "time out." If all queries are successfully answered in the specified time, ca_pend_io() returns ECA_NORMAL. If ca_pend_io() times out, it returns ECA_TIMEOUT. In the latter case, a program should assume that any operations which generated queries have failed. For each query answered, ca_pend_io() decrements the query count until it reaches zero. If ca_pend_io() times out, the query count is reset to zero.

Thus, in our program, we pass a value of 1.0 or one second to ca_pend_io().

	/* 4. Send the request and wait for channel to be found. */
	status = ca_pend_io(1.0);
	if(status != ECA_NORMAL){
		printf("%s: Not Found\n", argv[1]);
		exit(-1);
	}
Since only one query has been generated--by the call to ca_search()-- ca_pend_io() will wait until a server sends back a response to the client indicating that it has found the PV or until one second has elapsed. If ca_pend_io() returns ECA_TIMEOUT, our program will assume that the PV cannot be found and will exit after printing a message.

If ECA_NORMAL is returned, then the PV was found and a connection was established; chan will be a valid chid containing information about the channel connection and can be used for all subsequent operations for the PV.

Another way of checking the success/failure of remote operations involves event handler functions. By supplying a function address and specifying it as an event handler for an operation, the function will be called when the operation completes. Event handlers and how they can be used to indicate success/failure will be explained later in this chapter.

5. Reading a PV's Value

After a channel is established to the PV, our program then reads and prints its value. It does this by calling ca_bget(), which is an alias of ca_array_get(). Our program checks the validity of the request by calling SEVCHK(). Then we flush the request by calling ca_pend_io() with a value of one second. The ca_pend_io() call will wait either until its query is answered and the PV's value is returned to the client by the server, or until one second has elapsed. If ca_pend_io() returns ECA_NORMAL, it means that the PV's value was successfully read. Then the value is printed. If ca_pend_io() times out and returns ECA_TIMEOUT, our program assumes the value was not successfully read.

	/* 5. Make get request. */
	status = ca_bget(chan, string);
	SEVCHK(status, "Error in call to ca_bget()" );

	status = ca_pend_io(1.0);
	if (status != ECA_NORMAL){
		printf("%s: Get Timed Out\n", argv[1]);
		exit(-1);
	}
	printf("%s\n", string);
ca_bget() was designed to retrieve the string associated with the current state of an EPICS binary record such as a multiple-bit binary record. In EPICS binary records, each discrete state has (or should have) a corresponding string. When ca_bget() is called for a binary PV, the server will attempt to fulfill the request by returning the string corresponding to the current state of the binary PV. The prototype for ca_bget() is

	int ca_bget(chid CHID, char *PVALUE);
which shows that it accepts two arguments: a valid channel ID structure and a string pointer. If and when the query generated by ca_bget() is answered, the value string is written into the location specified by the pointer. Though designed for EPICS binary records, ca_bget() can be used to retrieve the value of any PV as a string. If the native type of the PV is a floating point number, for instance, the number will be converted to its string representation.
Note: when your program uses a function such as ca_bget() , your program must not use the value pointer until ca_pend_io() returns ECA_NORMAL.

The ca_bget() routine is an alias of ca_array_get() which accepts four arguments: (1) a constant representing a database request type found in db_access.h such as DBR_FLOAT, (2) the number of elements to be read from the PV starting with the first element, (3) a valid chid, i.e., a chid that was passed to a successful ca_search() operation, and (4) a pointer to an address capable of holding the external or DBR type specified by the first argument; for instance, if the first argument is DBR_CHAR, this argument can be a pointer to type char or dbr_char_t, which is the EPICS type.

ca_bget() simply calls ca_array_get() with an external variable of type DBR_STRING and with an element count of one. This is how Channel Access aliases work: they simply call the basic function with certain default arguments.

6. Closing Channel Access

Next, our program calls ca_task_exit() which frees any resources allocated by the client library for the client application. Calling ca_task_exit() is optional since whenever a client program exits, the allocated resources are freed in the same way. For our program, calling ca_task_exit() is a bit superfluous since our program is about to exit anyway. Nonetheless, it's still good practice. Calling ca_task_exit() is most useful for programs that want to free any resources allocated by the library on behalf of the process or group of tasks.

The next section discusses data types, data conversions, the ca_array_get_callback() function and its aliases, and how they differ from functions that don't specify an event handler like ca_bget().

Callback Functions

To read or write a PV's value, there are basically three different functions possible. For instance, to read a PV's value there are these three functions--ca_array_get(), ca_array_get_callback(), and ca_sg_get()--each of which has at least one and up to three aliases. Calls whose name follows the format ca_sg_nnn() are known as synchronous group functions and synchronously perform requests placed in a certain group.

The following table lists the routines available for retrieving PV values, writing to PVs, and monitoring PVs, and categorizes them in three groups: callback functions, non-call back functions, and synchronous-group functions.
Operation Typecallbacknon-callbacksynchronous group
Get channel valueca_array_get_callback(), ca_bget_callback(), ca_rget_callback(), ca_get_callback()ca_array_get(),

ca_bget(), ca_rget(), ca_get()

ca_sg_array_get()
Put channel valueca_array_put_callback(), ca_put_callback()ca_array_put(),

ca_bput(), ca_rput, ca_put()

ca_sg_array_put()
Monitor channel for value changesca_masked_array_event(), ca_add_array_event(), ca_add_event()nonenone

Monitor functions such as ca_add_event(), put functions such as ca_arrray_put(), and synchronous group functions such as ca_sg_array_get() are all discussed in subsequent chapters. For now, it is important to recognize that some of these calls specify functions called event handlers, functions that are called when an operation completes or when an event occurs. Functions that specify an event handler are referred to in this tutorial as callback functions. Functions that don't require an event handler will be referred to as non-callback functions. For instance, ca_get_callback() specifies an event handler and is a callback function, while ca_get() does not specify an event handler and is a non-callback function. The client application supplies the event handler, which can perform any task, the most basic of which would be to inform the client application of an operation's success or failure.

Let's look at another simple program called ezee2 that uses a callback function, ca_get_callback(), to read a PV's value. The program uses a for loop to prompt the user for a PV name. It then reads the value for that PV, prints it, clears any resources for the channel, and then loops around again to ask for another PV name. The loop terminates when the user enters "bye" instead of a PV name. Although not the most useful program, it will demonstrate certain features about using callback functions. It will also introduce some of the various data types that can be used with Channel Access operations.

Here is a listing of the main part of the program including the preprocessor directives and the prototype for the event handler my_own_handler(), though the listing for the handler is presented later on.

#include <stdio.h>
#include <alarmString.h> /* EPICS header files */
#include <cadef.h>

#define MAX 30

/* Prototype for event handler */
void my_own_handler(struct event_handler_args args);

/* Flag used to communicate between main() and my_own_handler() */
int PEND_FLAG

int main()
{
	int status;
	char chName[MAX];
	unsigned nelem;
	chid chan;
	chtype type;

	/* Initialize Channel Access. */
	status = ca_task_initialize();
	SEVCHK(status, "Unable to initialize");
	if (status != ECA_NORMAL)
		exit(-1);

	for (printf("Enter channel name or \"bye\" to quit.\n");
			gets(chName) != NULL && strcmp(chName, "bye") != 0;
			printf("Enter channel name or \"bye\" to quit.\n")){

		status = ca_search(chName, &chan);
		SEVCHK(status, "Bad channel name? Invalid chid.");
		status = ca_pend_io(1.0);
		if (status != ECA_NORMAL) 
			printf("%s: not found.\n", chName);
		else {
			/* Get PV's native data type.  */
			type = ca_field_type(chan);

			/* Get number of elements of PV */
			nelem = ca_element_count(chan);

			/* Make callback request */
			status = ca_array_get_callback(dbf_type_to_DBR_STS(type),
							nelem,					/* number of elements of database field */
							chan,					/* channel identifier or chid */
							my_own_handler,  /* name of callback event handler */
							NULL);
			SEVCHK(status, ca_message(status));

			/* Flush request and block for 0.1 seconds waiting for event */
			PEND_FLAG = 1;
			while (PEND_FLAG)
				status = ca_pend_event(0.1);
			if (status != ECA_TIMEOUT && status != ECA_NORMAL)
				printf("%s\n", ca_message(status));

		}
		SEVCHK(ca_clear_channel(chan), NULL);

	}
	return(0);
}
Basically, after including the appropriate header files and making the proper declarations, our program first initializes the Channel Access library (lines 13-16) and then enters a for loop (lines 17-39), which begins by prompting the user for the name of a PV and places it in chName. If the user enters "bye", the for loop exits and the program returns. Otherwise, the program tries to establish a connection to the PV specified by the user (line 20-39). If ca_pend_io() returns ECA_TIMEOUT or any other error code, the program assumes that a connection could not be established and skips the else statement in which the request to read the PV's value is made.

If a channel for the PV is established, the program then enters the else statement, in which it calls ca_array_get_callback() to read the PV's value. Before making the request, however, the program uses the call ca_field_type() to determine the PV's native type (line 26). It then calls ca_element_count() to determine the size of the PV's array (line 27). Both of these calls accept a channel ID as their only argument. ca_field_type() returns an integer of type chtype. The returned integer corresponds to a constant that represents the "native" data type of the PV, i.e., the data type of the PV in the server. In the EPICS database, this native type will be the field's storage type, which can one of the EPICS database field types: DBF_ENUM, DBF_DOUBLE, DBF_LONG, etc. The ca_element_count() macro returns an unsigned long integer that represents the maximum possible elements of the PV. For an array PV, this number would be the number of elements in the array, while for a scalar PV, this number would simply be a one. Note that ca_field_type() and ca_element_count() are not remote requests, so they are not placed in the request buffer but are immediately performed by the client software as long as the channel is connected.

1. Reading Values Using ca_array_get_callback()

Next, the request is made to read the PV's value (line 28-32). Here it calls the callback function ca_array_get_callback() to do this. The ca_array_get_callback() function accepts five arguments:

a database request type such as DBR_STRING. The data type indicates what type the client wants the data in. The server will try to convert the data from the PV's native type to the requested type.

an unsigned long integer which represents the number of elements the client wants to read from the PV's array starting with the first element. For instance, if the PV consists of an array of ten values, the client can request to read values 1-4 only. However, the beginning element must always be the first one; for example, a client cannot read elements 4-7, as the fourth element isn't the first element.

a channel ID or chid that is currently attached to a PV. This argument identifies the channel.

the name of an event handler defined by the client program

a void pointer to a field which can then be used subsequently by the event handler.

The first argument should be a database request type, which is just a constant of the format DBR_ENUM, DBR_FLOAT, DBR_CHAR, etc. ca_field_type(), on the other hand, returns the native type of a field: DBF_CHAR, DBF_ENUM, DBF_STRING, etc. The difference between these two sets is that the database request or DBR type indicates the data type requested by the Channel Access client while the native or DBF type indicates the data type for the PV used by the server.

For each native type there is a request type. For instance, DBF_CHAR corresponds to DBR_CHAR. In addition, the database request or DBR types include compound data types, which are structures which can hold information associated with a PV as well as the PV's value. For instance, the DBR_STS_FLOAT type is a structure which can hold the value of a requested PV in a floating point field along with the status and severity of that PV's record. A client could request a compound data type for the VAL field of a Calculation record, in which case the server would write the value of VAL and the current value of the STAT (status) and SEVR (severity) fields into the structure, which the program could then access. Any of the client calls that read or monitor PVs like the ca_array_get_callback() used in this program can request a compound data type for a PV. Calls that write values to PVs such as ca_put() do not accept compound data types.

For the first argument, our program uses the macro dbf_type_to_DBR_STS() which accepts a constant corresponding to a PV's native type--in this case the value returned by ca_field_type()--and returns a constant which corresponds to the appropriate DBR_STS type. For instance, if the native type of the PV is DBF_DOUBLE, dbf_type_to_DBR_STS() will return DBR_STS_DOUBLE. The DBR_STS types include information about the alarm status and severity of the PV. Thus, our program requests the compound type that corresponds to the PV's native type, which means that the server does not have to perform any type conversions, thus lightening the load on the server, a good practice in all client programs.

The next argument is simply chan, the channel identifier returned by ca_search(), and the next, the name of this program's event handler, my_own_handler(). The last argument can be a pointer to any type, which the callback function and the main program can then share.

After our program issues the get request, it checks the validity of the ca_array_get_callback() request by passing the status code returned from it to SEVCHK() (line 36). The actual success/failure of the request is determined by the event handler.

2. Allowing an Event Handler to Execute

If the request is valid, the callback request is placed in the request buffer. Our program then calls ca_pend_event() with an argument of 0.1 seconds (37-39).

ca_pend_event() is similar to ca_pend_io() in that they both flush the request buffer. However, client programs generally use ca_pend_io() to flush non-callback requests such as ca_get() from the request buffer while clients generally use ca_pend_event() to flush callback functions such as ca_get_callback() or ca_put_callback(). ca_pend_io() blocks until all outstanding queries are answered or until the number of specified seconds have elapsed. ca_pend_event(), in contrast, always blocks for the number of seconds specified in its argument. A timeout period of zero when passed to ca_pend_event() blocks the client program forever.

While ca_pend_event() is pending, any asynchronous events are allowed to execute. An asynchronous event refers to an event handler like, such as the one specified in the call to ca_get_callback(). When a callback request is sent to the PV's host, the server performs the requested operation, and after completion the specified event handler is called. In our program, the event handler my_own_handler() is called when the Channel Access server has retrieved the values associated with the DBR_STS structure requested by ca_get_callback().

For clients that are running in single-threaded environments like UNIX, when you use a callback request such as ca_get_callback() or ca_put_callback(), you must call ca_pend_event() sometime within your program, or else the event handler will never get a chance to execute. Your program can use ca_pend_io() to flush a callback request, but because ca_pend_io() only blocks when there are outstanding queries and callback requests generate no such queries, ca_pend_io() will return immediately after flushing the request buffer and will perform no other action. Thus, an asynchronous event will not be able to preempt the program's main thread in order to execute. On multi-threaded environments like VxWorks, one asynchronous event at a time is allowed to preempt the main thread of the Channel Access context. Therefore, calling ca_pend_event() in VxWorks is unnecessary.

ca_pend_event() can also be used to flush non-callback requests from the request buffer. However, it is generally not used for this purpose since ca_pend_event() always blocks for the specified number of seconds, always returning ECA_TIMEOUT. Remember client programs generally check the return value of ca_pend_io() to verify if a requested non-callback operation succeeded or failed in the server; if ca_pend_io() returns ECA_TIMEOUT, the program should assume that all non-callback routines that it flushes have failed. Thus, if a program uses ca_pend_event() to flush a non-callback request, it has no way of telling if its request has failed in the server.

Also because ca_pend_event() always times out, checking it using SEVCHK() is not necessary. If you check the status code returned by ca_pend_event() with SEVCHK(), the client software will print a warning message, which basically says that it timed out, since the corresponding severity of ECA_TIMEOUT is that of a warning. It is better to check ca_pend_event() for a status code other than ECA_TIMEOUT or ECA_NORMAL. Our program calls SEVCHK() only when the status code is neither of these.

3. Event Handlers

An event handlers refers to a function that is called when a certain event occurs or after an event completes. For instance, there are connection handlers, which are called when a channel's current connection state changes, i.e., when a previously connected channel is disconnected or when a disconnected channel is connected for the first time or reconnected. The connection handler is specified by a call to ca_search_and_connect(). Basically, there are five types of events:

a connection event as explained above

a put or get event--when a PV's value is retrieved or changed using ca_put_callback() or ca_get_callback() or any of their aliases.

an access-rights event--when the current user's access to a PV changes.

a monitor event. A monitor can be placed on a PV by a call to ca_add_event(). When a monitored PV changes by a specified amount or its alarm state changes, a monitor event occurs and the user-supplied event handler is called. Note that for PVs that have deadbands, the handler won't be called until the PV changes by more than the deadband.

an exception event. An exception occurs when a put or get operation fails or when a channel is disconnected. An exception handler is different from a connection handler or put-get event handler in that there can only be one exception handler for each Channel Access client task and this is called automatically upon each exception. There is a default exception handler that will be called upon each exception. The client program can supply its own exception handler by calling ca_add_exception_event() which accepts the address of a user-defined function as its first argument.

An exception event is the only type of event that has a default handler. Our program uses a callback function ca_array_get_callback() inside the for loop to read the PV's value. Since ca_array_get_callback() requires an event handler that the client program defines, our program supplies the handler my_own_handler(). It is called whenever the server has completed the request. Here, completed does not mean that the operation was performed successfully, i.e., that the server retrieved the value, but only that the server tried to complete the operation. Thus, a callback function should check to see if the operation successful before it performs any other task. How a function can do this is explained below.

An event handler is passed a structure specific to that type of event. For example, a connection handler is automatically passed a data structure called connect_handler_args and monitor or put/get event handlers are passed the structure event_handler_args. These structures are defined in cadef.h, and are easily understood, so we will not go into all of them here, but only event_handler_args.

Reviewing the call made to ca_array_get_callback(), it specified my_own_handler as a callback function:

 			status = ca_array_get_callback(dbf_type_to_DBR_STS(type),
 							nelem,					/* number of elements of database field */
 							chan,					/* channel identifier or chid */
 							my_own_handler,  /* name of callback event handler */
 							NULL);
The event handler accepts one argument, a structure called event_handler_args, whose definition is

struct   event_handler_args{
	void   *usr; 								/* User argument supplied when event added */
	struct channel_in_use chid;								/* Channel id */
	long type;								/* the type of the value returned		*/ 
	long count;								/* the element count of the item returned	*/
	void *dbr;								/* Pointer to the value returned		*/
	int status; 								/* CA Status of the op from server - CA V4.1	*/
};
A brief explanation of each member and how it is generally used follows.

  1. *user -- an address passed to the function in the function call as the last argument. In our case, we passed it NULL. The function and main part of the program communicate data through this pointer.

  2. *chid--the channel's identifier or chid. Remember that a chid is just a pointer to a structure that contains information about the channel and its connection. Usually, an event handler will not access the chid structure directly.

  3. type--a constant corresponding to a database request type such as DBR_FLOAT. This type will be the type which was requested from the original call such as ca_get_callback(). It is the external type, the converted type if the request type didn't match the PV's native type. An event handler will use this member in order to know how to access the data, which is mainly of interest to event handlers that are used for get or monitor operations.

  4. count--an unsigned long integer corresponding to the number of elements retrieved from or sent to the PV. Used to access arrays. Of interest to event handlers used for get or monitor operations.

  5. *dbr--a pointer to the data returned by the server. Event handlers used with put operations should not reference this pointer as it is blank for such operations since put operations don't retrieve values. Event handlers used with monitor or get operations will use this pointer to reference the value returned by the server. An event handler should not de-reference this pointer without making sure that the operation was successful, i.e., make sure that the status member is ECA_NORMAL.

  6. status--an integer that is the status code indicating either success (ECA_NORMAL) or an error code such as ECA_GETFAIL. This code represents the success or failure of the operation in the server. So, for instance, if ECA_PUTFAIL is returned for a put operation, the server couldn't change the PV's value for whatever reason.

3.1. my_own_handler()

As an example, let's examine my_own_handler(), the event handler specified in the call to ca_array_get_callback(). It's a little complicated because it must be able to print several data types. The last section of this chapter explains the different data types in more detail and how to deal with them.

Here is the prototype my_own_handler() as declared before main() in our program.

	6. void my_own_handler(struct event_handler_args args);
And here is its definition. The important parts of code are numbered.

/* callback function: informs user of success or failure of   */
/* get operation and prints the channel's value along with */
/* status and severity. 												                */
void my_own_handler(struct event_handler_args args)
{
 	int i;
	/* 1. union db_access_val defined in db_access.h */
 	union db_access_val *pBuf;
 
	/* 2. If operation succeeded, print status,
	   severity, and channel value(s). */ 
 	if (args.status == ECA_NORMAL){
		/* 3. Cast pointer to appropriate type
		    or member of db_access_val */
		pBuf = (union db_access_val *)args.dbr;
		printf("channel %s: ", ca_name(args.chid));
 		switch (args.type){
 			case DBR_STS_STRING:
 				printf("alarm status = %s, alarm severity = %s\n",
 						alarmStatusString[pBuf->sstrval.status],
 						alarmSeverityString[pBuf->sstrval.severity]);
 				printf("current %s:\n", args.count > 1?"values":"value");
 				for (i = 0; i < args.count; i++){
 					printf("%s\t", *(&(pBuf->sstrval.value) + i));
 					if ((i+1)%6 == 0) printf("\n");
				}
 				printf("\n");
 				break;
 			case DBR_STS_SHORT:
 				printf("alarm status = %s, alarm severity = %s\n",
 						alarmStatusString[pBuf->sshrtval.status], 
 						alarmSeverityString[pBuf->sshrtval.severity]);
 				printf("current %s:\n", args.count > 1?"values":"value");
 				for (i = 0; i < args.count; i++){
 					printf("%-10d", *(&(pBuf->sshrtval.value) + i));
 					if ((i+1)%8 == 0) printf("\n");
				}
 				printf("\n");
 				break;
 			case DBR_STS_FLOAT:
 				printf("alarm status = %s, alarm severity = %s\n",
 						alarmStatusString[pBuf->sfltval.status],
 						alarmStatusString[pBuf->sfltval.severity]);
 				printf("current %s:\n", args.count > 1?"values":"value");
 				for (i = 0; i < args.count; i++){
 					printf("-10.4f", *(&(pBuf->sfltval.value) + i));
 					if ((i+1)%8 == 0) printf("\n");
				}
 				printf("\n");
 				break;
 			case DBR_STS_ENUM:
				printf("alarm status = %s, alarm severity = %s\n",
						alarmStatusString[pBuf->senmval.status],
						alarmSeverityString[pBuf->senmval.severity]);
				printf("current %s:\n", args.count > 1?"values":"value");
				for (i = 0; i < args.count; i++){
					printf("%d ", *(&(pBuf->senmval.value) + i));
				}
				printf("\n");
				break;
			case DBR_STS_CHAR:
				printf("alarm status = %s, alarm severity = %s\n",
						alarmStatusString[pBuf->schrval.status],
						alarmSeverityString[pBuf->schrval.severity]);
				printf("current %s:\n", args.count > 1?"values":"value");
				for (i = 0; i < args.count; i++){
					printf("%-5", *(&(pBuf->schrval.value) + i));
					if ((i+1)%15 == 0) printf("\n");
				}
				printf("\n");
				break;
			case DBR_STS_LONG:
				printf("alarm status = %s, alarm severity = %s\n",
						alarmStatusString[pBuf->slngval.status],
						alarmSeverityString[pBuf->slngval.severity]);
				printf("current %s:\n", args.count > 1?"values":"value");
				for (i = 0; i < args.count; i++){
					printf("%-15d", *(&(pBuf->slngval.value) + i));
					if((i+1)%5 == 0) printf("\n");
				}
				printf("\n");
				break;
			case DBR_STS_DOUBLE:
				printf("alarm status = %s, alarm severity = %s\n",
						alarmStatusString[pBuf->sdblval.status],
						alarmSeverityString[pBuf->sdblval.severity]);
				printf("current %s:\n", args.count > 1?"values":"value");
				for (i = 0; i < args.count; i++){
					printf("%-15.4f", *(&(pBuf->sdblval.value) + i));
				}
				printf("\n");
				break;
				default:
					printf("data type was not dbr_sts_nnn.\n");
					break;
		}
	}
	/* if get operation failed, print channel name and message */
	else
		printf("channel %s: get operation failed\n", ca_name(args.chid));
	return;
}
Our handler's purpose is simply to check if the get operation succeeded or failed, and if it succeeded, the handler prints the PV's name and value(s). To check the status of the operation, it simply accesses the status member of the event_handler_args structure which holds this information (2):

if(args.status == ECA_NORMAL)
	print values
else
	print message
By far, most of the code is for dealing with the different possible data types. It will print the status and severity and value of most PV types. Since this event handler provides for all the different basic data types (ENUM, STRING, etc.), it is long.

The dbr member of the event_handler_args structure points to the value returned by the server, the value of the PV. To access it, our program first defines a pointer pBuf to point to a union structure called db_access_val (1), defined in db_access.h, and makes it point to the location pointed to by dbr, casting it as the union structure. db_access_val is a union of all the possible database request types (3).

 pBuf = (union db_access_val *)args.dbr;
The event handler then uses a switch statement for each possible DBR_STS type, These types are compound types, each of which is a structure that contains the PV's value and the status and severity of the PV's record. There are nine DBR_STS types, eight correspond to the basic types DBR_ENUM, DBR_FLOAT, DBR_CHAR, etc., and one type which differs, having two values for the ACKT and ACKS fields in the PV's record. Our handler doesn't provide for the possibility of this type, only the basic types. Thus, the purpose of the switch statement is to print all the possible types that can be returned by the server.

Note that the status and severity members in each type of DBR_STS structure are printed by accessing pBuf using the appropriate member; for example, when accessing a DBR_STS_DOUBLE

      printf("alarm status = %s, alarm severity = %s\n",
	     		alarmStatusString[pBuf->sdblval.status],
	     		alarmSeverityString[pBuf->sdblval.severity]);
The status and severity members of the DBR_STS structure are two short integers which represent the alarm status and severity of the PV. Since they are just integers, they are not very informative. However, in the header file alarmString.h are two arrays of strings which hold the strings that describe the status and severity codes. By indexing alarmStatusString with the status member of the DBR_STS structure, we can print the string that corresponds to the PV's alarm condition: NO_ALARM, HIGH, HIHI, etc., and likewise by indexing the alarmSeverityString array with the severity member, we can print the severity of the alarm, NO_ALARM, MINOR, MAJOR, or INVALID.

4. Data Types

The Channel Access client software uses database request types which have the DBR prefix. The EPICS database software uses database field types, which have the prefix DBF. By using the request types, the client can exchange data with the server though the client is running on a different host.

User programs can specify any DBR type and the server will convert from the native type to the DBR type. For instance, if the native type is DBF_FLOAT and the client program issues the following request,

ca_get_callback(DBR_INT, chid, my_handler, NULL);
the server will return the data as an integer after converting it from a floating-point number. Of course, a floating-point to an integer is an easy conversion to predict; however, not all conversions are predictable.

Although the server can perform all conversions, it is recommended that large client programs that make a lot of requests perform some of the conversions themselves. For instance, although our example program was a small one, it performs most of the conversion work itself. For instance, in the original call to ca_array_get_callback(), we used the following code:

			type = ca_field_type(chan);

			/* Get number of elements of PV */
			nelem = ca_element_count(chan);

			/* Make callback request */
			status = ca_array_get_callback(dbf_type_to_DBR_STS(type),
							nelem,					/* number of elements of database field */
							chan,					/* channel identifier or chid */
							my_own_handler,  /* name of callback event handler */
							NULL);

which obtains the native type of the PV using the ca_field_type() macro and then uses the dbf_type_to_DBR_STS() macro to request the DBR_STS type corresponding to the field's native type. So, for example, if the field's native type is DBF_FLOAT, the request passed to the call will be DBR_STS_FLOAT. We could have simply specified DBR_STS_STRING as the first argument. The server would then convert the floating-point field or whatever type of field to a string which could be easily printed out. As it is, our event handler must use a switch statement with seven cases to print the value because it doesn't ask the server to convert the value from the PV's native type.

All variables in the client program that are going to hold variables returned by the Channel Access server, should be defined as the proper DBR type, though just using the corresponding C type will most often work, though not recommended. For instance, if a call specifies that the value be returned as a string, DBR_STRING, the pointer variable could be simply declared as,

char string[20];
which will work, but the recommended declaration should be

dbr_string_t string;
which makes matters simpler. Thus, in our first program the declaration which holds the state-string returned by ca_bget() is

dbr_string_t string;
All the current database request types are listed below, along with the type to declare them, though a glance at db_access.h could also answer many questions:
typedeclaration
DBR_STRINGdbr_string_t
DBR_INTdbr_int_t
DBR_SHORTdbr_short_t
DBR_FLOATdbr_float_t
DBR_ENUMdbr_enum_t
DBR_CHARdbr_char_t
DBR_LONGdbr_long_t
DBR_DOUBLEdbr_double_t
DBR_STS_STRINGstruct dbr_sts_string_t
DBR_STS_SHORTstruct dbr_sts_short_t
DBR_STS_INTstruct dbr_sts_int_t
DBR_STS_FLOATstruct dbr_sts_float_t
DBR_STS_ENUMstruct dbr_sts_enum_t
DBR_STS_CHARstruct dbr_sts_char_t
DBR_STS_LONGstruct dbr_sts_long_t
DBR_STS_DOUBLEstruct dbr_sts_double_t
DBR_STSACK_STRINGstruct dbr_stsack_string_t
DBR_TIME_STRINGstruct dbr_time_string_t
DBR_TIME_SHORTstruct dbr_time_short_t
DBR_TIME_FLOATstruct dbr_time_float_t
DBR_TIME_ENUMstruct dbr_time_enum_t
DBR_TIME_CHARstruct dbr_time_char_t
DBR_TIME_LONGstruct dbr_time_long_t
DBR_TIME_DOUBLEstruct dbr_time_double_t
DBR_GR_STRINGstruct dbr_gr_string_t
DBR_GR_SHORTstruct dbr_gr_short_t
DBR_GR_INTstruct dbr_gr_int_t
DBR_GR_FLOATstruct dbr_gr_float_t
DBR_GR_ENUMstruct dbr_gr_enum_t
DBR_GR_CHARstruct dbr_gr_char_t
DBR_GR_LONGstruct dbr_gr_long_t
DBR_GR_DOUBLEstruct dbr_gr_double_t
DBR_CTRL_STRINGstruct dbr_ctrl_string_t
DBR_CTRL_SHORTstruct dbr_ctrl_short_t
DBR_CTRL_INTstruct dbr_ctrl_int_t
DBR_CTRL_FLOATstruct dbr_ctrl_float_t
DBR_CTRL_ENUMstruct dbr_ctrl_enum_t
DBR_CTRL_CHARstruct dbr_ctrl_char_t
DBR_CTRL_LONGstruct dbr_ctrl_long_t
DBR_CTRL_DOUBLEstruct dbr_ctrl_double_t

Of course most of these are compound types like DBR_GR_STRING or DBR_CTRL_INT.

The different compound types have different purposes and return different information, though all return the PV's value:

The DBR_STS types return the PV's value along with status and severity of the PV's record.

The DBR_TIME types return, in addition to the severity and status, the time stamp for the PV.

The DBR_GR types return, in addition to the severity and status, the engineering units for the PV (the EGU field), the upper and lower display limits (usually HOPR and LOPR), and the alarm and warning limits (HIHI, HIGH, LOW, LOLO).

The DBR_CTRL types return, in addition to the fields contained in the DBR_GR types, the control limits of the PV (often the DRVH or DRVL fields for analog records).

The DBR_STS_ACKT type returns the status, severity, and the ACKT and ACKS fields.

The definition of these structures can be seen by glancing at db_access.h. For now, lets examine a DBR_STS_ENUM structure:

struct dbr_sts_enum{
	dbr_short_t status;
	dbr_short_t severity
	dbr_enum_t value;};
As in this structure, the PV's value is in the value member. Note that this structure does not contain the enumerated strings for that PV. However, the DBR_GR_ENUM and DBR_CTRL_ENUM types do, so if you want to access all the possible state strings for a binary or multi-bit binary PV, you should use the DBR_GR or DBR_CTRL types.

There are many different macros defined both in cadef.h and db_access.h that should help a programmer to deal with data types. We have used two: ca_field_type() and dbf_type_to_DBR_STS(). ca_field_type() returns the native type of the PV, while dbf_type_to_DBR_STS() returns the DBR_STS type corresponding to a native type. There are other macros to avail the programmer. Most are in the db_access.h header file. All of them accept an unsigned integer, which can be defined as chtype, as at the beginning of our program:

	chtype type;
Some of them can be used to check a PV's type, such as dbf_type_is_valid() which returns a non-zero value if the type passed to it is valid and a zero if it is not. Another example would be dbr_type_is_TIME() which returns non-zero if the type is a TIME compound type like DBR_TIME_FLOAT or zero if the type is not a DBR_TIME compound type. There are similar macros for all other simple and compound types, such as dbr_type_is_LONG().

There are macros to convert simple types to compound types. Our program used dbf_type_to_DBR_STS() to convert the PV's native type to a DBR_STS type. There are similar macros to convert native types to other compound types such as dbf_type_to_DBR_GR(). In addition, a programmer can use the dbf_type_to_DBR() to convert a native type to a simple DBR type such as DBR_FLOAT. All these macros return an integer that corresponds to the type.

Other macros in db_access.h can help with memory allocation of the different types. For instance, the macro dbr_size_n() accepts a type and an element count as its two arguments and returns an unsigned integer that corresponds to the number of bytes used by the PV. Other similar macros include dbr_size() and dbr_value_size().

Once again, a glance at db_access.h will tell the programmer of the available macros and how to use them. They greatly reduce the amount of work when dealing with types in a Channel Access client application.

A Simple Program
1. - Include Files and Program Compilation
2. - Initializing Channel Access
3. - Establishing a Connection to a PV
4. - Checking Success/Failure of Requests
5. - Reading a PV's Value
6. - Closing Channel Access
Callback Functions
1. - Reading Values Using ca_array_get_callback()
2. - Allowing an Event Handler to Execute
3. - Event Handlers
3.1. - my_own_handler()
4. - Data Types

Channel Access Client Tutorial - 4 NOV 1997
[Next] [Previous] [Top] [Contents] [Index]

| LANL | Lansce | UC | DOE |

L O S   A L A M O S   N A T I O N A L    L A B O R A T O R Y
Operated by the University of California for the US Department of Energy

Copyright © 1996 UC   Disclaimer   


For problems or questions regarding this web site contact George Vaughn.