Bus API

Bus Interface Class

StreamDevice already comes with an interface to asynDriver. You should first try to implement your bus driver compatible to asynDriver. Then it can be used by StreamDevice automatically. Only if that does not work, write your own bus interface.

A bus interface is a C++ class that inherits from StreamBusInterface. Its purpose is to provide an interface to StreamDevice for a low-level I/O bus driver. StreamDevice acts as a client of the interface, calling interface methods and receiving replies via callbacks. Since the internal details of StreamDevice are not of interest to a bus interface, I will reference it simply as client in this chapter. The interface class must be registered via a call to RegisterStreamBusInterface() in the global context of the C++ file (not in a header file).

Interface methods called by the client must not block for arbitrary long times. That means the interface is allowed to take mutex semaphores to protect its internal data structures but it must not take event semaphores to wait for external I/O or similar.

It is assumed that the interface creates a separate thread to handle blocking I/O and to call the callback methods in the context of that thread when I/O has completed or timed out. The callback methods don't block but may in turn call interface methods. Much of the actual work will be done in the context of those callbacks, i.e. in the interface thread, thus be generous with stack.

Example bus interface class declaration

#include <StreamBusInterface.h>

class MyInterface : StreamBusInterface
{
    // ... (internally used attributes and methods)

    MyInterface(Client* client);
    ~MyInterface();

    // StreamBusInterface virtual methods
    bool lockRequest(unsigned long lockTimeout_ms);
    bool unlock();
    bool writeRequest(const void* output, size_t size,
        unsigned long writeTimeout_ms);
    bool readRequest(unsigned long replyTimeout_ms,
        unsigned long readTimeout_ms,
        long expectedLength, bool async);
    bool acceptEvent(unsigned long mask,
        unsigned long replytimeout_ms);
    const char* busName();
    void release();
    bool supportsEvent();
    bool supportsAsyncRead();
    void cancelAll();

public:
    // creator method
    static StreamBusInterface* getBusInterface(
        Client* client, const char* busname,
        int addr, const char* param);
};

// ... (implementation)

RegisterStreamBusInterface(MyInterface);

Methods to implement

The interface class must implement a public static creator method:

static StreamBusInterface* getBusInterface(Client* client, const char* busname, int addr, const char* param);

And it must implement the following pure virtual methods:

bool lockRequest(unsigned long lockTimeout_ms);
bool unlock();

It may implement additional virtual methods if the bus supports it:

bool writeRequest(const void* output, size_t size, unsigned long writeTimeout_ms);
bool readRequest(unsigned long replyTimeout_ms, unsigned long readTimeout_ms, long expectedLength, bool async);

bool supportsAsyncRead();
bool supportsEvent();
bool acceptEvent(unsigned long mask, unsigned long replytimeout_ms);
bool connectRequest(unsigned long connecttimeout_ms);
bool disconnect();

void cancelAll();

It also may override the following virtual methods:

bool setEos(const char* eos, size_t eoslen);
void release();

Callback methods provided

The base class StreamBusInterface implements a set of protected callback methods which must be called in response to the above request methods (most probably from another thread):

void lockCallback(IoStatus status);
void writeCallback(IoStatus status);
long readCallback(IoStatus status, const void* input = NULL, long size = 0);
void eventCallback(IoStatus status);
void connectCallback(IoStatus status);

Other provided methods, attibutes, and types

StreamBusInterface(Client* client);
long priority();
const char* clientName();
const char* eos;
size_t eoslen;
enum IoStatus {ioSuccess, ioTimeout, ioNoReply, ioEnd, ioFault};

Theory of Operation

Registration

RegisterStreamBusInterface(interfaceClass);

During initialization, the macro RegisterStreamBusInterface() registers the bus interface. It must be called exactly once for each bus interface class in global file context.

Creation and deletion

static StreamBusInterface* getBusInterface(Client* client, const char* busname, int addr, const char* param);
StreamBusInterface(Client* client);
void release();
const char* clientName();

During startup, each client instance searches for its bus interface by name. It does so by calling the static getBusInterface() method of every registered interface class. This method should check by busname if its interface class is responsible for that bus. If yes, it should check if the address addr is valid and associate a device with busname/addr. Some busses do not have addresses and allow only one device (e.g. RS232). Interfaces to such busses can ignore addr. The bus interface may then try to connect to the device, but it should allow it to be disconnected or switched off at the moment. If the bus interface requires additional parameters, parse the param string. Your constructor should pass client to the base class constructor StreamBusInterface(Client* client).

On success, getBusInterface should then return a pointer to a bus interface instance. Note that many client instances may want to connect to the same device. Each needs its own bus interface instance. The bus interface can get a string containing the name of the client instance from clientName(). This name is for use in error and log messages.

On failure, or if this interface class is not responsible for that bus, getBusInterface should return NULL. The client will then try other bus interface classes.

When the client does not need the interface any more, it calls release(). The default implementation of release() assumes that getBusInterface() has allocated a new bus interface and just calls delete. You should change release() if that assumption is not correct.

Connecting and disconnecting

bool connectRequest(unsigned long connecttimeout_ms);
bool disconnect();
void connectCallback(IoStatus status);

Connection should be handled automatically. If the device is disconnected, each attempt to access the device should try to (re-)connect. Normally, the interface should not try to disconnect unless the device does so.

However, sometimes the client wants to connect or disconnect explicitely. To connect, the client calls connectRequest(). This function should return true immediately or false if the request cannot be accepted. The interface should call connectCallback(ioSuccess) once the bus could be connected. If the bus cannot be connected within connecttimeout_ms milliseconds, the bus interface should call connectCallback(ioTimeout).

If a device cannot be connected, for example because there is something wrong with the I/O hardware, connectCallback(ioFault) may be called.

To disconnect, the client calls disconnect(); This function should return true immediately or false if disconnecting is impossible. There is no callback for disconnect().

Bus locking

bool lockRequest(unsigned long lockTimeout_ms);
void lockCallback(IoStatus status);
bool unlock();
long priority();

Before doing output, the client calls lockRequest() to get exclusive access to the device. This function should return true immediately or false if the request cannot be accepted. If the device is already locked, the bus interface should add itself to a queue, sorted by priority(). As soon as the device is available, the bus interface should call lockCallback(ioSuccess). If the bus cannot be locked within lockTimeout_ms milliseconds, the bus interface should call lockCallback(ioTimeout).

If a device cannot be locked, for example because there is something wrong with the I/O hardware, lockCallback(ioFault) may be called.

Normally, it is not necessary to lock the complete bus but only one device (i.e. one address). Other clients should still be able to talk to other devices on the same bus.

The client may perform several read and write operations when it has locked the device. When the dialog with the device has ended, the client calls unlock(). If other bus interfaces are in the lock queue, the next one should call lockCallback(ioSuccess) now.

Writing output

bool writeRequest(const void* output, size_t size, unsigned long writeTimeout_ms);
void writeCallback(IoStatus status);

To start output, the client calls writeRequest(). You can safely assume that the device has already been locked at this time. That means, no other client will call writeRequest() for this device and no other output is currently active for this device until it has been unlocked.

The function should arrange transmission of size bytes of output but return true immediately or false if the request cannot be accepted. It must not block until output has completed. After all output has been successfully transmitted, but not earlier, the interface should call writeCallback(ioSuccess).

If output blocks for writeTimeout_ms milliseconds, the interface should abort the transmision and call writeCallback(ioTimeout).

If output is impossible, for example because there is something wrong with the I/O hardware, writeCallback(ioFault) may be called.

The interface must transmit excactly the size bytes from output. It must not append or change anything and it should not assume that any bytes have a special meaning. In particular, a null byte does not terminate output and no terminator must be added. The buffer referenced by output stays valid until writeCallback() is called.

The client may request more I/O or call unlock() after writeCallback() has been called.

Reading input

bool readRequest(unsigned long replyTimeout_ms, unsigned long readTimeout_ms, long expectedLength, bool async);
long readCallback(IoStatus status, const void* input = NULL, long size = 0);
bool setEos(const char* eos, size_t eoslen);
bool supportsAsyncRead();

The client calls readRequest() to tell the bus interface that it expects input. Depending on the bus, this function might have to set the bus hardware into receive mode. If expectedLength>0, the the bus interface should stop input after this number of bytes have been received. In opposite to writing, the device may be in a non-locked status when readRequest() is called.

This function must not block until input is available. Instead, it should arrange for readCallback(ioSuccess,buffer,size) to be called when input has been received and return true immediately or false if the request cannot be accepted.

Here, buffer is a pointer to size input bytes. The bus interface is responsible for the buffer. The client copies its contents. It does not modify or free it.

It is not necessary to wait until all data has been received. The bus interface can call n=readCallback() after any amount of input has been received. If the client needs more input, readCallback() returns a non-zero value. A positive n means, the client needs another n bytes of input. A negative n means, the client needs an unspecified amount of additional input.

With some bus interfaces, readRequest() might not have to do anything because the bus is always receiving. It might also be that the bus has no local buffer associated to store input before it is fetched with some read() call. In this case, a race condition between device and client can occure. To avoid loss of data, readCallback(ioSuccess,buffer,size) may be called in this case even before readRequest(). If the client is expecting input in the next future, it will store it. Otherwise the input is dropped.

The replyTimeout_ms parameter defines how many milliseconds to wait for the first byte of a reply before the device is considered offline. If no input has been received after replyTimeout_ms milliseconds, the bus interface should call readCallback(ioNoReply).

The readTimeout_ms parameter is the maximum time to wait for further input. If input stops for longer than readTimeout_ms milliseconds the bus interface should call readCallback(ioTimeout,buffer,size). The client decides if this timeout is an error or a legal termination. Thus, pass all input received so far.

At the start of a communication, the client calls setEOS() (i.e. before the first readRequest() is called). The eos string is a hint to the bus interface to recognize the end of an input. Once the eos string is found, the bus interface should stop receiving input and call readCallback(ioSuccess,buffer,size). It should not try to remove the eos string from the input received. An empty eos string means: Don't look for eos.

Some busses (e.g. GPIB) support special "end of message" signals. If such a signal is received, the bus interface should call readCallback(ioEnd,buffer,size). Do not use ioEnd just because eos has been found! Only use it to indicate a special "end of message" signal which is not visible in the normal byte data stream.

If input is impossible, for example because there is something wrong with the I/O hardware, readCallback(ioFault) may be called.

If the async flag is true, the client wants to read input asyncronously without any timeout. That means, the bus interface should call readCallback() even if the input was requested by another client.

Sorry, this documentation is not yet complete.


Dirk Zimoch, 2006