MIDAS - EPICS - Channel Access
A MIDAS bus driver for EPICS channel access: m(idas)EpicsC(hannel)A(ccess).
Written in C++.
Requirements
- Environment variable
MIDASSYSwhich is a path to the MIDAS shared libraries and headers. The libraries must be in$MIDASSYS/lib, the headers in$MIDASSYS/include. - Environment variable
EPICSSYSwhich is a path to the EPICS shared libraries and headers. The libraries must be in$EPICSSYS/lib, the headers in$EPICSSYS/include.
Build
This is a header-only library which exposes the CMake target m_epics_ca. To
use the header in your driver, simply include it within your own CMakeLists.txt
via target_include_directories. The header can then be included in your own
code: #include "m_epics_ca.h".
Example
This example shows how to interact with an EPICS record. It assumes that an IOC is running and reachable and that the following record are part of the IOC database:
record(longin, "EXAMPLE:LONGIN") {
field(VAL, 42)
}
record(ao, "EXAMPLE:AO") {
field(VAL, 1.0)
field(DRVH, 13)
field(DRVL, 0)
}
This code connects to the VAL field of "EXAMPLE:LONGIN" and to DRVH of
EXAMPLE:AO, reads them and changes them. It also shows a type mismatch between
record and class value type
#include "cadef.h"
#include "m_epics_ca.h
#define EQUAL(left, right) \
do { \
if (!(left == right)) { \
std::cerr << "\nTest failed at line " << __LINE__ << ": '" \
<< (left) << "' (" << #left << ") != '" << (right) \
<< "' (" << #right << *")" << std::endl; \
return 1; \
} \
} while (0)
int main() {
/*
Type safety: Type of mEpicsCa must match that of the record. This is checked
when connecting to the EPICS channel. If there is a mismatch, an error will
be logged to MIDAS and any attempt to interact with the record will return
an error code.
*/
// int matches longin record
auto val_ca = mEpicsCa<int>("EXAMPLE:LONGIN");
// double matches ao record
auto drvh_ca = mEpicsCa<double>("EXAMPLE:AO.DRVH");
// Type mismatch
auto mismatch_ca = mEpicsCa<int>("EXAMPLE:AO.DRVL");
// Generic status variable which is reused for the error codes throughout
// the tests.
int status = CM_SUCCESS;
// Reading a value from a channel and checking the (MIDAS-native) error code
int val = 0;
status = val_ca.get(&val);
EQUAL(status, CM_SUCCESS);
EQUAL(val, 42);
double drvh = 0;
status = drvh_ca.get(&drvh);
EQUAL(status, CM_SUCCESS);
EQUAL(drvh, 13);
status = mismatch_ca.get(&val);
EQUAL(status, FE_ERR_DRIVER); // Type mismatch -> No interaction with record possible
EQUAL(val, 42); // Variable is left unchanged
// Writing to a channel
val = 21;
status = int_ca.put(&val);
EQUAL(status, CM_SUCCESS);
// Checking last reported status and severity of the record
EQUAL(int_ca.status(), menuAlarmStatNO_ALARM);
EQUAL(int_ca.severity(), menuAlarmSevrNO_ALARM);
return 0;
}
For more examples, see test/m_epics_ca_test.cxx.
Features
MIDAS integration
While mEpicsCa is mostly a generic EPICS driver, it provides the following
features for seamless usage in MIDAS drivers and frontends:
- Error messages are logged using
cm_msg. - Return codes are MIDAS-native (EPICS return codes are mapped to MIDAS)
Type safety
As shown in the example, it is necessary to specify the value type when
instantiating mEpicsCa. At compile time, it is checked if the given type is
a valid record type. For example, there is no record equivalent for
std::deque, so trying to instantiate mEpicsCa<std::deque> will result in a
compile error.
When connecting to a channel, the channel type is read out and compared to the
value type. If those do not match, then the class will switch into an error
state and just return FE_ERR_DRIVER when trying to get or put values.
For some record types, there are multiple valid value types. One example is the
binary out (bo) record, which is an enum record and therefore can accept
either strings or integers. Hence, trying to read a string from the VAL field
of a bo record using mEpicsCa<uint16_t> (uint16_t is the bo channel type)
will work.
Subscription vs manual polling
For some fields of some records, EPICS offers a "subscription" mechanism where
a callback is triggered every time the channel value changes. This can be used
to "cache" the current value inside mEpicsCa so that get does not actually
communicate with the record but instead returns the value received from the
last callback (provided that the channel is not disconnected or in an error
state).
Since the subscription mechanism does not work for all channels, it is opt-in
with a second boolean argument: mEpicsCa<int>("EXAMPLE:LONGIN", true).
Unfortunately, there is no way to detect whether subscription works or not using the EPICS C-API, so verify that it does!
Handling of record STAT and SEVR fields
When accessing the record with get (or receiving a callback event if
subscription is used), the values of the record's STAT and SEVR fields are
returned as well. They are then stored in the class and cann be accessed with
mEpicsCa::status and mEpicsCa::severity. If the severity is higher than 0
(== menuAlarmSevrNO_ALARM), then the pointer provided to get is left
unchanged and FE_ERR_DRIVER is returned. Additionally, an error message is
logged with cm_msg.
Synchronous and asynchronous mode
This driver supports asynchronous write operations via mEpicsCa::put<false>.
When writing asynchronously, multiple write commands can be send in batches:
auto val_ca = mEpicsCa<double>("EXAMPLE:AO");
auto drvh_ca = mEpicsCa<double>("EXAMPLE:AO.DRVH");
auto drvl_ca = mEpicsCa<double>("EXAMPLE:AO.DRVL");
drvh_ca.put<false>(&10.0);
drvl_ca.put<false>(&0.0);
val_ca.put<false>(&5.0);
val_ca.flushIo();
All three write commands get send at once with mEpicsCa::flushIo>. This helps
to minimize network traffic when writing multiple values at once.
Testing
This driver comes with its own test suite which can be run via CMake:
mkdir build
cmake -B build
cmake --build build
ctest --test-dir build
The source code for the test binary is in test/m_epics_ca_test.cxx. This file
can also serve as a collection of examples for different record types, like
longin, ao, bo, stringin, waveform, ... .
CI
In CI, this test is run in the Docker container lin/midas_base_gitea. To
manually run tests in the CI environment, log into the runner machine and use
sudo docker run --rm -it -v "$PWD":/workspace -w /workspace lin/midas_base_gitea bash
Please note that EPICS_HOST_ARCH is linux-x86_64 within that container!
Therefore, that environment variable must not be set explicitly within the test.