Message system independent API
William Lupton, 01-Jul-99

Introduction

This note describes a message system independent API which has been defined in order to allow the EPICS sequencer to work with multiple underlying message systems, e.g. CA, CDEV, or the Keck-specific KTL.

Several existing tools, including medm and alh, have both CA and CDEV versions. Each tool has its own method of handling the two systems.

An alternative approach is to define an API which is independent of both the tool and the underlying message systems and to write tools to use it rather than explicitly calling routines of a specific message system. The result is that new message systems can be added without changing tools (at most re-linking them and providing some way for them to select the message system).

Such an API has been defined as part of the port of the EPICS sequencer to Unix. It is referred to as the "PV" (process variable) API. It has been implemented for CA (pvCa) and for KTL (pvKtl), with a CDEV implementation to follow, and the sequencer's CA calls have been changed to PV calls. The API has been designed to have a close fit to CA and it should always be easy to convert applications from CA to PV.

The standard "demo" sequence (three state sets) runs unchanged under Unix using both pvCa and pvKtl. The same sequence runs under VxWorks using pvCa (there is no VxWorks version of KTL).

Design criteria

  1. close fit to CA API (for ease of porting of CA applications); leads to use of db_access_val-type approach rather than gdd-type approach
  2. not dependent on any CA or DB definitions (is dependent on OSIsem for locks and OSItime - and therefore tsDefs - for absolute time)
  3. only support what the sequencer needs (but extensible)
  4. implement in C++ but provide C API

Location

The files are all in the unbundled sequencer tree, which will be placed in the CVS repository at ANL as unbundled/seq2k.

Base classes

The file pv.h contains various utility typedefs, e.g.:

typedef enum {
    pvStatOK      = 0,
    pvStatERROR   = -1,
    pvStatDISCONN = -2,
    pvStatTIMEOUT = -3
} pvStat;

typedef enum {
    pvSevrERROR   = -1,
    pvSevrNONE    = 0,
    pvSevrMINOR   = 1,
    pvSevrMAJOR   = 2,
    pvSevrINVALID = 3
} pvSevr;

typedef enum {
    pvTypeERROR       = -1,
    pvTypeCHAR        = 0,
    pvTypeSHORT       = 1,
    pvTypeLONG        = 2,
    pvTypeFLOAT       = 3,
    pvTypeDOUBLE      = 4,
    pvTypeSTRING      = 5,
    pvTypeTIME_CHAR   = 6,
    pvTypeTIME_SHORT  = 7,
    pvTypeTIME_LONG   = 8,
    pvTypeTIME_FLOAT  = 9,
    pvTypeTIME_DOUBLE = 10,
    pvTypeTIME_STRING = 11
} pvType;

and also a pvValue union which supports the pvType types. It then defines virtual base classes pvSystem (each underlying message system sub-classes this and returns an instance of the sub-classed object) and pvVariable (message systems also sub-class this; there is one instance  per process variable).

/*
 * Connect (connect/disconnect and event (get, put and monitor) functions
 */
typedef void (*pvConnFunc)( void *var, int connected );

typedef void (*pvEventFunc)( void *var, pvType type, int count,
                             pvValue *value, void *arg, pvStat status );

/*
 * System
 *
 * This is somewhat analogous to a cdevSystem object (CA has no equivalent
 * and must therefore use ca_static, ca_import() and ca_import_cancel())
 */

class pvSystem {

public:
    pvSystem( int debug = 0 );
    virtual ~pvSystem();

    inline pvSystem &getSystem() { return *this; }

    virtual pvStat flush() = 0;
    virtual pvStat pend( double seconds = 0.0, int wait = FALSE ) = 0;

    virtual pvVariable *newVariable( char *name, pvConnFunc func = NULL,
                                     int debug = 0 ) = 0;

    void lock();
    void unlock();

    inline int getMagic() { return magic_; }
    inline void setDebug( int debug ) { debug_ = debug; }
    inline int getDebug() { return debug_; }

    inline void setError( int status, pvSevr sevr, pvStat stat,
                          const char *mess )
        { status_ = status; sevr_ = sevr; stat_ = stat; mess_ = mess; }
    inline int getStatus() { return status_; }
    inline pvSevr getSevr() { return sevr_; }
    inline pvStat getStat() { return stat_; }
    inline const char *getMess() { return mess_; }

private:
    int         magic_;         /* magic number (used for authentication) */
    int         debug_;         /* debugging level (inherited by pvs) */

    int         status_;        /* message system-specific status code */
    pvSevr      sevr_;          /* severity */
    pvStat      stat_;          /* status */
    const char  *mess_;         /* error message */

    OSIsemBinary lock_;         /* prevents more than one thread in library */
};

////////////////////////////////////////////////////////////////////////////////
/*
 * Process variable
 *
 * This is somewhat analogous to a cdevDevice object (or a CA channel)
 */
class pvVariable {

public:
    pvVariable( pvSystem &system, char *name, pvConnFunc func = NULL,
                int debug = 0 );
    virtual ~pvVariable();

    virtual pvStat get( pvType type, int count, pvValue *value ) = 0;
    virtual pvStat getNoBlock( pvType type, int count, pvValue *value ) = 0;
    virtual pvStat getCallback( pvType type, int count,
                pvEventFunc func, void *arg = NULL ) = 0;
    virtual pvStat put( pvType type, int count, pvValue *value ) = 0;
    virtual pvStat putNoBlock( pvType type, int count, pvValue *value ) = 0;
    virtual pvStat putCallback( pvType type, int count, pvValue *value,
                pvEventFunc func, void *arg = NULL ) = 0;
    virtual pvStat monitorOn( pvType type, int count,
                pvEventFunc func, void *arg = NULL,
                pvCallback **pCallback = NULL ) = 0;
    virtual pvStat monitorOff( pvCallback *callback = NULL ) = 0;

    virtual int getConnected() = 0;
    virtual pvType getType() = 0;
    virtual int getCount() = 0;

    inline int getMagic() { return magic_; }
    inline void setDebug( int debug ) { debug_ = debug; }
    inline int getDebug() { return debug_; }
    inline pvConnFunc getFunc() { return func_; }

    inline pvSystem &getSystem() { return system_; }
    inline char *getName() { return name_; }
    inline void setPrivate( void *priv ) { private_ = priv; }
    inline void *getPrivate() { return private_; }

private:
    int         magic_;         /* magic number (used for authentication) */
    int         debug_;         /* debugging level (inherited from system) */
    pvConnFunc  func_;          /* connection state change function */

    pvSystem    &system_;       /* associated system */
    char        *name_;         /* variable name */
    void        *private_;      /* client's private data */
};

pv.h also defines a pvCallback class, which is used for communicating information to callback handlers. Finally, it defines the C interface, of which a few routines are:

pvStat pvSysCreate( char *name, void **pSys );
pvStat pvSysDestroy( void *sys );
pvStat pvSysFlush( void *sys );
pvStat pvSysPend( void *sys, double seconds, int wait );
pvStat pvSysLock( void *sys );
pvStat pvSysUnlock( void *sys );

pvStat pvVarCreate( void *sys, char *name, pvConnFunc func, void **pVar );
pvStat pvVarDestroy( void *var );
pvStat pvVarGet( void *var, pvType type, int count, pvValue *value );
pvStat pvVarGetNoBlock( void *var, pvType type, int count, pvValue *value );
pvStat pvVarGetCallback( void *var, pvType type, int count,
                         pvEventFunc func, void *arg );

The file pv.cc implements constructors and destructors for pvSystem, pvVariable and pvCallback. It also implements the pvSystem lock() and unlock() methods (which can be used by message systems if they need them). Finally, it implements the C interface. This does not have to be implemented for the individual message systems.

CA classes

The files pvCa.h and pvCa.cc implement caSystem and caVariable classes, which implement pvSystem and pvVariable respectively. Because the PV API is such a close match with the CA API, only the chid has to be held as private data.

Here are some examples.

/* invoke CA function and send error details to system object */
#define INVOKE(_function) \
    do { \
        int _status = _function; \
        getSystem().setError( _status, sevrFromCA( _status ), \
                    statFromCA( _status ), ca_message( _status ) ); \
    } while ( FALSE )

caSystem::caSystem( int debug ) :
    pvSystem( debug )
{
    if ( getDebug() > 0 )
        printf( "%8p: caSystem::caSystem( %d )\n", this, debug );

    INVOKE( ca_task_initialize() );
}

pvStat caVariable::monitorOn( pvType type, int count, pvEventFunc func,
                              void *arg, pvCallback **pCallback )
{
    if ( getDebug() > 0 )
        printf( "%8p: caVariable::monitorOn( %d, %d )\n",
                this, type, count );

    pvCallback *callback = new pvCallback( *this, type, count, func, arg,
                                           getDebug() );

    evid id = NULL;
    INVOKE( ca_add_masked_array_event( typeToCA( type ), count, chid_,
                        monitorHandler, callback, 0.0, 0.0, 0.0,
                        &id, DBE_VALUE|DBE_ALARM  ) );
    callback->setPrivate( id );

    if ( pCallback != NULL )
        *pCallback = callback;
    return getSystem().getStat();
}

static void monitorHandler( struct event_handler_args args )
{
    pvCallback &callback = * ( pvCallback * ) args.usr;
    pvEventFunc func = callback.getFunc();
    pvVariable &variable = callback.getVariable();
    pvType type = callback.getType();
    int count = callback.getCount();
    void *arg = callback.getArg();

    pvValue *value = new pvValue[count]; // ### larger than needed
    copyFromCA( args.type, args.count, ( union db_access_val * ) args.dbr,
                value );
    // ### should assert args.type is equiv to type and args.count is count
    ( *func ) ( ( void * ) &variable, type, count, value, arg,
                statFromCA( args.status ) );
    delete [] value;
}

KTL classes

The files pvKtl.h and pvKtl.cc implement ktlSystem and ktlVariable classes, which implement pvSystem and pvVariable respectively. The PV API is not such a close match with the KTL CA API as it is with the KTL API and an extra ktlService class is also defined (each PV is associated with a KTL service, so each ktlVariable object has a reference to the corresponding ktlService object).

KTL is not of general interest to the EPICS community, so only one code example is given. Note that KTL is not thread-safe and can activate shareable libraries which may not be thread-safe, so a lock is taken around most KTL calls. For example

/* lock / unlock shorthands */
#define LOCK   getSystem().lock()
#define UNLOCK getSystem().unlock()

pvStat ktlVariable::put( pvType type, int count, pvValue *value )
{
    if ( getDebug() > 0 )
        printf( "%8p: ktlVariable::put( %d, %d )\n", this, type, count );

    KTL_POLYMORPH ktlValue;
    INVOKE( copyToKTL( type, count, value, type_, count_, &ktlValue ) );
    if ( getSystem().getStat() == pvStatOK ) {
        LOCK;
        INVOKE( ktl_write( getHandle(), KTL_WAIT, keyName_, NULL, &ktlValue,
                           NULL ) );
        UNLOCK;
        freeKTL( type_, &ktlValue );
    }

    return getSystem().getStat();
}

CDEV classes

The files pvCdev.h and pvCdev.cc will implement cdevSystem and cdevVariable classes, which will implement pvSystem and pvVariable respectively. The CDEV and CA APIs are fairly similar and implementation will be quite easy.

Problems / comments

Performance

  1. as can be seen from the code for the CA monitorHandler() above, the main limitation to monitor performance wrt CA is the need to convert the value from the DB type to the PV type
  2. I didn't want to make the API dependent on EPICS-specific types; however, if there were an OSI package for types and values then that wouldn't be a problem; if CA started to use it too then no conversion would be necessary

Error handling is poor

  1. no attempt is made to propagate server status
  2. status values and error messages are temporarily stored in the system object, which ignores multi-threaded issues
  3. could implement a "maximize severity" and/or error stack scheme
  4. could use C++ exceptions
  5. could maintain on a per-variable rather than a per-system basis

OSI issues

  1. PV defines its own pvStamp structure so that it need not be dependent on TS_STAMP; however, it ends up being dependent on it anyway, because OSItime is dependent on it; would be nice if OSItime exported an OSItimeStamp, maybe of Unix epoch?
  2. would be very nice if OSI provided a set of OSI types and type conversion routines (I thought about using ait but it seemed too complicated; I certainly didn't want to use gdd)