510 lines
17 KiB
C++
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();
|
|
} |