WIP state for Hugo to potentially fix a segfault

This commit is contained in:
2026-04-20 14:24:50 +02:00
parent 87003df25c
commit 28e5cd4ec4
7 changed files with 771 additions and 163 deletions
+44 -8
View File
@@ -1,6 +1,21 @@
#ifndef mEpicsCa_H
#define mEpicsCa_H
#include "cadef.h"
#include <optional>
#include <string>
/**
* @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 <typename T> class mEpicsCa {
public:
/**
@@ -12,25 +27,25 @@ template <typename T> 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 <typename V> int put(V *value, double timeout);
template <typename V> int get(V *value);
int get(char *buf, u_long len);
template <typename V> int put(V *value);
template <typename V> int get(V *value);
template <typename V> int get(V *value, double timeout);
int put(const char *buf, u_long len);
bool connected();
double _timeout;
const std::optional<T> &cached() const;
private:
/**
* @brief A callback for reacting to channel access channel state changes.
@@ -44,8 +59,29 @@ template <typename T> 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<ChInfo> _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<T> _cached;
};
#include "m_epics_ca.tpp"
#include "m_epics_ca.tpp"
#endif
+378 -98
View File
@@ -1,6 +1,7 @@
#include "cadef.h"
#include "dbDefs.h"
#include "midas.h"
#include <cstring>
#include <string>
#include <iostream>
@@ -9,38 +10,33 @@
// Helper struct for dbfFromType
template <typename> 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 <typename T> constexpr int dbfFromType() {
if constexpr (std::is_same_v<T, char *> ||
std::is_same_v<T, const char *> ||
std::is_same_v<T, std::string *>) {
return DBF_STRING;
} else if constexpr (std::is_same_v<T, int>) {
return DBF_INT; // Same as DBF_SHORT
} else if constexpr (std::is_same_v<T, float>) {
return DBF_FLOAT;
} else if constexpr (std::is_enum_v<T>) {
return DBF_ENUM;
} else if constexpr (std::is_same_v<T, char>) {
return DBF_CHAR;
} else if constexpr (std::is_same_v<T, long>) {
return DBF_LONG;
} else if constexpr (std::is_same_v<T, double>) {
return DBF_DOUBLE;
} else {
static_assert(alwaysFalse<T>::value,
"Unsupported EPICS database field type. See "
"epics-base/src/ca/client/db_access.h for the list "
"of allowed types");
}
}
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>());
@@ -48,93 +44,111 @@ template <typename T, typename V> 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 <typename T>
mEpicsCa<T>::mEpicsCa(const char *pChanName, caCh *pConnStateCallback,
void *pUserPrivate, double timeout)
: _timeout(timeout), _pChanID(nullptr), _chanName(pChanName) {
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 = 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<T>::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<T>::value, 1, this->_pChanID, DBE_VALUE,
&mEpicsCa<T>::eventCallback, this, nullptr);
}
SEVCHK(status, "ca_create_channel");
}
template <typename T>
mEpicsCa<T>::mEpicsCa(const char *pChanName)
: mEpicsCa(pChanName, nullptr, nullptr) {}
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
ca_clear_channel(this->_pChanID);
}
template <typename T> template <typename V> int mEpicsCa<T>::put(V *value) {
return put(value, _timeout);
}
template <typename T>
template <typename V>
int mEpicsCa<T>::put(V *value, double timeout) {
assertEqual<T, V>();
int dbfType = dbfFromType<V>();
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 <typename T> template <typename V> int mEpicsCa<T>::get(V *value) {
return get(value, _timeout);
// 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>
template <typename V>
int mEpicsCa<T>::get(V *value, double timeout) {
assertEqual<T, V>();
int dbfType = dbfFromType<V>();
template <typename T> int mEpicsCa<T>::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<uint16_t>, then the type check is omitted
if (dbfFromType<T>() != DBF_ENUM) {
assertEqual<T, char *>();
}
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<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
status = ca_pend_io(timeout);
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
handleEcaCode(status, this->_chanName.c_str());
return status;
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>
@@ -219,12 +493,18 @@ 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 _pChanID && ca_state(_pChanID) == cs_conn;
return _channelInfo.has_value();
}