Overview
There are two aspects to device code, accessing the device and the device functionality. This section describes both of these aspects of device code.
Accessing a Device
Devices are usually accessed by threads since the device typically blocks outside of CLIP and would therefore stall a method worker thread. However, when a device is created in a circuit, the device is owned by the circuit. So a thread wishing to access a device using a Reference Connection (a device is not a CLIP event provider/consumer object and so we cannot connect using an Event Connection) must access the device via the parent circuit. Depending on the device, it may need to be initialized before accessing and terminated after use to release any resources, others may simply need to be opened and closed as required.
Example
Uns Cct1_IOThrdElem::Initialise()
{
Uns failed = FALSE;
failed |= ! Cct1_IOThrdBaseElem::Initialise();
// TODO: Add custom initialize code
// initialize device
Cct1IODev& dev = Parent().mIODev[this->ElemNum()];
dev.Initialise( TCP_SERVER, TCP_IO_PORT_NUM );
dev.Connect();
return ! failed;
}
Uns Cct1_IOThrdElem::Execute()
{
Uns failed = FALSE;
failed |= ! Cct1_IOThrdBaseElem::Execute();
// TODO: Add custom initialize code
// open device
Cct1IODev& dev = Parent().mIODev[this->ElemNum()];
dev.Open();
switch( this->ElemNum() )
{
case 0:
while ( ! failed )
{
// read from device
const Uns numRead = dev.Read( this->Workspace().Buffer(), this->Workspace().BufferSize() );
if ( numRead == 0 )
failed = TRUE;
else
{
// do something with buffer
}
}
break;
default:
// do something else with device
}
// close device
dev.Close();
return ! failed;
}
Uns Cct1_IOThrdElem::Exit()
{
Uns failed = FALSE;
failed |= ! Cct1_IOThrdBaseElem::Exit();
// TODO: Add custom initialize code
// terminate device
Parent().mIODev[this->ElemNum()].Disconnect();
return ! failed;
}
When two or more threads access a shared device, the device should only be initialized/terminated by one of them, or better still, the owning circuit should initialize/terminate it.
Device Functionality
When defining a device a user can create a new device or derive a class from an existing device. To create a device from an existing device simply specify the device type in the Device Properties Type field
This will then be shown in the device itself
The translator will then automatically derive IO device from the specified type ClpTcpDev and generate class stubs for ClpTcpDev if it doesnt already exist in the project.
#include <ClpTcpDev.hpp>
class Cct1_IODev : public ClpTcpDev
{
public:
...
};
When writing devices from scratch, developers should consider 'future proofing' and hiding the underlying hardware from the application by abstracting the basic functionality of the device. This simplifies code changes when the device changes or the application is moved to another platform. To this end developers should provide a basic set of standard functions, that provide the basic functionality of any device: Initialise, Open, Close, Read, Write, and optionally Ctrl, LastError.
It is common practice (although not-universal) to provide return codes, where Negative numbers represent errors (0 generic error) and Positive numbers represent non-errors.
class MyDevice
{
private:
Uns mLastError;
<Struct> mParams;
public:
typedef enum
{
// error codes
GENERIC_FAILURE = 0;
INIT_INVALID_PARAMS = -1;
OPEN_INVALID_NAME = -2;
OPEN_INVALID_MODE = -3;
...
// success codes
NO_ERROR = 1;
...
} ERROR_CODES;
// Basic Initialization of the device - to be called before any Open/Read/Write access
// if initialise fails
// set last error
// return MyDevice::GENERIC_FAILURE
// else
// clear last error
// return MyDevice::NO_ERROR
Uns Initialise( <Struct>& Params );
// Open a device for use - to be called before any Read/Write access
// if open fails
// set last error
// return MyDevice::GENERIC_FAILURE
// else
// clear last error
// return MyDevice::NO_ERROR
Uns Open( Char* Name, Uns Mode );
// Read from a device into a buffer 'Dest', 'Dest' must be big enough to hold NumToRead
// Read will block until request is fulfilled or an error occurs
// if read fails
// set last error
// return MyDevice::GENERIC_FAILURE
// else
// clear last error
// return NumRead
Uns Read( void* Dest, Uns NumToRead );
// Write from buffer 'Srce'
// Write will block until request is fulfilled or an error occurs
// if write fails
// set last error
// return MyDevice::GENERIC_FAILURE
// else
// clear last error
// return NumWritten
Uns Write( void* Srce, Uns NumToWrite );
// Close the device, no further Read/Writes are allowed until it is re-opened
// if close fails
// set last error
// return MyDevice::GENERIC_FAILURE
// else
// clear last error
// return MyDevice::NO_ERROR
Uns Close();
// Optional - Modify any parameters whilst the device is open
// if ctrl fails
// set last error
// return MyDevice::GENERIC_FAILURE
// else
// clear last error
// return MyDevice::NO_ERROR
Uns Ctrl( <Struct>& Params );
// Optional - Get the last known error code
// return ErrorCode
Uns LastError();
};
NOTE: For most hardware devices, the Read/Write functions will block until they fulfil their purpose, however some may return immediately. Developers should append comments in the device header file as to which API is provided and where possible provide both blocking and non-blocking variants.
In addition to the basic functions required by virtually every device, a device may provide additional specific functionality. Developers can provide additional functions to handle this functionality, but should ensure that the functions are generic to the device type and not to the hardware, by performing NULL actions and returning sensible values should a specific hardware/platform not provide the functionality.
Eg.
Uns MyDevice::Rewind()
{
Uns status;
mLastError = MyDevice::NO_ERROR;
#ifdef TAPE_DEVICE
if ( mTape.Rewind() ) // rewind tape
status = MyDevice::NO_ERROR;
else
{
mLastError = MyDevice::REWIND_ERROR;
status = MyDevice::GENERIC_FAILURE;
}
#else // disk
status = MyDevice::NO_ERROR; // no need to rewind
#endif
return status;
}