Files
mepicsca/bus/m_epics_ca.tpp
T

510 lines
17 KiB
C++

#include "cadef.h"
#include "dbDefs.h"
#include "midas.h"
#include <cstring>
#include <string>
#include <iostream>
#include <stdio.h>
// Helper struct for dbfFromType
template <typename> struct alwaysFalse : std::false_type {};
template <typename T> struct dbf_type;
template <> struct dbf_type<int> {
static constexpr int value = DBF_INT;
};
template <> struct dbf_type<double> {
static constexpr int value = DBF_DOUBLE;
};
template <> struct dbf_type<char *> {
static constexpr int value = DBF_STRING;
};
template <> struct dbf_type<const char *> {
static constexpr int value = DBF_STRING;
};
template <> struct dbf_type<std::string> {
static constexpr int value = DBF_STRING;
};
template <> struct dbf_type<uint16_t> {
static constexpr int value = DBF_ENUM;
};
template <typename T> constexpr int dbfFromType() { return dbf_type<T>::value; }
template <typename T, typename V> constexpr void assertEqual() {
static_assert(dbfFromType<T>() == dbfFromType<V>());
}
/**
* @brief Handle the ECA (error channel access) code returned by the ca_get and
* 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 EPRICS status code
* @param pChanName: Name of the EPICS channel
* @return int Corresponding MIDAS status code
*/
int convertAndHandleEcaCode(int status, const char *pChanName) {
switch (status) {
case ECA_NORMAL:
return CM_SUCCESS;
case ECA_BADCHID:
cm_msg(MERROR, __FILE__,
"PV '%s': Channel ID has been corrupted. Restart the IOC.",
pChanName);
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);
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);
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);
return FE_ERR_DRIVER;
case ECA_NOWTACCESS:
cm_msg(MERROR, __FILE__,
"PV '%s': Write access denied. Please call the support.",
pChanName);
return FE_ERR_DRIVER;
case ECA_ALLOCMEM:
cm_msg(MERROR, __FILE__,
"PV '%s': Unable to allocate memory. Please call the support.",
pChanName);
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);
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);
return CM_TIMEOUT;
default:
cm_msg(MERROR, __FILE__, "PV '%s': Received unknown error code %d.",
pChanName, status);
return FE_ERR_DRIVER;
}
}
template <typename T>
mEpicsCa<T>::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<T>();
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
convertAndHandleEcaCode. Preemptive callbacks are disabled by default for
this driver.
*/
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
// default callback from the method connStateCallback.
if (pConnStateCallback == NULL) {
pConnStateCallback = &mEpicsCa::connStateCallback;
pUserPrivate = this;
}
// 0 is the lowest network priority, 99 is the highest.
int priority = 0;
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<T>::value, 1, this->_pChanID, DBE_VALUE,
&mEpicsCa<T>::eventCallback, this, nullptr);
}
SEVCHK(status, "ca_create_channel");
}
template <typename T>
void mEpicsCa<T>::eventCallback(struct event_handler_args args) {
auto *self = static_cast<mEpicsCa<T> *>(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<T, std::string>) {
// 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<const char *>(args.dbr);
} else {
self->_cached = *static_cast<const T *>(args.dbr);
}
}
template <typename T> const std::optional<T> &mEpicsCa<T>::cached() const {
return _cached;
}
template <typename T>
mEpicsCa<T>::mEpicsCa(const char *pChanName, bool subscribe)
: mEpicsCa(pChanName, subscribe, nullptr, nullptr) {}
template <typename T> mEpicsCa<T>::~mEpicsCa() {
// Free up the channel resources
convertAndHandleEcaCode(ca_clear_channel(this->_pChanID),
this->_chanName.c_str());
}
template <typename T> template <typename V> int mEpicsCa<T>::get(V *value) {
// If "this" has the type mEpicsCa<uint16_t>, then V needs to be a string
// or an uint16_t.
if constexpr (dbfFromType<T>() == DBF_ENUM) {
constexpr auto t = dbfFromType<T>();
constexpr auto v = dbfFromType<V>();
static_assert((t == v) || (t == DBF_ENUM && v == DBF_STRING) ||
(t == DBF_STRING && v == DBF_ENUM),
"Incompatible EPICS types");
} else {
assertEqual<T, V>();
}
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<V, std::string>) {
char buf[40] = {0};
int status = getRaw(buf, sizeof(buf));
if (status == CM_SUCCESS)
*value = buf;
return status;
} else {
return getRaw(value);
}
}
template <typename T> int mEpicsCa<T>::get(char *buf, u_long len) {
// If "this" has the type mEpicsCa<uint16_t>, then the type check is omitted
if (dbfFromType<T>() != DBF_ENUM) {
assertEqual<T, char *>();
}
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<T, std::string>) {
strncpy(buf, _cached.value().c_str(), len);
} else {
strncpy(buf, _cached.value(), len);
}
return CM_SUCCESS;
}
return getRaw(buf, len);
}
template <typename T> int mEpicsCa<T>::getRaw(int *value) {
int status = convertAndHandleEcaCode(
ca_get(dbfFromType<int>(), 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 <typename T> int mEpicsCa<T>::getRaw(double *value) {
int status = convertAndHandleEcaCode(
ca_get(dbfFromType<double>(), 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 <typename T> int mEpicsCa<T>::getRaw(uint16_t *value) {
int status = convertAndHandleEcaCode(
ca_get(dbfFromType<uint16_t>(), 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 <typename T> int mEpicsCa<T>::getRaw(char *buf, u_long len) {
int status = CM_SUCCESS;
if (_channelInfo.value().count == 1) {
// String record
status = convertAndHandleEcaCode(
ca_get(dbfFromType<char *>(), 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 <typename T> template <typename V> int mEpicsCa<T>::put(V *value) {
// If "this" has the type mEpicsCa<uint16_t>, then V needs to be a string or
// an integer
if constexpr (dbfFromType<T>() == DBF_ENUM) {
constexpr auto t = dbfFromType<T>();
constexpr auto v = dbfFromType<V>();
static_assert((t == v) || (t == DBF_ENUM && v == DBF_STRING) ||
(t == DBF_STRING && v == DBF_ENUM),
"Incompatible EPICS types");
} else {
assertEqual<T, V>();
}
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<V, std::string>) {
// Use the c-style string buffer put algorithm
return putRaw(value->c_str(), value->size());
} else {
return putRaw(value);
}
}
template <typename T> int mEpicsCa<T>::put(const char *buf, u_long len) {
// If "this" has the type mEpicsCa<uint16_t>, then the type check is omitted
if constexpr (dbfFromType<T>() != DBF_ENUM) {
assertEqual<T, char *>();
}
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 <typename T> int mEpicsCa<T>::putRaw(double *value) {
int status = convertAndHandleEcaCode(
ca_put(dbfFromType<double>(), 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 <typename T> int mEpicsCa<T>::putRaw(u_int16_t *value) {
int status = convertAndHandleEcaCode(
ca_put(dbfFromType<uint16_t>(), 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 <typename T> int mEpicsCa<T>::putRaw(const char *buf, u_long len) {
if constexpr (dbfFromType<T>() == 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<char *>(), 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 <typename T> int mEpicsCa<T>::putRaw(int *value) {
int status = convertAndHandleEcaCode(
ca_put(dbfFromType<int>(), 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 <typename T>
void mEpicsCa<T>::connStateCallback(struct connection_handler_args args) {
mEpicsCa *self = static_cast<mEpicsCa *>(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 <typename T> bool mEpicsCa<T>::connected() {
return _channelInfo.has_value();
}