StreamDevice implements the generic part of an EPICS device support. However it cannot know the internals of a specific record type, such as the .VAL or .RVAL fields or the .INP or .OUT links. It can only access a record as dbCommon. Thus it is necessary to write an interface for each record type which takes care of these details.
A record interface consists of three functions, readData()
and
writeData()
and initRecord()
.
initRecord()
.
The name of the device support structure must have the form
devrecordtypeStream
and the name of the record
interface source code file must be
devrecordtypeStream.c
to work seamlessly with the
build system implemented in the Makefile of StreamDevice.
Finally add recordtype
to the
RECORDTYPES
variable in the file src/CONFIG_STREAM
and rebuild.
A record interface typically #include
s the header file
for the supported record type, "recordtypeRecord.h"
and "devStream.h"
.
For many record interfaces this is sufficient, but sometimes additional
header files may be needed.
A record interface has to implement three functions:
static long readData(dbCommon *record, format_t *format);
static long writeData(dbCommon *record, format_t *format);
static long initRecord(dbCommon *record);
The function writeData()
is called whenever a
protocol needs
to handle a prining format converter
(without redirection),
typically in an out
command.
It is also possible that writeData()
is called for an input
record, e.g. when the =
flag
is used in an in
command.
Thus implement this function for input records as well.
The functions is called with a dbCommon *record
argument,
which the function should cast to the specific record type to get
access to the record specific fields, in particular .VAL and .RVAL.
The second argument, format_t *format
, contains information
about the format converter. The only field of interest in this argument
is format->type
which specifies the data type of the format
conversion. Its value is one of DBF_ULONG
,
DBF_ULONG
, DBF_ENUM
, DBF_DOUBLE
,
or DBF_STRING
.
The writeData()
function may access different fields depending
on format->type
, e.g. .VAL for DBF_DOUBLE
but
.RVAL for DBF_LONG
.
It also may interpret the fields in a different way, e.g. cast to
long
for DBF_LONG
but to unsigned long
for DBF_ULONG
.
This is typically done with a switch(format->type)
statement.
The function may refuse to handle format->type
values
that make no sense for the record type, e.g. DBF_STRING
for a
record type that cannot handle strings. In that case the function should
return ERROR
. It is a good idea to return ERROR
in the default
part of the switch
statement.
StreamDevice provides a function to output a value from the record:
long streamPrintf(dbCommon *record, format_t *format, ...);
Once the correct record field and type cast has been chosen, the
writeData()
function calls
return streamPrintf(record, format, value)
where the type of
value should match field->type
(long
,
unsigned long
, double
, or char*
),
returning the result of that call.
Example:
static long writeData(dbCommon *record, format_t *format) { recordtypeRecord *rec = (recordtypeRecord *)record; switch (format->type) { case DBF_ULONG: case DBF_ENUM: return streamPrintf(record, format, (unsigned long)rec->rval); case DBF_LONG: return streamPrintf(record, format, (long)rec->rval); case DBF_DOUBLE: return streamPrintf(record, format, rec->val); default: return ERROR; } }
The arguments of this function are the same as for writeData()
.
But this function stores a value into record fields depending on
format->type
.
StreamDevice provides two functions to receive a value;
ssize_t streamScanf(dbCommon *record, format_t *format, void* value);
ssize_t streamScanfN(dbCommon *record, format_t *format, void* value, size_t maxStringSize);
The argument value
is a pointer to the variable where the value
is to be stored. Its type must match field->type
(long*
, unsigned long*
, double*
, or
char*
).
The streamScanfN()
function is meant for strings and gets the
additional argument maxStringSize
to specify the size of the
string buffer.
The streamScanf()
function is actually a macro calling
streamScanfN()
with MAX_STRING_SIZE
(=40) for
the last argument. For field->type
values other than
DBF_STRING
, this argument is ignored.
In case of strings, these functions return the number of characters
actually stored (which may be less than maxStringSize
).
Some record types may want to store this value into a field of the record.
The functions return ERROR
on failure. In this case the
readData()
function should return ERROR
as well.
Otherwise the function should store the value received into the appropriate
record field.
If record->pact
is true
, the function
should now return OK
or DO_NOT_CONVERT
(=2),
depending on wheter conversion from .RVAL to .VAL should be left to the
record or not.
If record->pact
is false
, the record is curretly
executing the @init
handler.
This typically only affects output records.
As the record is not processed by EPICS at this time, changes in fields would
not trigger monitor updates.
Also the record will not convert .RVAL to .VAL in this case, thus the
readData()
function should now convert .RVAL
to .VAL as usually done by the record.
In order to make monitors work properly, the readData()
function
should then first call recGblResetAlarms()
and then call
db_post_events()
as needed. Usually the code
from the record support function monitor()
needs to be copied.
Unfortunately the monitor()
function of the record cannot be
called directly because it is static
.
Example:
static long readData(dbCommon *record, format_t *format) { recordtypeRecord *rec = (recordtypeRecord *)record; unsigned long rval; unsigned short monitor_mask; switch (format->type) { case DBF_ULONG: case DBF_LONG: case DBF_ENUM: if (streamScanf(record, format, &rval) == ERROR) return ERROR; rec->rval = rval; if (record->pact) return OK; /* emulate convertion to val */ rec->val = rval * rec->eslo + rec->eoff; break; case DBF_DOUBLE: if (streamScanf(record, format, &rec->val) == ERROR) return ERROR; break; if (record->pact) return DO_NOT_CONVERT; default: return ERROR; } /* In @init handler, no processing, enforce monitor updates. */ monitor_mask = recGblResetAlarms(record); if (rec->oraw != rec->rval) { db_post_events(record, &rec->rval, monitor_mask | DBE_VALUE | DBE_LOG); rec->oraw = rec->rval; } if (!(fabs(rec->mlst - rec->val) <= rec->mdel)) { monitor_mask |= DBE_VALUE; ao->mlst = rec->val; } if (!(fabs(rec->alst - rec->val) <= rec->adel)) { monitor_mask |= DBE_VALUE; ao->alst = rec->val; } if (monitor_mask) db_post_events(record, &rec->val, monitor_mask); return OK; }
The main purpose of this function is to pass the .INP or .OUT link to
StreamDevice for parsing and to make the two functions
readData
and writeData
known.
Often the only thing the initRecord()
function does is to call
streamInitRecord()
and return its result.
long streamInitRecord(dbCommon *record, const struct link *ioLink,
streamIoFunction readData, streamIoFunction writeData);
static long initRecord(dbCommon *record) { recordtypeRecord *rec = (recordtypeRecord *)record; return streamInitRecord(record, &rec->out, readData, writeData); }
For most record types the device support structure contains 5 functions,
report
, init
, init_record
,
get_ioint_info
, and read
or write
.
Few other record typess, for examle ai and ao may have additional functions.
For most of these functions simply pass one of the provided
StreamDevice functions streamReport
,
streamInit
, streamGetIoInitInfo
, and
streamRead
or streamWrite
.
Only for init_record
pass your own
initRecord
function. Then export the structure.
struct { long number; DEVSUPFUN report; DEVSUPFUN init; DEVSUPFUN init_record; DEVSUPFUN get_ioint_info; DEVSUPFUN write; } devrecordtypeStream = { 5, streamReport, streamInit, initRecord, streamGetIointInfo, streamWrite }; epicsExportAddress(dset,devrecordtypeStream);