diff --git a/.gitignore b/.gitignore index 0f958cd..a97cb7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .vscode build -.ioc \ No newline at end of file +.ioc* \ No newline at end of file diff --git a/bus/m_epics_ca.h b/bus/m_epics_ca.h index cd2693b..9cc5f8e 100644 --- a/bus/m_epics_ca.h +++ b/bus/m_epics_ca.h @@ -1,6 +1,21 @@ +#ifndef mEpicsCa_H +#define mEpicsCa_H + #include "cadef.h" +#include #include +/** + * @brief Information about the channel type (integer, double, string, ...) + */ +struct ChInfo { + // Channel type (like e.g. DBF_INT, DBF_DOUBLE, DBF_STRING etc.) + short type; + // Number of elements. Is 1 for most record types except for waveforms, + // whose length is defined in the record. + int count; +}; + template class mEpicsCa { public: /** @@ -12,25 +27,25 @@ template class mEpicsCa { * @param priority * @param pChanID */ - mEpicsCa(const char *pChanName, caCh *pConnStateCallback, + mEpicsCa(const char *pChanName, bool subscribe, caCh *pConnStateCallback, void *pUserPrivate, double timeout = 2.0); - mEpicsCa(const char *pChanName); + mEpicsCa(const char *pChanName, bool subscribe); ~mEpicsCa(); - template int put(V *value, double timeout); + template int get(V *value); + int get(char *buf, u_long len); template int put(V *value); - - template int get(V *value); - - template int get(V *value, double timeout); + int put(const char *buf, u_long len); bool connected(); double _timeout; + const std::optional &cached() const; + private: /** * @brief A callback for reacting to channel access channel state changes. @@ -44,8 +59,29 @@ template class mEpicsCa { */ static void connStateCallback(struct connection_handler_args args); + static void eventCallback(struct event_handler_args args); + + int getRaw(char *buf, size_t len); + int getRaw(int *value); + int getRaw(double *value); + int getRaw(uint16_t *value); + + int putRaw(const char *buf, size_t len); + int putRaw(int *value); + int putRaw(double *value); + int putRaw(uint16_t *value); + + std::optional _channelInfo; chid _pChanID; std::string _chanName; + + /* + In case the subscription mechanism is used, this variable holds the last + value returned by the callback. + */ + std::optional _cached; }; -#include "m_epics_ca.tpp" \ No newline at end of file +#include "m_epics_ca.tpp" + +#endif \ No newline at end of file diff --git a/bus/m_epics_ca.tpp b/bus/m_epics_ca.tpp index 1445d96..37e9c38 100644 --- a/bus/m_epics_ca.tpp +++ b/bus/m_epics_ca.tpp @@ -1,6 +1,7 @@ #include "cadef.h" #include "dbDefs.h" #include "midas.h" +#include #include #include @@ -9,38 +10,33 @@ // Helper struct for dbfFromType template struct alwaysFalse : std::false_type {}; -/** - * @brief Check at compile time if T is a valid database field (DBF) type. - * - * This function will fail to compile if T is not a valid database field type. - * - * @tparam T - * @return int - */ -template constexpr int dbfFromType() { - if constexpr (std::is_same_v || - std::is_same_v || - std::is_same_v) { - return DBF_STRING; - } else if constexpr (std::is_same_v) { - return DBF_INT; // Same as DBF_SHORT - } else if constexpr (std::is_same_v) { - return DBF_FLOAT; - } else if constexpr (std::is_enum_v) { - return DBF_ENUM; - } else if constexpr (std::is_same_v) { - return DBF_CHAR; - } else if constexpr (std::is_same_v) { - return DBF_LONG; - } else if constexpr (std::is_same_v) { - return DBF_DOUBLE; - } else { - static_assert(alwaysFalse::value, - "Unsupported EPICS database field type. See " - "epics-base/src/ca/client/db_access.h for the list " - "of allowed types"); - } -} +template struct dbf_type; + +template <> struct dbf_type { + static constexpr int value = DBF_INT; +}; + +template <> struct dbf_type { + static constexpr int value = DBF_DOUBLE; +}; + +template <> struct dbf_type { + static constexpr int value = DBF_STRING; +}; + +template <> struct dbf_type { + static constexpr int value = DBF_STRING; +}; + +template <> struct dbf_type { + static constexpr int value = DBF_STRING; +}; + +template <> struct dbf_type { + static constexpr int value = DBF_ENUM; +}; + +template constexpr int dbfFromType() { return dbf_type::value; } template constexpr void assertEqual() { static_assert(dbfFromType() == dbfFromType()); @@ -48,93 +44,111 @@ template constexpr void assertEqual() { /** * @brief Handle the ECA (error channel access) code returned by the ca_get and - * ca_put macros. + * ca_put macros and convert it to a MIDAS errror code. * * Handling means creating an appropriate MIDAS error message to inform the * user. * - * @param status + * @param status EPRICS status code * @param pChanName: Name of the EPICS channel + * @return int Corresponding MIDAS status code */ -void handleEcaCode(int status, const char *pChanName) { +int convertAndHandleEcaCode(int status, const char *pChanName) { switch (status) { case ECA_NORMAL: - break; + return CM_SUCCESS; case ECA_BADCHID: cm_msg(MERROR, __FILE__, "PV '%s': Channel ID has been corrupted. Restart the IOC.", pChanName); - break; + return FE_ERR_DISABLED; case ECA_BADTYPE: cm_msg(MERROR, __FILE__, "PV '%s': Type mismatch. This is a driver bug, please call the " "support.", pChanName); - break; + return FE_ERR_DRIVER; case ECA_BADCOUNT: cm_msg( MERROR, __FILE__, "PV '%s': Invalid element count requested. This is a driver bug, " "please call the support.", pChanName); - break; + return FE_ERR_DRIVER; case ECA_STRTOBIG: cm_msg(MERROR, __FILE__, "PV '%s': String length exceeds allowed maximum length. Try " "providing a shorter string or call the support.", pChanName); - break; + return FE_ERR_DRIVER; case ECA_NOWTACCESS: cm_msg(MERROR, __FILE__, "PV '%s': Write access denied. Please call the support.", pChanName); - break; + return FE_ERR_DRIVER; case ECA_ALLOCMEM: cm_msg(MERROR, __FILE__, "PV '%s': Unable to allocate memory. Please call the support.", pChanName); - break; + return FE_ERR_DRIVER; case ECA_DISCONN: cm_msg(MERROR, __FILE__, "PV '%s': Channel disconnected. Check if the PV name is correct " "and if the IOC is running.", pChanName); - break; + return FE_ERR_DRIVER; case ECA_TIMEOUT: cm_msg( MERROR, __FILE__, "PV '%s': Timed out while trying to read. Check if the PV name is " "correct and if the IOC is running.", pChanName); - break; + return CM_TIMEOUT; default: cm_msg(MERROR, __FILE__, "PV '%s': Received unknown error code %d.", pChanName, status); - break; + return FE_ERR_DRIVER; } } template -mEpicsCa::mEpicsCa(const char *pChanName, caCh *pConnStateCallback, - void *pUserPrivate, double timeout) - : _timeout(timeout), _pChanID(nullptr), _chanName(pChanName) { +mEpicsCa::mEpicsCa(const char *pChanName, bool subscribe, + caCh *pConnStateCallback, void *pUserPrivate, + double timeout) + : _timeout(timeout), _pChanID(nullptr), _chanName(pChanName), + _cached(std::nullopt) { // Compile time check of T dbfFromType(); - int status = CA_M_SUCCESS; + int status = CM_SUCCESS; + + /* + ca_disable_preemptive_callback: No callback thread is created and CA context + has to be "polled" (e.g. via ca_pend_event or ca_pend_io). All callbacks + happen in the main thread. + ca_enable_preemptive_callback: EPICS creates a dedicated callback thread + where the callback is run. + + Obviously, ca_enable_preemptive_callback is the variant we want for + subscription. + */ + auto choice = ca_preemptive_callback_select::ca_disable_preemptive_callback; + if (subscribe) { + choice = ca_preemptive_callback_select::ca_enable_preemptive_callback; + } /* Try to create the Channel access context explicitly: https://docs.epics-controls.org/projects/base/en/latest/cadef_h.html#_CPPv417ca_context_create29ca_preemptive_callback_select This function returns ECA_NOTTHREADED if a context has already been created for the current thread. This is generally not an issue and can - safely be ignored. All other failures are handled in handleEcaCode. - Preemptive callbacks are disabled by default for this driver. + safely be ignored. All other failures are handled in + convertAndHandleEcaCode. Preemptive callbacks are disabled by default for + this driver. */ - status = ca_context_create( - ca_preemptive_callback_select::ca_disable_preemptive_callback); - if (status != ECA_NORMAL && status != ECA_NOTTHREADED) { - handleEcaCode(status, this->_chanName.c_str()); + status = ca_context_create(choice); + if (status != ECA_NOTTHREADED) { + status = convertAndHandleEcaCode(status, this->_chanName.c_str()); } // Use the user-provided callback, if it is not NULL, otherwise use the @@ -146,72 +160,332 @@ mEpicsCa::mEpicsCa(const char *pChanName, caCh *pConnStateCallback, // 0 is the lowest network priority, 99 is the highest. int priority = 0; - status = ca_create_channel(pChanName, pConnStateCallback, pUserPrivate, - priority, &this->_pChanID); - handleEcaCode(status, this->_chanName.c_str()); + status = convertAndHandleEcaCode( + ca_create_channel(pChanName, pConnStateCallback, pUserPrivate, priority, + &this->_pChanID), + this->_chanName.c_str()); // Store a pointer to "this" in the channel for retrieval in the // connStateCallback function. ca_set_puser(this->_pChanID, this); + if (subscribe) { + ca_create_subscription(dbf_type::value, 1, this->_pChanID, DBE_VALUE, + &mEpicsCa::eventCallback, this, nullptr); + } + SEVCHK(status, "ca_create_channel"); } template -mEpicsCa::mEpicsCa(const char *pChanName) - : mEpicsCa(pChanName, nullptr, nullptr) {} +void mEpicsCa::eventCallback(struct event_handler_args args) { + auto *self = static_cast *>(ca_puser(args.chid)); + + if (!self) + return; + + // Connection events are handled in connStateCallback, which also + // invalidates the cache when disconnecting. + if (args.status != ECA_NORMAL) + return; + + if constexpr (std::is_same_v) { + // Special handling of string type: args.dbr will be a char array, so + // we need to cast to a char array first which is then converted into + // a string. + self->_cached = static_cast(args.dbr); + } else { + self->_cached = *static_cast(args.dbr); + } +} + +template const std::optional &mEpicsCa::cached() const { + return _cached; +} + +template +mEpicsCa::mEpicsCa(const char *pChanName, bool subscribe) + : mEpicsCa(pChanName, subscribe, nullptr, nullptr) {} template mEpicsCa::~mEpicsCa() { // Free up the channel resources - ca_clear_channel(this->_pChanID); -} - -template template int mEpicsCa::put(V *value) { - return put(value, _timeout); -} - -template -template -int mEpicsCa::put(V *value, double timeout) { - assertEqual(); - int dbfType = dbfFromType(); - - int status = ca_put(dbfType, this->_pChanID, value); - handleEcaCode(status, this->_chanName.c_str()); - - if (status != ECA_NORMAL) - return status; - - // caget is asynchronous, so the pointer value is only updated when - // ca_pend_io returns ECA_NORMAL - status = ca_pend_io(timeout); - - handleEcaCode(status, this->_chanName.c_str()); - return status; + convertAndHandleEcaCode(ca_clear_channel(this->_pChanID), + this->_chanName.c_str()); } template template int mEpicsCa::get(V *value) { - return get(value, _timeout); + + // If "this" has the type mEpicsCa, then V needs to be a string + // or an uint16_t. + if constexpr (dbfFromType() == DBF_ENUM) { + constexpr auto t = dbfFromType(); + constexpr auto v = dbfFromType(); + static_assert((t == v) || (t == DBF_ENUM && v == DBF_STRING) || + (t == DBF_STRING && v == DBF_ENUM), + "Incompatible EPICS types"); + } else { + assertEqual(); + } + + if (!connected()) { + cm_msg(MERROR, __FILE__, + "PV '%s': Channel disconnected. Check if the PV name is correct " + "and if the IOC is running.", + _chanName.c_str()); + return FE_ERR_DRIVER; + } + + // If a subscription mechanism is active, use the last cached value instead + // of explictly invoking ca_get + if (_cached.has_value()) { + *value = _cached.value(); + return CM_SUCCESS; + } + + if constexpr (std::is_same_v) { + char buf[40] = {0}; + + int status = getRaw(buf, sizeof(buf)); + if (status == CM_SUCCESS) + *value = buf; + return status; + } else { + return getRaw(value); + } } -template -template -int mEpicsCa::get(V *value, double timeout) { - assertEqual(); - int dbfType = dbfFromType(); +template int mEpicsCa::get(char *buf, u_long len) { - int status = ca_get(dbfType, this->_pChanID, value); - handleEcaCode(status, this->_chanName.c_str()); + // If "this" has the type mEpicsCa, then the type check is omitted + if (dbfFromType() != DBF_ENUM) { + assertEqual(); + } - if (status != ECA_NORMAL) + if (!connected()) { + cm_msg(MERROR, __FILE__, + "PV '%s': Channel disconnected. Check if the PV name is correct " + "and if the IOC is running.", + _chanName.c_str()); + return FE_ERR_DRIVER; + } + + // If a subscription mechanism is active, use the last cached value instead + // of explictly invoking ca_get + if (_cached.has_value()) { + if constexpr (std::is_same_v) { + strncpy(buf, _cached.value().c_str(), len); + } else { + strncpy(buf, _cached.value(), len); + } + return CM_SUCCESS; + } + + return getRaw(buf, len); +} + +template int mEpicsCa::getRaw(int *value) { + int status = convertAndHandleEcaCode( + ca_get(dbfFromType(), this->_pChanID, value), + this->_chanName.c_str()); + + if (status != CM_SUCCESS) return status; // caget is asynchronous, so the pointer value is only updated when // ca_pend_io returns ECA_NORMAL - status = ca_pend_io(timeout); + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); +} - handleEcaCode(status, this->_chanName.c_str()); - return status; +template int mEpicsCa::getRaw(double *value) { + int status = convertAndHandleEcaCode( + ca_get(dbfFromType(), this->_pChanID, value), + this->_chanName.c_str()); + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); +} + +template int mEpicsCa::getRaw(uint16_t *value) { + int status = convertAndHandleEcaCode( + ca_get(dbfFromType(), this->_pChanID, value), + this->_chanName.c_str()); + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); +} + +template int mEpicsCa::getRaw(char *buf, u_long len) { + int status = CM_SUCCESS; + if (_channelInfo.value().count == 1) { + // String record + status = convertAndHandleEcaCode( + ca_get(dbfFromType(), this->_pChanID, buf), + this->_chanName.c_str()); + } else { + // Waveform record + status = convertAndHandleEcaCode( + ca_array_get(DBR_CHAR, len, this->_pChanID, buf), + this->_chanName.c_str()); + } + + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); +} + +// ============================================================================= +// put +// ============================================================================= + +template template int mEpicsCa::put(V *value) { + + // If "this" has the type mEpicsCa, then V needs to be a string or + // an integer + if constexpr (dbfFromType() == DBF_ENUM) { + constexpr auto t = dbfFromType(); + constexpr auto v = dbfFromType(); + static_assert((t == v) || (t == DBF_ENUM && v == DBF_STRING) || + (t == DBF_STRING && v == DBF_ENUM), + "Incompatible EPICS types"); + } else { + assertEqual(); + } + + if (!connected()) { + cm_msg(MERROR, __FILE__, + "PV '%s': Channel disconnected. Check if the PV name is correct " + "and if the IOC is running.", + _chanName.c_str()); + return FE_ERR_DRIVER; + } + + if constexpr (std::is_same_v) { + // Use the c-style string buffer put algorithm + return putRaw(value->c_str(), value->size()); + } else { + return putRaw(value); + } +} + +template int mEpicsCa::put(const char *buf, u_long len) { + + // If "this" has the type mEpicsCa, then the type check is omitted + if constexpr (dbfFromType() != DBF_ENUM) { + assertEqual(); + } + + if (!connected()) { + cm_msg(MERROR, __FILE__, + "PV '%s': Channel disconnected. Check if the PV name is correct " + "and if the IOC is running.", + _chanName.c_str()); + return FE_ERR_DRIVER; + } + return putRaw(buf, len); +} + +template int mEpicsCa::putRaw(double *value) { + int status = convertAndHandleEcaCode( + ca_put(dbfFromType(), this->_pChanID, value), + this->_chanName.c_str()); + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); +} + +template int mEpicsCa::putRaw(u_int16_t *value) { + int status = convertAndHandleEcaCode( + ca_put(dbfFromType(), this->_pChanID, value), + this->_chanName.c_str()); + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); +} + +template int mEpicsCa::putRaw(const char *buf, u_long len) { + + if constexpr (dbfFromType() == DBF_ENUM) { + // Check if the given string corresponds to one of the enum variants and + // fetch the corresponding index, if it does. Otherwise, create a nice + // error message. + struct dbr_ctrl_enum data; + ca_get(DBR_CTRL_ENUM, this->_pChanID, &data); + ca_pend_io(_timeout); + + std::string variants; + + for (int i = 0; i < data.no_str; ++i) { + if (i > 0) + variants += ", "; + + variants += data.strs[i]; + + if (strcmp(data.strs[i], buf) == 0) + return putRaw(&i); + } + + // Given string did not match any of the variants -> return an error + cm_msg(MERROR, __FILE__, + "PV '%s': Given string '%s' does not match one of the enum " + "variants [%s]", + this->_chanName.c_str(), buf, variants.c_str()); + return FE_ERR_DRIVER; + } else { + int status = CM_SUCCESS; + if (_channelInfo.value().count == 1) { + // String record + status = convertAndHandleEcaCode( + ca_put(dbfFromType(), this->_pChanID, buf), + this->_chanName.c_str()); + } else { + // Waveform record + status = convertAndHandleEcaCode( + ca_array_put(DBR_CHAR, len, this->_pChanID, buf), + this->_chanName.c_str()); + } + + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); + } +} + +template int mEpicsCa::putRaw(int *value) { + int status = convertAndHandleEcaCode( + ca_put(dbfFromType(), this->_pChanID, value), + this->_chanName.c_str()); + if (status != CM_SUCCESS) + return status; + + // caget is asynchronous, so the pointer value is only updated when + // ca_pend_io returns ECA_NORMAL + return convertAndHandleEcaCode(ca_pend_io(_timeout), + this->_chanName.c_str()); } template @@ -219,12 +493,18 @@ void mEpicsCa::connStateCallback(struct connection_handler_args args) { mEpicsCa *self = static_cast(ca_puser(args.chid)); if (args.op == CA_OP_CONN_UP) { + struct ChInfo info; + info.type = ca_field_type(self->_pChanID); + info.count = ca_element_count(self->_pChanID); + self->_channelInfo = std::optional(info); cm_msg(MDEBUG, __FILE__, "PV %s connected", self->_chanName.c_str()); } else { + self->_channelInfo.reset(); + self->_cached.reset(); cm_msg(MDEBUG, __FILE__, "PV %s disconnected", self->_chanName.c_str()); } } template bool mEpicsCa::connected() { - return _pChanID && ca_state(_pChanID) == cs_conn; + return _channelInfo.has_value(); } \ No newline at end of file diff --git a/test/ioc/record.db b/test/ioc/record.db index 94270b4..11f6ff3 100644 --- a/test/ioc/record.db +++ b/test/ioc/record.db @@ -1,10 +1,25 @@ -record(longin, "TEST:INT") { - field(DESC, "Example record") +record(longin, "MEPICSCA:TEST:LONGIN") { field(VAL, 42) + field(HOPR, 100) + field(LOPR, 0) field(PINI, "YES") } -record(ai, "TEST:DOUBLE") { - field(DESC, "Example record") +record(ai, "MEPICSCA:TEST:AI") { field(VAL, 84) +} + +record(stringin, "MEPICSCA:TEST:STRINGIN") { + field(VAL, "MyTestString") +} + +record(waveform, "MEPICSCA:TEST:WAVEFORM") { + field(FTVL, "CHAR") + field(NELM, "200") +} + +record(bo, "MEPICSCA:TEST:BO") { + field(VAL, 1) + field(ZNAM, "OFF") + field(ONAM, "ON") } \ No newline at end of file diff --git a/test/ioc/st.cmd b/test/ioc/st.cmd index 0374674..87c061f 100755 --- a/test/ioc/st.cmd +++ b/test/ioc/st.cmd @@ -3,3 +3,6 @@ dbLoadRecords("$(IOCDIR)/record.db") iocInit() + +# Initialize the waveform record +dbpf("MEPICSCA:TEST:WAVEFORM", "ABC") diff --git a/test/ioc/startioc b/test/ioc/startioc index 4b30f4a..7083d34 100755 --- a/test/ioc/startioc +++ b/test/ioc/startioc @@ -7,4 +7,5 @@ export EPICS_CA_ADDR_LIST=127.0.0.1 cd "$(dirname "$0")" export IOCDIR=$(pwd) -./st.cmd + +exec ./st.cmd diff --git a/test/m_epics_ca_test.cxx b/test/m_epics_ca_test.cxx index 797fdbc..7641563 100644 --- a/test/m_epics_ca_test.cxx +++ b/test/m_epics_ca_test.cxx @@ -2,7 +2,10 @@ #include "m_epics_ca.h" #include #include +#include +#include #include +#include #include #include #include @@ -10,7 +13,7 @@ #include #include -#define CHECK(cond) \ +#define ASSERT(cond) \ do { \ if (!(cond)) { \ std::cerr << "Test failed: " #cond << std::endl; \ @@ -21,9 +24,9 @@ #define EQUAL(left, right) \ do { \ if (!(left == right)) { \ - std::cerr << "\nTest failed: " << (left) << " (" << #left \ - << ") != " << (right) << " (" << #right << *")" \ - << std::endl; \ + std::cerr << "\nTest failed at line " << __LINE__ << ": " \ + << (left) << " (" << #left << ") != " << (right) << " (" \ + << #right << *")" << std::endl; \ return 1; \ } \ } while (0) @@ -31,12 +34,24 @@ class IocProcess { public: explicit IocProcess(const std::string &path) { + /* + Important note: DO NOT USE setpgid HERE! + Even though it looks "cleaner" than the pkill solution in the destructor + below (because you could just terminate the group), this seems to cause + trouble deep within EPICS, causing the server not to be reachable + anymore with caget. + */ _pid = fork(); if (_pid == 0) { + + // Hide IOC output + int devnull = open("/dev/null", O_WRONLY); + dup2(devnull, STDOUT_FILENO); + close(devnull); + // Start the IOC from the child process - setpgid(0, 0); - execl(path.c_str(), path.c_str(), (char *)nullptr); + execl(path.c_str(), path.c_str(), (char *)NULL); perror("execl failed"); _exit(1); } @@ -44,17 +59,13 @@ class IocProcess { if (_pid < 0) { throw std::runtime_error("fork failed"); } - // Set the ID of the entire process group so it can be torn down in the - // destructor later. This ensures IOC teardown. - setpgid(_pid, _pid); } ~IocProcess() { if (_pid <= 0) return; - // Kill the entire process group due to the -1 - kill(-_pid, SIGTERM); + kill(_pid, SIGTERM); // Give the IOC 50 * 100 * 1000 = 5 seconds time for graceful // termination. @@ -66,25 +77,78 @@ class IocProcess { usleep(100 * 1000); } - // Force kill group if needed - kill(-_pid, SIGKILL); - waitpid(_pid, nullptr, 0); + kill(_pid, SIGKILL); } private: pid_t _pid = -1; }; +void handler(int sig) { printf("Received signal: %d\n", sig); } + int main() { + using clock = std::chrono::steady_clock; + + signal(SIGPIPE, handler); + signal(SIGTERM, handler); + signal(SIGINT, handler); + signal(SIGHUP, handler); // Client setting for local testing setenv("EPICS_CA_AUTO_ADDR_LIST", "NO", 1); - setenv("EPICS_CA_ADDR_LIST", "127.0.0.255", 1); + setenv("EPICS_CA_ADDR_LIST", "127.0.0.1", 1); + + // Create a new instance of the mEpicsCa bus driver and use it to + // interact with the spawned IOC. + auto int_ca = mEpicsCa("MEPICSCA:TEST:LONGIN", false); + auto int_hopr_ca = mEpicsCa("MEPICSCA:TEST:LONGIN.HOPR", false); + auto double_ca = mEpicsCa("MEPICSCA:TEST:AI", false); + auto string_ca = mEpicsCa("MEPICSCA:TEST:STRINGIN", false); + auto waveform_ca = mEpicsCa("MEPICSCA:TEST:WAVEFORM", false); + auto bo_ca = mEpicsCa("MEPICSCA:TEST:BO", false); + + // Ending s indicates subscription mechanism is active + auto int_cas = mEpicsCa("MEPICSCA:TEST:LONGIN", true); + + // Generic status variable which is reused for the error codes throughout + // the tests. + int status = CM_SUCCESS; + + { + // ===================================================================== + // Tests before IOC is running + // ===================================================================== + + // Drivers are not connected + ASSERT((!int_ca.connected())); + + // Cached values are invalid + ASSERT((!int_cas.cached().has_value())); + + // Attempting to write or read a value results in an error. stdout is + // suppressed for the duration of this test + + int saved_stdout = dup(STDOUT_FILENO); + int devnull = open("/dev/null", O_WRONLY); + dup2(devnull, STDOUT_FILENO); + close(devnull); + + int int_val = 0; + status = int_ca.get(&int_val); + EQUAL(status, FE_ERR_DRIVER); + + double new_double_val = 4.2; + status = double_ca.put(&new_double_val); + EQUAL(status, FE_ERR_DRIVER); + + // Restore stdout + dup2(saved_stdout, STDOUT_FILENO); + close(saved_stdout); + } // Scope everything so the IOC gets torn down by the IocProcess destructor // before reporting that the tests were successfull. { - using clock = std::chrono::steady_clock; // Start the test IOC std::string path = __FILE__; @@ -92,11 +156,6 @@ int main() { std::string ioc_exe = dir + "/ioc/startioc"; IocProcess proc = IocProcess(ioc_exe); - // Create a new instance of the mEpicsCa bus driver and use it to - // interact with the spawned IOC. - mEpicsCa int_ca = mEpicsCa("TEST:INT"); - mEpicsCa double_ca = mEpicsCa("TEST:DOUBLE"); - // Wait for the IOC to become available double timeout = 20.0; @@ -111,46 +170,260 @@ int main() { auto now = clock::now(); double elapsed = std::chrono::duration(now - start).count(); if (elapsed > timeout) { - printf( - "Could not connect to IOC in %.3lf seconds. The IOC likely " - "failed to start.\n", - timeout); + printf("Could not connect to IOC in %.3lf seconds. The IOC " + "likely " + "failed to start.\n", + timeout); return -1; } }; - int status = ECA_NORMAL; + // ===================================================================== + // No subscription + // ===================================================================== - // Read an integer from a record - int int_val = 0; - status = int_ca.get(&int_val); - EQUAL(status, ECA_NORMAL); - EQUAL(int_val, 42); + { + // ----------------------------------------------------------------- + // Test get + // ----------------------------------------------------------------- - // Read a double from a record - double double_val = 0.0; - status = double_ca.get(&double_val); - EQUAL(status, ECA_NORMAL); - EQUAL(double_val, 84.0); + // Read an integer from a record + int int_val = 0; + status = int_ca.get(&int_val); + EQUAL(status, CM_SUCCESS); + EQUAL(int_val, 42); - // Write an integer to a record and read it to check - int new_int_val = 21; - status = int_ca.put(&new_int_val); - EQUAL(status, ECA_NORMAL); - status = int_ca.get(&int_val); - EQUAL(status, ECA_NORMAL); - EQUAL(int_val, new_int_val); + // Read from a different field + status = int_hopr_ca.get(&int_val); + EQUAL(status, CM_SUCCESS); + EQUAL(int_val, 100); - // Write a double to a record and read it to check - double new_double_val = 4.2; - status = double_ca.put(&new_double_val); - EQUAL(status, ECA_NORMAL); - status = double_ca.get(&double_val); - EQUAL(status, ECA_NORMAL); - EQUAL(double_val, new_double_val); + // Read a double from a record + double double_val = 0.0; + status = double_ca.get(&double_val); + EQUAL(status, CM_SUCCESS); + EQUAL(double_val, 84.0); + + // Read a string from a stringin record using a string + std::string string_val = ""; + status = string_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, "MyTestString"); + + // Read a string from a stringin record using a char array + char char_arr_val[40] = {0}; + status = string_ca.get(char_arr_val, sizeof(char_arr_val)); + EQUAL(status, CM_SUCCESS); + EQUAL(std::string(char_arr_val), "MyTestString"); + + // Read a string from a waveform record + status = waveform_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, "ABC"); + + // Read an enum as a value and as a string + uint16_t short_uint_val = 0; + status = bo_ca.get(&short_uint_val); + EQUAL(status, CM_SUCCESS); + EQUAL(short_uint_val, 1); + status = bo_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, "ON"); + + // ----------------------------------------------------------------- + // Test put + // ----------------------------------------------------------------- + + // Write an integer to a record and read it to check + int new_int_val = 21; + status = int_ca.put(&new_int_val); + EQUAL(status, CM_SUCCESS); + status = int_ca.get(&int_val); + EQUAL(status, CM_SUCCESS); + EQUAL(int_val, new_int_val); + + // Write to the limit field of the longin record + new_int_val = 40; + status = int_hopr_ca.put(&new_int_val); + EQUAL(status, CM_SUCCESS); + int_val = 0; // Overwrite with other value + status = int_hopr_ca.get(&int_val); + EQUAL(status, CM_SUCCESS); + EQUAL(int_val, new_int_val); + + // Write a double to a record and read it to check + double new_double_val = 4.2; + status = double_ca.put(&new_double_val); + EQUAL(status, CM_SUCCESS); + double_val = 0; // Overwrite with other value + status = double_ca.get(&double_val); + EQUAL(status, CM_SUCCESS); + EQUAL(double_val, new_double_val); + + // Write a string to the stringin record using a string + std::string new_string_val = "NewStringValue"; + status = string_ca.put(&new_string_val); + EQUAL(status, CM_SUCCESS); + string_val = ""; // Overwrite with other value + status = string_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, new_string_val); + + // Write a string to the stringin record using a char array + char new_char_arr_val[40] = "NewCharArrValue"; + status = string_ca.put(new_char_arr_val, + (u_long)strlen(new_char_arr_val)); + EQUAL(status, CM_SUCCESS); + char_arr_val[0] = 0; // Overwrite with other value + status = string_ca.get(char_arr_val, (u_long)strlen(char_arr_val)); + EQUAL(status, CM_SUCCESS); + EQUAL(std::string(char_arr_val), std::string(new_char_arr_val)); + + // Write to an enum with an integer value + short_uint_val = 0; + status = bo_ca.put(&short_uint_val); + EQUAL(status, CM_SUCCESS); + short_uint_val = 2; // Overwrite with other value + status = bo_ca.get(&short_uint_val); + EQUAL(status, CM_SUCCESS); + EQUAL(short_uint_val, 0); + status = bo_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, "OFF"); + + // Write to an enum with a string + std::string new_enum_string = "ON"; + status = bo_ca.put(&new_enum_string); + EQUAL(status, CM_SUCCESS); + status = bo_ca.get(&short_uint_val); + EQUAL(status, CM_SUCCESS); + EQUAL(short_uint_val, 1); + status = bo_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, "ON"); + + // Attempt to write an invalid string. stdout is suppressed for the + // duration of this test + + int saved_stdout = dup(STDOUT_FILENO); + int devnull = open("/dev/null", O_WRONLY); + dup2(devnull, STDOUT_FILENO); + close(devnull); + + new_enum_string = "INVALID"; + status = bo_ca.put(&new_enum_string); + EQUAL(status, FE_ERR_DRIVER); // Attempted to write invalid variant! + + // Assert that nothing has changed in the record + status = bo_ca.get(&short_uint_val); + EQUAL(status, CM_SUCCESS); + EQUAL(short_uint_val, 1); + status = bo_ca.get(&string_val); + EQUAL(status, CM_SUCCESS); + EQUAL(string_val, "ON"); + + // Restore stdout + dup2(saved_stdout, STDOUT_FILENO); + close(saved_stdout); + } + + // ===================================================================== + // With subscription + // ===================================================================== + + { + int int_val = 0; + int new_int_val = 13; + + // Initial status of cached + ASSERT(int_cas.cached().has_value()); + status = int_cas.get(&int_val); + EQUAL(status, CM_SUCCESS); + EQUAL(int_val, 21); // Value which was put in the record by int_ca + + status = int_cas.put(&new_int_val); + EQUAL(status, CM_SUCCESS); + + // Wait a bit to allow the subscription thread to update + double timeout = 5.0; + + auto start = clock::now(); + while (true) { + // Assert that the cached value has changed + status = int_cas.get(&int_val); + EQUAL(status, CM_SUCCESS); + + if (int_val == new_int_val) + break; + + ca_pend_event(0.1); + + auto now = clock::now(); + double elapsed = + std::chrono::duration(now - start).count(); + if (elapsed > timeout) { + printf("Failed to update int_cas value in %lf seconds.\n", + timeout); + return -1; + } + }; + } } - printf("\nTest was successful\n"); + // Wait for the IOC to go offline + double timeout = 20.0; + auto start = clock::now(); + while (true) { + if (!int_ca.connected()) { + break; + } + + ca_pend_event(0.1); + + auto now = clock::now(); + double elapsed = std::chrono::duration(now - start).count(); + if (elapsed > timeout) { + printf("Could not connect to IOC in %.3lf seconds. The IOC " + "likely " + "failed to start.\n", + timeout); + return -1; + } + }; + + { + // ===================================================================== + // Tests after IOC has stopped + // ===================================================================== + + // Drivers are not connected + ASSERT((!int_ca.connected())); + + // Cached values are invalid + ASSERT((!int_cas.cached().has_value())); + + // Attempting to write or read a value results in an error. stdout + // is suppressed for the duration of this test + + int saved_stdout = dup(STDOUT_FILENO); + int devnull = open("/dev/null", O_WRONLY); + dup2(devnull, STDOUT_FILENO); + close(devnull); + + int int_val = 0; + status = int_ca.get(&int_val); + EQUAL(status, FE_ERR_DRIVER); + + double new_double_val = 4.2; + status = double_ca.put(&new_double_val); + EQUAL(status, FE_ERR_DRIVER); + + // Restore stdout + dup2(saved_stdout, STDOUT_FILENO); + close(saved_stdout); + } + + printf("Test was successful.\n"); return 0; } \ No newline at end of file