Integrate into EPICS means "provide an EPICS PV that represents a value in the device", so that if one writes to the EPICS PV, the value gets written to the device; or if one reads from the EPICS PV, one gets a value from the device. (Note that writing to an EPICS PV via channel access can cause the processing that gets the value out to the device, but reading from a PV via channel access will not cause processing to occur. If you want a PV to track some value in the device, you must arrange for that value to be read. For example, you might configure the deviceCmdReply database to process periodically.)
A message based device is one that communicates with its user via sequences of bytes -- typically, ASCII character strings. Message based devices typically communicate via serial, GPIB, or socket (TCP/IP or UDP/IP) interfaces.
deviceCmdReply is essentially a wrapper around the EPICS asyn record. The deviceCmdReply database consists essentially of two sCalcout (string-calc-and-output) records -- one to format the command, one to parse the reply -- and an asyn record, which performs the actual writing and reading. The asyn record provides most of the raw capabilities of deviceCmdReply. Among them are the following:
write to and read from serial, GPIB, or socket interface | This allows deviceCmdReply to control a wide range of devices. |
connect several asyn records to a single port | This allows multiple instances of deviceCmdReply to work together to control
different aspects of a single device. Infrastructure supporting the asyn record
keeps multiple intances of deviceCmdReply from interfering with each other, and
with other asyn-based support talking to the same device.
This capability also permits deviceCmdReply to supplement existing support for a message based device. |
disconnect from one port and connect to another port without interfering with ongoing port traffic | This permits one to load a small number of deviceCmdReply databases, whose eventual use may not even be known at load time, and to target as many of those databases as are needed at a particular device, to support the required set of commands. |
modify port configuration at run time | This allows the user to try, for example, different baud rates and handshaking arrangements, to find one that works. |
show commands and replies as they actually are sent and received | This allows the user quickly and efficiently to debug command formatting, reply parsing, and interface configuration. |
Thus, several instances of deviceCmdReply can be targeted at a single device, to implement different commands, or read different values. For example, a single deviceCmdReply might periodically read the readback temperature from a controller, while another deviceCmdReply is used to write the temperature set point.
streamDevice | Connects standard EPICS record types directly to hardware, using a protocol file to specify the command formatting, reply parsing, etc. See streamDevice 2 |
devXxStrParm | Connects selected record types directly to hardware. Command formatting and reply parsing are specified with in the user-parameter section of output or input links. See devXxStrParm.README in the documentation directory of the synApps ip module. |
SNL | Typically, SNL code monitors PV's and writes to/reads from hardware using an asyn record. |
Other | There are other approaches in use, but I don't know enough about them to describe how they work. |
deviceCmdReply is part of the synApps ip module, and it requires the EPICS asyn module and the synApps calc module to operate. This documentation assumes version 2.7 of the ip module, version 4.6 or higher of asyn, and version 2.6.3 or higher of calc.
The database is loaded into an ioc with the following example command:
dbLoadRecords("$(IP)/ipApp/Db/deviceCmdReply.db", "P=xxx:,N=1,PORT=serial1,ADDR=0,OMAX=40,IMAX=40")
where $(IP)
will be expanded to the value of the environment
variable IP
-- the full path to the ip module. (The EPICS
build will put this in the cdCommands
file if IP
is
defined in the file configure/RELEASE
.)
The following macro arguments target or configure the database to a specific application:
P=xxx:
N=1
PORT=serial1
ADDR=0
ADDR
specifies
which of several devices is to be written to or read from. (The address can be
changed at run time.)
OMAX=40
BOUT
. This matters only if
BOUT
is used, which happens only if the asyn record's
OFMT
field is set to "Binary" or "Hybrid". If OFMT
is
set to "ASCII" (the default), then the AOUT
field is used instead
of BOUT
. AOUT
is an EPICS string, with a fixed size
of 40 bytes.
IMAX=40
BINP
. This matters only if
BINP
is used, which happens only if the asyn record's
IFMT
field is set to "Binary" or "Hybrid". If IFMT
is
set to "ASCII" (the default), then the AINP
field is used instead
of BINP
. AINP
is an EPICS string, with a fixed size of
40 bytes.
This database will contain the following records by which the user programs the device:
record name | record type | function |
---|---|---|
xxx:deviceCmdReplyn_formatCmd | sCalcout | build the string to be written to the device |
xxx:deviceCmdReplyn_do_IO | asyn | send to/receive from hardware |
xxx:deviceCmdReplyn_parseReply | sCalcout | parse reply string |
where xxx: was specified by the P macro, and n was specified by the N macro.
From now on, I'll call these records "..formatCmd", "the asyn record", and "...parseReply", respectively.
file deviceCmdReply.req P=$(P),N=1
where P
and N
are the same as in the above
dbLoadRecords
command, for each database loaded.
This display can be called up from another MEDM display with a "Related Display" button defined with the same macro arguments as in the dbLoadRecords() command above:
Display Label | Display File | Arguments |
---|---|---|
whatever | deviceCmdReply_full.adl | P=xxx:,N=1 |
TMOD
field (labelled "Transfer:" in this display, and
in the asyn record's "asynOctet.adl" display) controls this, and provides the
following options:
Write/Read | Send a command and wait for a reply |
Write | Send a command |
Read | Wait for a reply |
Flush | Not used in deviceCmdReply |
NoI/O | Useful for testing, and for disabling output while the "..formatCmd" record is being configured. |
When TMOD
includes "Write", the first sCalcout record
("...formatCmd") is used to format the string to be sent to the device. It
places the formatted string into the asyn record's AOUT
field, and
causes the asyn record to process.
When TMOD
includes "Read", the second sCalcout record
("...parseReply") is processed by the asyn record after the device read has
completed. It retrieves the string read by the asyn record from the asyn
record's AINP
or BINP
field, depending on the asyn
record's IFMT
field. If IFMT
= ASCII, then
AINP
is used, otherwise BINP
is used.
TMOD
includes "Write", the next job is to program the
"...formatCmd" sCalcout record to craft an output command string from the
information available to it. The information available to an sCalcout record
includes any numeric or string value that has been written to one of its fields,
and the values of any other EPICS PVs to which the sCalcout record can connect
an input link. Note that the asyn record will append a terminator to the
command if the asyn record's OEOS
field (the text field labelled
"TERM" in the "Output" section of the display above) is not empty. The
terminator can be any one or two character string. Common terminators include
"\r" (carriage return) and "\r\n" (carriage return, line feed).
TMOD
includes "Write", the next job is to configure the
asyn record to talk to the port that connects with the device. Click on
the "I/O details" related display button to see a menu of the asyn-record
displays with which this can be done. For example, you might select
"Serial port parameters" to specify the baud rate, etc.
TMOD
includes "Read", the next job is to configure the
asyn record so that it can recognize when the device has finished sending a
reply. Three strategies for recognizing the end of a transmission are supported
by the asyn record:
IEOS
field (the
text field labelled "TERM" in the "Input" section of the display above) to
the terminator the device will use. (This terminator will be stripped from
the string before it is displayed and passed to the "...parseReply" sCalcout
record.)
NRRD
field (the text field labelled "Length
Requested", in the "Input" section of the above display) to the expected number
of characters
TMOT
field (the text field labelled "Timeout:") to the number of seconds after
which the reply is certain to have arrived.
TMOD
includes "Read", the next job is to program the
"...parseCmd" sCalcout record to parse the string returned by the device,
and extract from it the number or string in which you're interested.
PRINTF(format,variable)
$P(format,variable)
In the following examples, the
number to be sent to the device is written to the sCalcout record's
A
field.
desired output | CALC expression | comment |
---|---|---|
M03; | $P("M%02d;",A) | doesn't guarantee 2 digits if A > 99 |
M03; | $P("M%02d;",max(0,min(99,A))) | enforces limit on A
|
S1.234000 | $P("S%f",A) | default precision and field width |
S1.234 | $P("S%.3f",A) | controlled precision |
S 1.234 | $P("S%6.3f",A) | controlled precision and field width |
S01.234 | $P("S%06.3f",A) | leading zero pad, if needed |
A
field, and a value to be sent to that address,
which we'll assume is in the B
field). Note that the command will be
sent when either A
or B
is written to (via
channel access). You can work around this by setting the asyn record's
TMOD
field to "NoI/O" when you don't want the command sent.
desired output | CALC expression | comment |
---|---|---|
M03=4.235 | $P("M%02d=",A)+$P("%.3f",B) | Note $P() takes only two arguments. |
desired output | CALC expression | comment |
---|---|---|
M3=01;M3=74 | $P("M3=%02d",A>>8)+$P("M4=%02d",A&255) | A=330, sent as base 10 numbers |
M3=01;M3=4A | $P("M3=%02X",A>>8)+$P("M4=%02X",A&255) | A=330, sent as hex numbers |
AA
field, the "1" string in the
BB
, etc., and treating the string fields as an array, using the
stringCalc operator "@@" to index the array:
desired output | CALC expression | comment |
---|---|---|
S0 OPEN | $P("S0 %s",@@A) | A addresses the string array: AA="CLOSE";BB="OPEN" |
desired output | CALC expression | comment |
---|---|---|
\ | "\\" | small price to pay for the ability to send unprintable characters |
Some devices want to see numbers in their raw, binary form. Prior to EPICS version 3.14, there was no widely supported way to pass strings that might contain embedded ASCII NULL characters from one record to another, so deviceCmdReply would not have been useable for this class of devices.
But EPICS 3.14 provides an escape-translation service for strings containing
unprintable characters, to put their content into a form that can be transported
in a normal EPICS string. This allows us to load such strings into a database,
send them via channel access, autosave them, etc. For purposes here, the
service is implemented by the pair of functions
dbTranslateEscape()
, which produces raw binary from a string
containing escape sequences, and epicsStrSnPrintEscaped()
, which
does the opposite. (See the EPICS Application Developer's Guide for more
information.)
In the following tables, a one-byte binary value will be represented by <n>
desired output | CALC expression | comment |
---|---|---|
<2>#<254> | "\x02#\xfe" | using hex escape sequences |
<2>#<254> | "\002#\376" | using octal escape sequences |
To embed variable binary numbers into an output string, you can use the
sCalcout record's WRITE(format,variable)
$W(format,variable)
The format-indicator characters used withWRITE()
are intended to be familiar from experience you may have had with the standard C library'sprintf()
function, but they're used here to specify how binary numbers will be encoded, so any field-width or precision specifications will be ignored.
desired output | CALC expression | comment |
---|---|---|
<2> | encode the value of the sCalcout record's A field as a one-byte integer | |
<2>#<254> | encode A and B as two-byte integers | |
<2> | encode A as a four-byte integer | |
<2.1> | encode A as a four-byte float | |
<2.1> | encode A as an eight-byte float |
desired output | CALC expression | comment |
---|---|---|
append XOR8 checksum to string | ||
append modbus/RTU CRC to string |
AINP
field, and put it in the sCalcout record's AA
field. Our job here,
then, is simply to parse the content of the AA
field. Parsing the
reply from a device generally requires two operations: we have to specify the
part of the reply that contains the information of interest; and we have to
convert that information to the desired form.
INT(string)
string
for the first thing that looks like an integer number, and
returns the value of that number. Similarly, the function
DBL(string)
string
for the first
thing that looks like a floating point number, and returns its value.
reply string | CALC expression | comment |
---|---|---|
VALUE=12 | INT(AA) | convert to integer |
VALUE=1.23 | DBL(AA) | convert to double-precision number |
reply string | CALC expression | comment |
---|---|---|
VALUE1=1.23 | INT(AA[7,-1]) | move past "VALUE1=" and convert to float |
VALUE1=1.23 | $S(AA,"%*7c%f") | skip 7 characters and convert to float |
See the sCalcout record documentation for more information on the substring operator "[]
". For purposes here, the syntax is[<start>,<end>]
. If<start>
is a number, it indicates the number of bytes to skip.<end>
will always be-1 in this documentation.
reply string | CALC expression | comment |
---|---|---|
REG237=1.23 | DBL(AA["=",-1]) | move past "=" and convert to double |
reg:2 1.23 | $S(AA,"%*s %f) | move past |
move past two "=" characters, find an integer |
In examples above, we've used the substring operator "[<start>,<end>]
" with a string-valued first argument -- a pattern string -- and the usual second argument of -1. If the pattern is found, the result of the operation is the substring beginning just after the pattern, and continuing to the end of string. (If the pattern occurs more than once, only the first instance counts.)
Parsing unprintable input poses a different sort of problem than parsing printable input. The strategy is still pretty simple: move to the interesting stuff and convert it. But moving to the interesting stuff might be complicated by escape sequences in the preceding bytes. I'll first discuss moving as a separate problem, and then worry about converting it.
[]
operator, as in the previous section. If the
fixed bytes include escape sequences, we just treat them as plain text for the
purpose of counting characters, or of recognizing patterns. In the following
examples, <2> represents an unprintable byte whose binary value is 2:
actual reply string | expression | comment | |
---|---|---|---|
<2><target> | \002<target> | AA[4,-1] | skip one actual byte, encoded as a four-byte escape sequence |
<2>abc<3><target> | \002abc\003<target> | skip five actual bytes, encoded as an 11-byte escape sequence...OR... | |
<2>abc<3><target> | \002abc\003<target> | just find the "\003" escape sequence | |
Thus, in this case we can't count bytes in the escaped string to find the
interesting stuff, and an expression like
AA[<number>,-1]
"AA[<string>,-1]
actual reply string | expression | comment | |
---|---|---|---|
<?>abc<3><target> | find the "abc\003" escape sequence | ||
READ(string,format)
actual reply string | expression | comment | |
---|---|---|---|
<n><target> | skip N bytes in raw string | ||
So now we have the tools to do some actual conversions. We'll use the
READ(string, format)
$R(...)
actual reply string | expression | comment | |
---|---|---|---|
<2><6> | \002\006 | READ(AA[4,-1],"%c") | skip four-byte escape sequence, read 8-bit integer |
<2><7><2> | \002\a\002 | READ(AA[4,-1],"%hd") | skip four-byte escape sequence, read 16-bit integer |
<2><5><1><3><4> | \002\005\001\003\004 | READ(AA[4,-1],"%d") | skip four-byte escape sequence, read 32-bit integer |
<2>abc<3><10> | \002abc\003\n | skip 11-byte escape sequence, read 8-bit integer | |
<2>abc<3><101><5> | \002abc\003A\005 | find "\003", read 16-bit integer | |
<?>abc<3><5> | find "abc\003", read 8-bit integer | ||
<3 bytes><4> | skip 3 bytes in raw string, read 8-bit integer |