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

Channel Access Client Tutorial

3. Writing to PVs and Using Connection Handlers

From the previous chapter we learned that there are two basic functions to read a PV's value: ca_array_get_callback() and ca_array_get(). Both of these calls have several aliases. For example, the aliases of ca_array_get() are ca_get(), ca_bget(), and ca_rget(). Likewise, there are two basic functions available to write a value to a PV: ca_array_put() and ca_array_put_callback() and each of these has several aliases. For instance, ca_bput() is an alias of ca_array_put(). This chapter will demonstrate further the use of callback functions.

This chapter will also demonstrate connection handlers, event handlers which are specified by a call to ca_search_and_connect().

An Example Program

Let's use another trivial program to demonstrate writing or putting values to a PV. It is somewhat long, but most of the code is in the connection handlers, and most of the code in them is simply for the initialization and maintenance of a linked list. Let's first look at the main part of the program.

#include <cadef.h>

#define MAXLEN  20
#define POLL 1.0e-9
#define SHRT_DLY 0.1
#define LNG_DLY 0.25

struct Channel{
	dbr_string_t name;
	chid chan;
	struct Channel *next;
	struct Channel *prev;} *pFirst = NULL, *pLast;

void connect_handler1(struct connection_handler_args);
void connect_handler2(struct connection_handler_args);
void an_event_handler(struct event_handler_args);
struct Channel *search(char *Name);

int main()
{
	int status, i, j, k;
	dbr_float_t *pValue;
	dbr_string_t Value;
	dbr_string_t Name[MAXLEN];
	struct Channel *pCurrent;
	unsigned long Nelem; 

	/* initialize Channel Access task */
	SEVCHK(ca_task_initialize(), "Unable to initialize");

	/* Get channel names and establish connection for each.*/
	/* Allocate memory for first node, and then for each       */
	/* addtional node after connection is established. Loop   */
	/* until user enters"quit".											                     */
	for (printf("Enter a channel name.\n"),
		pCurrent=(struct Channel *)calloc(1, sizeof(struct Channel));
		gets(pCurrent->name) != NULL && strcmp(pCurrent->name, "quit") != 0;
		printf("Enter a channel name or \"quit\" to stop.\n")){

		status = ca_search_and_connect(pCurrent->name, /* name string */
										&(pCurrent->chan),/* chid */
										connect_handler1, /* connection handler */
										pCurrent      /* user-supplied address */ );
		SEVCHK(status, NULL);
		ca_pend_event(LNG_DLY);
		if (ca_state(pCurrent->chan) != cs_conn)
			printf("couldn't establish connection to %s; ignoring\n", pCurrent->name);
		else 
			pCurrent =(struct Channel *) calloc(1,sizeof(struct Channel));
	}

	/* Install a different connection handler on each channel. */
	if(pFirst != NULL){
		for (pCurrent = pFirst; pCurrent != NULL; pCurrent = pCurrent->next){
			status=ca_change_connection_event(pCurrent->chan, connect_handler2);
			SEVCHK(status, "ca_change_connection_event: couldn't change handler");
		}
	
	ca_flush_io();

	/* Loop until "quit" is entered. Get Channel name. If */
	/* found, prompt for values; else skip rest of loop.    */
	/* Call ca_put_callback().                                          */
	for (i = 0, printf("%s\n%s\n", "Enter the PV's name", "Enter \"quit\" to stop");
		gets(Name) != NULL && strcmp(Name, "quit") !=0;
		printf("%s\n", "Enter PV name or \"quit\" to stop")){

			ca_pend_event(POLL);
			pCurrent = search(Name);
			if(pCurrent == NULL){
				printf("Cannot find channel name in list. Disconnected?\n");
				continue;
			}
			Nelem = ca_element_count(pCurrent->chan);
			if(Nelem == 1){
				printf("Enter value.\n");
				if(gets(Value) != NULL){
					status = ca_put_callback(DBR_STRING,   /* external type */
											pCurrent->chan,                 /* chid */
											Value,                  /* string of value */
											an_event_handler,         /* callback */
											pCurrent   /* user supplied address */);
					SEVCHK(status, NULL);
				}
			}
			else if (Nelem > 1){
				printf("Enter up to %u values or q to quit.\n", Nelem);
				pValue = (float *) calloc(Nelem, sizeof(*pValue));
				for(k = 0; (j = scanf("%f", pValue+k)) == 1&& j != EOF && k < Nelem; k++)
					;
				j = getchar();
				status = ca_array_put_callback(DBR_FLOAT,      /* external type */
											Nelem,  /* no. of elements to be sent */
											Current->chan,                       /* chid */
											pValue,                    /* array of values */
											an_event_handler,             /* callback */
											pCurrent       /* user supplied address */ );
				SEVCHK(status, NULL);
			}
			ca_pend_event(SHRT_DLY);
	}
	return(0);
}
After the appropriate header files are included, and prototypes and variables declared (lines 1-21), Channel Access is initialized (line 22). Then the program enters a for loop which prompts the user for a PV name each iteration and attempts to establish a connection with the specified PV using ca_search_and_connect() (lines 23-36). The function ca_search_and_connect() is similar to ca_search(): it will attempt to establish a channel to the specified PV. However, in contrast ca_search() it can specify a connection handler, which is an event handler that is called when the channel's connection state changes. This occurs when a new channel is established, or when a channel is disconnected or reconnected. Our connection handler is called connect_handler1() and will be discussed later on. Here, it's only important to know that the handler will be called if Channel Access can establish a channel for the specified PV and that it will add the PV to a linked list of connected PVs.

After ca_search_and_connect() is flushed from the request buffer, the macro ca_state() is called (line 33) to check if the channel is connected. ca_state() returns an enumerated integer that represents one of four states:

cs_never_conn--connection could not be established.

cs_prev_conn--channel no longer connected.

cs_conn--channel connected.

cs_closed--channel closed, i.e., ca_clear_channel() called with this chid and therefore the chid is no longer valid.

If the state is not cs_conn, the channel isn't connected. Our program prints a message if this is the case. Otherwise the program allocates memory for another node and continues. Using the macro ca_state() is one way to check a channel's connection state and is most useful within the main() part of the program or within a function that is not a connection handler. In connection handlers, there is an easier way to check the connection state.

After the user enters "quit" and the for loop ends, another for loop is entered (lines 37-40). The purpose of this loop is to replace the connection handler installed on each PV in the previous loop, with a different connection handler called connect_handler2(). The first connection handler is meant to be called when the channel is first established, and the second one, when a previously established channel is disconnected or reconnected. The second connection handler is installed using the function ca_change_connection_event(). Each connection handler will be discussed later.

The ca_change_connection() request is flushed from the request buffer using ca_flush_io() (line 41). ca_flush_io() is like ca_pend_io() and ca_pend_event() in that it flushes the request buffer. However, it doesn't wait for queries like ca_pend_io() or events like ca_pend_event(): it merely flushes the request buffer and returns, which is useful in some cases. Here, since the program doesn't anticipate the connection handler being called until a channel to a PV is broken, we don't anticipate the connection handler being called. A question then arises: if on a single-threaded environment such as UNIX the connection handler will not be able to run unless ca_pend_event() is called, then how will the connection handler run if a connection is broken? The answer is that in the next for loop, the next block of code, ca_pend_event() is called twice, once when the for loop is entered, and once after ca_put_callback() is called. The first call specifies an extremely small timeout period, SHRT_DLY, which is defined as 1.0e-9. When ca_pend_event() is called with such a small timeout, it is known as a poll. Polls check for any file descriptor or background activity. Thus, as long as the user enters in PV names and values, the connection handlers will be able to execute.

After the request buffer is flushed, the program enters another for loop (lines 42-72). The purpose of this loop is to prompt the user for a PV name, look for the PV in the linked list containing all currently connected PVs, and if found prompt the user for a value or values and write those values to the PV. After it gets the PV name from the user, the program searches for the PV in the linked list by calling search(), a simple routine that transverses the linked list looking for the PV (line 46). If search() cannot find the PV, the program skips the rest of the loop (lines 47-49 ).

After the PV is found, the program calls the macro ca_element_count() to determine the array size of the PV (line 50). It returns an unsigned short integer indicating the number of elements of the PV when passed the PV's chid. It returns a one for scalar PVs (or array PVs of one element). If the PV is scalar, the program prompts the user for single value, it then calls ca_array_put_callback() to write to the PV (lines 51-59 ). If the PV is an array, the program prompts the user for up to n elements, n being the number of total elements in the array, and calls ca_put_array_callback() to write the values to the PV (lines 60-71). ca_put_array_callback() is a callback function used to write one or more values to PVs. When a client program calls it, the program should specify an event handler, which will be called once the operation or attempted operation is complete.

Next, ca_pend_event() pends for SHRT_DLY seconds (line 72). The handlers can run while it's pending. Then, either the user can enter in another PV whose value he would like to change, or "quit" if he doesn't wish to write any more values to the PV, in which case the program ends.

1. Connection Handlers

Our program uses a doubly-linked list to store each PV's name and channel ID. Thus, later on in the program when the user enters the PV's name, the list can be traversed for a match with that name; when a match is found, the chid in the same node can be used for any other operations on the PV. The node structure is defined globally before main() (lines 6-10), where pFirst and pLast are the nodes used to keep track of the first and last elements in the list. pCurrent points to the current node and is declared in main().

ca_search_and_connect() is the basic function used to establish connections to PVs. The call ca_search() is just an alias for ca_search_and_connect() that specifies NULL in place of a connection handler's name. Here is the prototype for it in cadef.h:

int ca_search_and_connect (
		/* Name				IO		Value					*/
		/* ----				--		-----					*/
char *,		/* NAME				R		NULL term ASCII channel name string 					*/
chid *,		/* CHIDPTR				RW		channel index written here 					*/
void (*)(struct connection_handler_args),
		/* PFUNC				R		the address of user's connection handler					*/
void *		/* PUSER				R		placed in the channel's puser field 					*/
);
The above prototype is the format that appears in the Reference Manual and is fairly easy to understand: the Name is simply the name of the argument, while IO simply indicates if Channel Access reads the value of the argument (R), writes a value into the argument (W), or both (RW).

Our call to ca_search_and_connect() specifies the connection handler connect_handler_1() (lines 27-30). The other arguments are, first, the name of the PV, then, second, the channel identifier or chid, and, last, the pointer to the current node. This last argument can be any type of address, a pointer to any object. Generally, the connection handler and the calling routine, main() in this case, will use this address to exchange data. In our case, we are passing it the pointer to the current node, pCurrent, and connect_handler1() will use this address to add the node to the linked list.

Note that after our program calls ca_pend_event() with an argument of 0.25 seconds, the definition of LNG_DLY (lines 5, 32). Since ca_search_and_connect() specifies a callback function, connect_handler1(), it is best to flush it using ca_pend_event() rather than ca_pend_io(), because in a single-threaded environment like UNIX the connection handler must be allowed time to run when ca_pend_event() is pending. As it is in our program, ca_pend_event() blocks for 0.25 seconds. After ca_search_and_connect() is called, a connection is sought to the specified PV. If a PV is found, the connection handler will be called and will execute if the program is pending under ca_pend_event(). If the program is not pending under ca_pend_event(), the connection handler will execute when the next call to ca_pend_event() occurs. In vxWorks, a separate task that calls ca_pend_event() with a specified frequency is spawned when the Channel Access modules are loaded onto an IOC; thus, ca_pend_event() doesn't have to be called in this manner.

Connection handlers are called when a channel's connection state changes, either when it is disconnected and becomes connected, or when it's connected and becomes disconnected. Note that when we use ca_search_and_connect() to establish a connection, connect_handler1() will be called only when the connection is first established. It will not be called if the connection is never established; therefore, a connection handler cannot inform the user if a channel connection was ever established. After the connection is established, if the connection is lost for whatever reason, then, yes, the connection handler will be called and can inform the user of the channel's disconnected state.

Connection handlers can perform any task. The first connection handler, connect_handler1(), initializes the linked list and prints a message telling the user that the connection was established. Our program uses connect_handler1() to initialize the list, but then installs the second connection handler, connect_handler2(), on each channel. This handler will delete a node from the list when the node's channel becomes disconnected, freeing the memory associated with this node, and if the channel becomes re-connected, it will add a node to the list for the channel. All other list maintenance will be performed by this handler.

Here's the code for connect_handler1().

void connect_handler1(struct connection_handler_args connect_args)
{
	struct Channel *pCurrent;

	/* set point to user-supplied address */
	pCurrent = ca_puser(connect_args.chid);

	/* if connection up, intialize list or add node */
	if(connect_args.op == CA_OP_CONN_UP){
		if(pFirst == NULL){
			pFirst = pCurrent;
			pFirst->prev = NULL;
			pLast = pCurrent;
		}
		pLast->next = pCurrent;
		pCurrent->prev = pLast;
		pCurrent->next = NULL;
		pLast = pCurrent;
		printf("%s is connected.\n", pCurrent->name);
	}
	/* if connection down, print message. This block */
	/*  is unlikely to ever be used.											        */
	else
		printf("%s is not connected.\n", pCurrent->name);
	return;
}
Firstly, the pointer passed as the last argument to connect_handler1(), a pointer to the current node, is accessed by a call to the macro ca_puser(), which returns the address passed to the handler from the calling routine. The address is stored in the channel ID structure also passed to the handler. The member puser holds the address, so alternatively, the address returned by ca_puser() can be accessed with this member as in:

	pCurrent=connect_args.chid.puser;
Next, our handler checks the channel's connection state:

	 if(connect_args.op == CA_OP_CONN_UP)
This is the usual way an event handler will check a channel's connection state, by checking the op member of the connection_handler_args structure which is automatically passed to all connection handlers. To check to see if the state is down, there is the constant CA_OP_CONN_DOWN. Here is the definition of the connection_handler_args structure:

struct  connection_handler_args{
        struct channel_in_use   *chid;  /* Channel id                   */
        long                    op;     /* External codes for CA op     */
};
The structure is very simple and consists of two members: the channel ID or chid structure associated with the PV and the op member that indicates the success/failure of the operation.

The rest of the connection handler's code initializes the list. The disconnect message in the else statement at the end of the handler is unlikely to ever be used, since we are going to install another handler for each channel.

The second connection handler, connect_handler_2() is similar to connect_handler1(), except, instead of initializing a list, it deletes a node from the list when a channel is disconnected and adds a node when a channel is re-connected. It performs all the necessary link maintenance. Note how it checks the channel's connection state, performing one of two possible courses of action:

if (connect_args.op == CA_OP_CONN_DOWN)
	relink list
	delete node
else if(connect_args.op == CA_OP_CONN_UP){
	add node to list

void connect_handler2(struct connection_handler_args connect_args)
{
	struct Channel *pCurrent;
	struct Channel *pPrevious, *pNext;

	/* if connection down, free memory of user       */
	/* supplied address, and delete nodes from list. */
	if (connect_args.op == CA_OP_CONN_DOWN){
		pCurrent = ca_puser(connect_args.chid);
		if(pCurrent != pLast && pCurrent != pFirst){
			pPrevious = pCurrent->prev;
			pNext = pCurrent->next;
			pPrevious->next = pNext;
			pNext->prev = pPrevious;
			free(pCurrent);
		}
		else if (pCurrent == pLast && pCurrent != pFirst){
			pLast = pCurrent->prev;
			pLast->next = NULL;
			free(pCurrent);
		}
		else if (pCurrent == pFirst && pCurrent != pLast){
			pFirst = pCurrent->next;
			pFirst->prev = NULL;
			free(pCurrent);
		}
		else if(pCurrent == pFirst && pCurrent == pLast){
			pFirst = NULL;
			pLast = NULL;
			free(pCurrent);
		}
		printf("%s: channel disconnected.\n", ca_name(connect_args.chid));
	}
	/* if re-connected, re-allocate memory */
	else if(connect_args.op == CA_OP_CONN_UP){
		pCurrent = (struct Channel *) calloc(1, sizeof(struct Channel));
		connect_args.chid->puser = pCurrent;
		strcpy(pCurrent->name, ca_name(connect_args.chid));
		pCurrent->chan = connect_args.chid;
		if (pFirst == NULL){
			pFirst = pCurrent;
			pFirst->prev = NULL;
			pLast = pCurrent;
			pCurrent->next = NULL;
		}
		else{
			pLast->next = pCurrent;
			pCurrent->prev = pLast;
			pCurrent->next = NULL;
			pLast = pCurrent;
		}
		printf("%s: channel re-connected.\n", pCurrent->name);
	}
	return;
}

2. Writing to a PV

Our program uses ca_put_callback() to write to scalar PVs (lines 54-58) and ca_array_put_callback() to write to array PVs (lines 65-70). To write to a PV, one can a use a non-callback function such as ca_array_put() or any of its aliases: ca_bput(), ca_rput(), and ca_put(). These functions are used similar to the way ca_bget() was used in the last chapter, except that they don't generate queries, so one cannot verify if they failed or not by checking to see if ca_pend_io() times out, because ca_pend_io() will immediately return if no queries are outstanding. Alternatively, one can use the callback function ca_array_put_callback() and verify the success/failure in an event handler.

When a client program calls ca_array_put_callback() or ca_array_put_callback(), they return a status code which indicates not only that the request was valid, but also that the operation succeeded.

A client program can then flush the ca_put_callback() request from the request buffer by calling ca_pend_event().

In this call for the scalar PV, its first argument is the external data type that is to be sent to the database. If the type does not match the PV's native type, the server will perform the necessary conversion(s). In this case, we are sending it a string pointer, thus the database request type is DBR_STRING. The section Data Types in the previous chapter fully explains the different data types available, and some of the ways a client program can use them.

The third argument is the PV's chid, and the fourth is the pointer to the data to be sent to the server, in this case a string. The next argument is the name of an event handler to be called when the put operation completes. The last argument is a pointer to any object. This pointer can then be accessed from within the event handler.

Basically, ca_array_put_callback() operates similarly to the ca_get_callback() functions described in the previous chapter, except that it changes a PV's value instead of retrieving it. After the put operation is completed, the event handler is called. Remember that when an operation completes, it doesn't mean that it was successful, only that the server tried to satisfy the request. The event handler specified by ca_put_callback() should at least check the status returned by the server to see if the operation was successful. Remember that for most operations, checking the status code returned by the call itself is only good for checking the validity of the request. Within the event handler, however, the status of the operation as returned by the server can be directly checked.

The event handler called by our program is called an_event_handler(). It is very simple. It checks the status returned from the server to see if the operation failed, i.e., if the status is not ECA_NORMAL. If it isn't, an appropriate message is printed; SEVCHK() is called and is passed the status.

void an_event_handler(struct event_handler_args handler_args)
{
  
	if (handler_args.status != ECA_NORMAL){
		printf("channel %s: put operation not sucessful.\n",
				ca_name(handler_args.chid));
		SEVCHK(handler_args.status, NULL);
	}
	else
		printf("channel %s: put operation completed.\n"); 
	return;
}
If the operation's status is ECA_NORMAL, a message indicating that the operation was successfully completed is printed.

The other members of event_handler_args are explained in the section Event Handlers in the previous chapter. To review, there is a member that indicates what the type of the data is (type), a pointer to the data (dbr), a user supplied pointer (usr), the status of the operations (status), an integer indicating the number of elements of the PV, and the PV's chid. For event handlers that are used with ca_array_put_callback(), status indicates the success or failure of the put, type is the database request or DBR type of the value sent to the server, count indicates the PV's native element count, and dbr is a NULL pointer. It is important to note that the dbr element of an event_handler_args structure is always NULL for a put operation. Thus, your program should not try to de-reference dbr in an event handler specified by ca_array_put_callback(); de-referencing it will not retrieve the value sent to the database.

If the PV entered by the user is an array PV, i.e., consists of more than one element, the program allows the user to enter up to n values where n is the maximum number of elements of the PV. The user can type in a 'q' if he/she wants to send less than the maximum number of elements. It is important to note that Channel Access does not allow a put operation to specify an offset from the beginning of the array. If, for example, the user enters five values to be sent to a PV. Our variable pValue will contain five values, pValue[0] to pValue[4]. When this array is sent to the server, the first five elements of the PV will be changed. There is no way to specify an offset so that, for instance, the fifth through tenth elements are changed. The put operation always begins at the first element of the PV.

Our program gets the values entered by the user as floating-point numbers. Thus, the call to ca_array_put_callback() uses DBR_FLOAT as the first argument. This was done because in order to change an array PV using DBR_STRING, we would need an array of strings, which is easily done, but instead our program simply sends an array of floating point values to the PV.

To flush the request, our program then calls ca_pend_event(), which pends for 0.25 seconds, during which the callback function can execute. If a PV's channel has been disconnected, its connection handler will also execute in this time frame.

Well, that's our program, except for the search() function, which just finds the node in the linked list that contains the matching PV name.

struct Channel *search(char *Name)
{
	struct Channel *pCurrent;
	int flag = 0;
	if(pFirst != NULL){
		for(pCurrent = pFirst; pCurrent != NULL; pCurrent = pCurrent->next){
			if(strcmp(pCurrent->name, Name) == 0){
				flag = 1;
				break;
			}
		}
	}
	if(flag)
		return (pCurrent);
	else
		return (NULL);
}
For more information on data types, see the section Data Types in the previous chapter. You may also find the section Event Handlers helpful.

3. Exception Handlers

As with get operations, a client program can send values to PVs using a client call that specifies an event handler or using one that does not specify an event handler. Which one is better depends on the application.

The following "put" calls do not specify event handlers. The basic call is ca_array_put(). The other calls are just aliases for ca_array_put().

ca_array_put()--accepts four arguments: the DBR type, the number of elements to be changed in the PV starting with the first element, the chid, and a pointer to the array or variable that will hold the element(s) to be sent to the PV.

ca_bput()--sends value to a PV as a string. Accepts a chid and a string pointer as its two arguments.

ca_rput()--sends a floating point value to a PV. Accepts a chid and a pointer-to-float as its two arguments.

ca_put()--like ca_array_put() but is used for scalar PVs or to change the first element of an array PV. Accepts same arguments as ca_array_put() except that the element count is not passed to it.

These calls work like all non-callback functions. They cannot directly indicate the status of remote operations, that is, the integer that they return merely indicates the validity of the request, not the success of the operation in the database. These calls can be flushed using ca_pend_io(), ca_pend_event(), or ca_flush_io(). However, the programmer must be aware that these calls do not generate queries. Thus, the success or failure of these calls cannot be checked by the flushing them with ca_pend_io() and then checking to see if ca_pend_io() times out. Thus, in the following piece of code, ca_pend_io() will never time out. Remember that when ca_pend_io() flushes a request or requests that do not generate queries, it simply flushes the request buffer and returns without performing any other tasks.

status = ca_put(DBR_FLOAT, chand_id, pFloat);
SEVCHK(status, "invalid request");
status = ca_pend_io(2.0)
if (status = ECA_TIMEOUT)
	printf("ca_put() failed.\n");
That doesn't mean there's no way to tell if a put operation that specifies no event handler has failed. For all failed operations, an exception handler is executed. An exception is an event that occurs when the server cannot perform a request for whatever reason. For any Channel Access task, there can be only one exception handler. All tasks are initialized with a default exception handler, which merely prints a message describing the error and the severity of the error. For some errors, the message prints the host on which the error occurred. For other errors, it may also print the line number in the client program's source file where the error occurred. For example, the following exception message occurred when the server could not perform a put request by a client program:

CA.Client.Diagnostic..............................................
    Message: "Could not perform a database value put for that channel"
    Severity: "Warning" Context: "Could not perform a database value put for that channel"
    Source File: ezee1.c Line Number: 218
..................................................................
A client program can replace the default exception handler by calling ca_add_exception_event(). This call accepts two arguments: (1) the name of the exception handler defined by the client program and (2) a pointer to any object, which the exception handler can then access.

Like other event handlers, an exception handler accepts a structure defined in cadef.h as its only argument. The structure for exception handlers is called exception_handler_args, and is defined as the following:

struct 	exception_handler_args{
	void			*usr;	  /* User argument supplied when event added 	*/
	struct channel_in_use	*chid;	 /* Channel id			 		*/
	long 			type;	  /* Requested type for the operation		*/
	long 			count; 	/* Requested count for the operation		*/
	void 			*addr; 	/* User's address to write results of CA_OP_GET	*/
	long			stat;	  /* Channel access std status code 		*/
	long			op;	/* External codes for channel access operations	*/
	char			*ctx;	/* A character string containing context info	*/
};
Except for ctx and addr, most of these members should be self-explanatory. ctx is a string that indicates the host where the error occurred. The member addr is simply an address where the Channel Access software writes the result of a get operation, so it can be used to identify the particular operation that failed.

Calling ca_add_exception_event() for a second time with NULL as the first argument, in place of a function name, will re-install the default exception handler. For more information on event handlers in general, see the section Event Handlers in the previous chapter.

An Example Program
1. - Connection Handlers
2. - Writing to a PV
3. - Exception Handlers

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.